前端:Vue3 对比 Vue2 有哪些变化?


prtyaa
prtyaa 2023-12-25 11:32:12 63136
分类专栏: 资讯

前言

希望本篇文章能帮你加深对 Vue 的理解,能信誓旦旦地说自己熟练Vue2/3。

内容混杂用法 + 原理 + 使用小心得,建议收藏,慢慢看。

区别

生命周期的变化

整体来看,变化不大,只是名字大部分需要 + on,功能上类似。使用上 Vue3 组合式 API 需要先引入;Vue2 选项 API 则可直接调用,如下所示。

//vue3

<scriptsetup>

import{onMounted}from'vue'

 

onMounted(()=>{

...

})

//可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不被覆盖

onMounted(()=>{

...

})

</script>

 

//vue2

<script>

exportdefault{

mounted(){

...

},

}

</script>

 

常用生命周期表格如下所示。

Tips: setup是围绕beforeCreatecreated生命周期钩子运行的,所以不需要显式地去定义。

多根节点

Vue3 支持了多根节点组件,也就是fragment

Vue2中,编写页面的时候,我们需要去将组件包裹在<div>中,否则报错警告。

<template>

<div>

<header>...</header>

<main>...</main>

<footer>...</footer>

</div>

</template>

Vue3,我们可以组件包含多个根节点,可以少写一层,niceeee !

<template>

<header>...</header>

<main>...</main>

<footer>...</footer>

</template>

异步组件

Vue3 提供 Suspense组件,允许程序在等待异步组件时渲染兜底的内容,如 loading ,使用户体验更平滑。 使用它,需在模板中声明,并包括两个命名插槽:defaultfallbackSuspense确保加载完异步内容时显示默认插槽,并将fallback插槽用作加载状态。

<tempalte>

<suspense>

<template#default>

<todo-list/>

</template>

<template#fallback>

<div>

Loading...

</div>

</template>

</suspense>

</template>

真实的项目中踩过坑,若想在 setup 中调用异步请求,需在 setup 前加async关键字。这时,会受到警告async setup() is used without a suspense boundary

解决方案:在父页面调用当前组件外包裹一层Suspense组件。

Teleport

Vue3 提供Teleport组件可将部分DOM移动到 Vue app之外的位置。比如项目中常见的Dialog组件。

<button@click="dialogVisible=true">点击</button>

<teleportto="body">

<divclass="dialog"v-if="dialogVisible">

</div>

</teleport>

组合式API

Vue2 是 选项式API(Option API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期函数等),导致代码的可读性变差,需要上下来回跳转文件位置。Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起。

除了增强了代码的可读性、内聚性,组合式API 还提供了较为完美的逻辑复用性方案,举个 ,如下所示公用鼠标坐标案例。

//main.vue

<template>

<span>mouseposition{{x}}{{y}}</span>

</template>

 

<scriptsetup>

import{ref}from'vue'

importuseMousePositionfrom'./useMousePosition'

 

const{x,y}=useMousePosition()

 

}

</script>

//useMousePosition.js

import{ref,onMounted,onUnmounted}from'vue'

 

functionuseMousePosition(){

letx=ref(0)

lety=ref(0)

 

functionupdate(e){

x.value=e.pageX

y.value=e.pageY

}

 

onMounted(()=>{

window.addEventListener('mousemove',update)

})

 

onUnmounted(()=>{

window.removeEventListener('mousemove',update)

})

 

return{

x,

y

}

}

</script>

解决了 Vue2 Mixin的存在的命名冲突隐患,依赖关系不明确,不同组件间配置化使用不够灵活。

响应式原理

Vue2 响应式原理基础是Object.defineProperty;Vue3 响应式原理基础是 Proxy

Object.defineProperty

基本用法:直接在一个对象上定义新的属性或修改现有的属性,并返回对象。

Tips: writable 和 value 与 getter 和 setter 不共存。

letobj={}

letname='瑾行'

Object.defineProperty(obj,'name',{

enumerable:true,//可枚举(是否可通过for...in或Object.keys()进行访问)

configurable:true,//可配置(是否可使用delete删除,是否可再次设置属性)

//value:'',//任意类型的值,默认undefined

//writable:true,//可重写

get:function(){

returnname

},

set:function(value){

name=value

}

})

搬运 Vue2 核心源码,略删减。

functiondefineReactive(obj,key,val){

//一key一个dep

constdep=newDep()

 

//获取key的属性描述符,发现它是不可配置对象的话直接return

constproperty=Object.getOwnPropertyDescriptor(obj,key)

if(property&&property.configurable===false){return}

 

//获取getter和setter,并获取val值

constgetter=property&&property.get

constsetter=property&&property.set

if((!getter||setter)&&arguments.length===2){val=obj[key]}

 

//递归处理,保证对象中所有key被观察

letchildOb=observe(val)

 

Object.defineProperty(obj,key,{

enumerable:true,

configurable:true,

//get劫持obj[key]的进行依赖收集

get:functionreactiveGetter(){

constvalue=getter?getter.call(obj):val

if(Dep.target){

//依赖收集

dep.depend()

if(childOb){

//针对嵌套对象,依赖收集

childOb.dep.depend()

//触发数组响应式

if(Array.isArray(value)){

dependArray(value)

}

}

}

}

returnvalue

})

//set派发更新obj[key]

set:functionreactiveSetter(newVal){

...

if(setter){

setter.call(obj,newVal)

}else{

val=newVal

}

//新值设置响应式

childOb=observe(val)

//依赖通知更新

dep.notify()

}

}

那 Vue3 为何会抛弃它呢?那肯定是有一些缺陷的。

主要原因:无法监听对象或数组新增、删除的元素。
Vue2 方案:针对常用数组原型方法pushpopshiftunshiftsplicesortreverse进行了hack处理;提供Vue.set监听对象/数组新增属性。对象的新增/删除响应,还可以new个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。

Tips: Object.defineOProperty是可以监听数组已有元素,但 Vue2 没有提供的原因是性能问题,具体可看见参考第二篇 ~。

Proxy

Proxy是ES6新特性,通过第2个参数handler拦截目标对象的行为。相较于Object.defineProperty提供语言全范围的响应能力,消除了局限性。但在兼容性上放弃了(IE11以下)

局限性

  1. 对象/数组的新增、删除。
  2. 监测.length修改。
  3. Map、Set、WeakMap、WeakSet的支持。

基本用法:创建对象的代理,从而实现基本操作的拦截和自定义操作。

consthandler={

get:function(obj,prop){

returnpropinobj?obj[prop]:''

},

set:function(){},

...

}

搬运 Vue3 的源码 reactive.ts 文件

functioncreateReactiveObject(target,isReadOnly,baseHandlers,collectionHandlers,proxyMap){

...

//collectionHandlers:处理Map、Set、WeakMap、WeakSet

//baseHandlers:处理数组、对象

constproxy=newProxy(

target,

targetType===TargetType.COLLECTION?collectionHandlers:baseHandlers

)

proxyMap.set(target,proxy)

returnproxy

}

以 baseHandlers.ts 为例,使用Reflect.get而不是target[key]的原因是receiver参数可以把this指向getter调用时,而非Proxy构造时的对象。

//依赖收集

functioncreateGetter(isReadonly=false,shallow=false){

returnfunctionget(target:Target,key:string|symbol,receiver:object){

...

//数组类型

consttargetIsArray=isArray(target)

if(!isReadonly&&targetIsArray&&hasOwn(arrayInstrumentations,key)){

returnReflect.get(arrayInstrumentations,key,receiver)

}

//非数组类型

constres=Reflect.get(target,key,receiver);

 

//对象递归调用

if(isObject(res)){

returnisReadonly?readonly(res):reactive(res)

}

 

returnres

}

}

//派发更新

functioncreateSetter(){

returnfunctionset(target:Target,key:string|symbol,value:unknown,receiver:Object){

value=toRaw(value)

oldValue=target[key]

//因ref数据在setvalue时就已trigger依赖了,所以直接赋值return即可

if(!isArray(target)&&isRef(oldValue)&&!isRef(value)){

oldValue.value=value

returntrue

}

 

//对象是否有key有keyset,无keyadd

consthadKey=hasOwn(target,key)

constresult=Reflect.set(target,key,value,receiver)

 

if(target===toRaw(receiver)){

if(!hadKey){

trigger(target,TriggerOpTypes.ADD,key,value)

}elseif(hasChanged(value,oldValue)){

trigger(target,TriggerOpTypes.SET,key,value,oldValue)

}

}

returnresult

}

}

虚拟DOM

Vue3 相比于 Vue2 虚拟DOM 上增加patchFlag字段。我们借助Vue3 Template Explorer来看。

<divid="app">

<h1>技术摸鱼</h1>

<p>今天天气真不错</p>

<div>{{name}}</div>

</div>

渲染函数如下。

import{createElementVNodeas_createElementVNode,toDisplayStringas_toDisplayString,openBlockas_openBlock,createElementBlockas_createElementBlock,pushScopeIdas_pushScopeId,popScopeIdas_popScopeId}from"vue"

 

const_withScopeId=n=>(_pushScopeId("scope-id"),n=n(),_popScopeId(),n)

const_hoisted_1={id:"app"}

const_hoisted_2=/*#__PURE__*/_withScopeId(()=>/*#__PURE__*/_createElementVNode("h1",null,"技术摸鱼",-1/*HOISTED*/))

const_hoisted_3=/*#__PURE__*/_withScopeId(()=>/*#__PURE__*/_createElementVNode("p",null,"今天天气真不错",-1/*HOISTED*/))

 

exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){

return(_openBlock(),_createElementBlock("div",_hoisted_1,[

_hoisted_2,

_hoisted_3,

_createElementVNode("div",null,_toDisplayString(_ctx.name),1/*TEXT*/)

]))

}

注意第 3 个_createElementVNode的第 4 个参数即patchFlag字段类型,字段类型情况如下所示。1 代表节点为动态文本节点,那在 diff 过程中,只需比对文本对容,无需关注 class、style等。除此之外,发现所有的静态节点,都保存为一个变量进行静态提升,可在重新渲染时直接引用,无需重新创建。

exportconstenumPatchFlags{

TEXT=1,//动态文本内容

CLASS=1<<1,//动态类名

STYLE=1<<2,//动态样式

PROPS=1<<3,//动态属性,不包含类名和样式

FULL_PROPS=1<<4,//具有动态key属性,当key改变,需要进行完整的diff比较

HYDRATE_EVENTS=1<<5,//带有监听事件的节点

STABLE_FRAGMENT=1<<6,//不会改变子节点顺序的fragment

KEYED_FRAGMENT=1<<7,//带有key属性的fragment或部分子节点

UNKEYED_FRAGMENT=1<<8,//子节点没有key的fragment

NEED_PATCH=1<<9,//只会进行非props的比较

DYNAMIC_SLOTS=1<<10,//动态的插槽

HOISTED=-1,//静态节点,diff阶段忽略其子节点

BAIL=-2//代表diff应该结束

}

事件缓存

Vue3 的 cacheHandler可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数。加一个click事件。

<divid="app">

<h1>技术摸鱼</h1>

<p>今天天气真不错</p>

<div>{{name}}</div>

<spanonCLick="()=>{}"><span>

</div>

渲染函数如下

import{createElementVNodeas_createElementVNode,toDisplayStringas_toDisplayString,openBlockas_openBlock,createElementBlockas_createElementBlock,pushScopeIdas_pushScopeId,popScopeIdas_popScopeId}from"vue"

 

const_withScopeId=n=>(_pushScopeId("scope-id"),n=n(),_popScopeId(),n)

const_hoisted_1={id:"app"}

const_hoisted_2=/*#__PURE__*/_withScopeId(()=>/*#__PURE__*/_createElementVNode("h1",null,"技术摸鱼",-1/*HOISTED*/))

const_hoisted_3=/*#__PURE__*/_withScopeId(()=>/*#__PURE__*/_createElementVNode("p",null,"今天天气真不错",-1/*HOISTED*/))

const_hoisted_4=/*#__PURE__*/_withScopeId(()=>/*#__PURE__*/_createElementVNode("span",{onCLick:"()=>{}"},[

/*#__PURE__*/_createElementVNode("span")

],-1/*HOISTED*/))

 

exportfunctionrender(_ctx,_cache,$props,$setup,$data,$options){

return(_openBlock(),_createElementBlock("div",_hoisted_1,[

_hoisted_2,

_hoisted_3,

_createElementVNode("div",null,_toDisplayString(_ctx.name),1/*TEXT*/),

_hoisted_4

]))

}

Diff 优化

搬运 Vue3 patchChildren 源码。结合上文与源码,patchFlag帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。

functionpatchChildren(n1,n2,container,parentAnchor,parentComponent,parentSuspense,isSVG,optimized){

//获取新老孩子节点

constc1=n1&&n1.children

constc2=n2.children

constprevShapeFlag=n1?n1.shapeFlag:0

const{patchFlag,shapeFlag}=n2

 

//处理patchFlag大于0

if(patchFlag>0){

if(patchFlag&&PatchFlags.KEYED_FRAGMENT){

//存在key

patchKeyedChildren()

return

}elsif(patchFlag&&PatchFlags.UNKEYED_FRAGMENT){

//不存在key

patchUnkeyedChildren()

return

}

}

 

//匹配是文本节点(静态):移除老节点,设置文本节点

if(shapeFlag&&ShapeFlags.TEXT_CHILDREN){

if(prevShapeFlag&ShapeFlags.ARRAY_CHILDREN){

unmountChildren(c1asVNode[],parentComponent,parentSuspense)

}

if(c2!==c1){

hostSetElementText(container,c2asstring)

}

}else{

//匹配新老Vnode是数组,则全量比较;否则移除当前所有的节点

if(prevShapeFlag&ShapeFlags.ARRAY_CHILDREN){

if(shapeFlag&ShapeFlags.ARRAY_CHILDREN){

patchKeyedChildren(c1,c2,container,anchor,parentComponent,parentSuspense,...)

}else{

unmountChildren(c1asVNode[],parentComponent,parentSuspense,true)

}

}else{

 

if(prevShapeFlag&ShapeFlags.TEXT_CHILDREN){

hostSetElementText(container,'')

}

if(shapeFlag&ShapeFlags.ARRAY_CHILDREN){

mountChildren(c2asVNodeArrayChildren,container,anchor,parentComponent,...)

}

}

}

}

patchUnkeyedChildren 源码如下。

functionpatchUnkeyedChildren(c1,c2,container,parentAnchor,parentComponent,parentSuspense,isSVG,optimized){

c1=c1||EMPTY_ARR

c2=c2||EMPTY_ARR

constoldLength=c1.length

constnewLength=c2.length

constcommonLength=Math.min(oldLength,newLength)

leti

for(i=0;i<commonLength;i++){

//如果新Vnode已经挂载,则直接clone一份,否则新建一个节点

constnextChild=(c2[i]=optimized?cloneIfMounted(c2[i]asVnode)):normalizeVnode(c2[i])

patch()

}

if(oldLength>newLength){

//移除多余的节点

unmountedChildren()

}else{

//创建新的节点

mountChildren()

}

 

}

patchKeyedChildren源码如下,有运用最长递增序列的算法思想。

functionpatchKeyedChildren(c1,c2,container,parentAnchor,parentComponent,parentSuspense,isSVG,optimized){

leti=0;

conste1=c1.length-1

conste2=c2.length-1

constl2=c2.length

 

//从头开始遍历,若新老节点是同一节点,执行patch更新差异;否则,跳出循环

while(i<=e1&&i<=e2){

constn1=c1[i]

constn2=c2[i]

 

if(isSameVnodeType){

patch(n1,n2,container,parentAnchor,parentComponent,parentSuspense,isSvg,optimized)

}else{

break

}

i++

}

 

//从尾开始遍历,若新老节点是同一节点,执行patch更新差异;否则,跳出循环

while(i<=e1&&i<=e2){

constn1=c1[e1]

constn2=c2[e2]

if(isSameVnodeType){

patch(n1,n2,container,parentAnchor,parentComponent,parentSuspense,isSvg,optimized)

}else{

break

}

e1--

e2--

}

 

//仅存在需要新增的节点

if(i>e1){

if(i<=e2){

constnextPos=e2+1

constanchor=nextPos<l2?c2[nextPos]:parentAnchor

while(i<=e2){

patch(null,c2[i],container,parentAnchor,parentComponent,parentSuspense,isSvg,optimized)

}

}

}

 

//仅存在需要删除的节点

elseif(i>e2){

while(i<=e1){

unmount(c1[i],parentComponent,parentSuspense,true)

}

}

 

//新旧节点均未遍历完

//[i...e1+1]:ab[cde]fg

//[i...e2+1]:ab[edch]fg

//i=2,e1=4,e2=5

else{

consts1=i

consts2=i

//缓存新Vnode剩余节点上例即{e:2,d:3,c:4,h:5}

constkeyToNewIndexMap=newMap()

for(i=s2;i<=e2;i++){

constnextChild=(c2[i]=optimized

?cloneIfMounted(c2[i]asVNode)

:normalizeVNode(c2[i]))

 

if(nextChild.key!=null){

if(__DEV__&&keyToNewIndexMap.has(nextChild.key)){

warn(

`Duplicatekeysfoundduringupdate:`,

JSON.stringify(nextChild.key),

`Makesurekeysareunique.`

)

}

keyToNewIndexMap.set(nextChild.key,i)

}

}

}

 

letj=0

//记录即将patch的新Vnode数量

letpatched=0

//新Vnode剩余节点长度

consttoBePatched=e2-s2+1

//是否移动标识

letmoved=false

letmaxNewindexSoFar=0

 

//初始化新老节点的对应关系(用于后续最大递增序列算法)

constnewIndexToOldIndexMap=newArray(toBePatched)

for(i=0;i<toBePatched;i++)newIndexToOldIndexMap[i]=0

 

//遍历老Vnode剩余节点

for(i=s1;i<=e1;i++){

constprevChild=c1[i]

 

//代表当前新Vnode都已patch,剩余旧Vnode移除即可

if(patched>=toBePatched){

unmount(prevChild,parentComponent,parentSuspense,true)

continue

}

 

letnewIndex

//旧Vnode存在key,则从keyToNewIndexMap获取

if(prevChild.key!=null){

newIndex=keyToNewIndexMap.get(prevChild.key)

//旧Vnode不存在key,则遍历新Vnode获取

}else{

for(j=s2;j<=e2;j++){

if(newIndexToOldIndexMap[j-s2]===0&&isSameVNodeType(prevChild,c2[j]asVNode)){

newIndex=j

break

}

}

}

 

//删除、更新节点

//新Vnode没有当前节点,移除

if(newIndex===undefined){

unmount(prevChild,parentComponent,parentSuspense,true)

}else{

//旧Vnode的下标位置+1,存储到对应新Vnode的Map中

//+1处理是为了防止数组首位下标是0的情况,因为这里的0代表需创建新节点

newIndexToOldIndexMap[newIndex-s2]=i+1

 

//若不是连续递增,则代表需要移动

if(newIndex>=maxNewIndexSoFar){

maxNewIndexSoFar=newIndex

}else{

moved=true

}

 

patch(prevChild,c2[newIndex],...)

patched++

}

}

 

//遍历结束,newIndexToOldIndexMap={0:5,1:4,2:3,3:0}

//新建、移动节点

constincreasingNewIndexSequence=moved

//获取最长递增序列

?getSequence(newIndexToOldIndexMap)

:EMPTY_ARR

 

j=increasingNewIndexSequence.length-1

 

for(i=toBePatched-1;i>=0;i--){

constnextIndex=s2+i

constnextChild=c2[nextIndex]asVNode

constanchor=extIndex+1<l2?(c2[nextIndex+1]asVNode).el:parentAnchor

//0新建Vnode

if(newIndexToOldIndexMap[i]===0){

patch(null,nextChild,...)

}elseif(moved){

//移动节点

if(j<0||i!==increasingNewIndexSequence[j]){

move(nextChild,container,anchor,MoveType.REORDER)

}else{

j--

}

}

}

}

打包优化

tree-shaking:模块打包webpackrollup等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于importexport语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。

nextTick为例子,在 Vue2 中,全局 API 暴露在 Vue 实例上,即使未使用,也无法通过tree-shaking进行消除。

importVuefrom'vue'

 

Vue.nextTick(()=>{

//一些和DOM有关的东西

})

Vue3 中针对全局 和内部的API进行了重构,并考虑到tree-shaking的支持。因此,全局 API 现在只能作为ES模块构建的命名导出进行访问。

import{nextTick}from'vue'

 

nextTick(()=>{

//一些和DOM有关的东西

})

通过这一更改,只要模块绑定器支持tree-shaking,则 Vue 应用程序中未使用的api将从最终的捆绑包中消除,获得最佳文件大小。受此更改影响的全局API有如下。

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅全构建)
  • Vue.set (仅兼容构建)
  • Vue.delete (仅兼容构建)

内部 API 也有诸如 transition、v-model等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。

根据 尤大 直播可以知道如今 Vue3 将所有运行功能打包也只有22.5kb,比 Vue2 轻量很多。

自定义渲染API

Vue3 提供的createApp默认是将 template 映射成 html。但若想生成canvas时,就需要使用custom renderer api自定义render生成函数。

//自定义runtime-render函数

import{createApp}from'./runtime-render'

importAppfrom'./src/App'

 

createApp(App).mount('#app')

TypeScript 支持

Vue3 由TS重写,相对于 Vue2 有更好地TypeScript支持。

  • Vue2 Option API中 option 是个简单对象,而TS是一种类型系统,面向对象的语法,不是特别匹配。
  • Vue2 需要vue-class-component强化vue原生组件,也需要vue-property-decorator增加更多结合Vue特性的装饰器,写法比较繁琐。

周边

列举一些 Vue3 配套产物,具体Composition API新语法可见官方迁移文档,参考中有链接~ 。

  • vue-cli 4.5.0
  • Vue Router 4.0
  • Vuex 4.0
  • Element plus
  • Vite

网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。

本文链接:https://www.xckfsq.com/news/show.html?id=30044
赞同 0
评论 0 条
prtyaaL0
粉丝 1 发表 2554 + 关注 私信
上周热门
银河麒麟添加网络打印机时,出现“client-error-not-possible”错误提示  1448
银河麒麟打印带有图像的文档时出错  1365
银河麒麟添加打印机时,出现“server-error-internal-error”  1151
统信桌面专业版【如何查询系统安装时间】  1073
统信操作系统各版本介绍  1070
统信桌面专业版【全盘安装UOS系统】介绍  1028
麒麟系统也能完整体验微信啦!  984
统信【启动盘制作工具】使用介绍  627
统信桌面专业版【一个U盘做多个系统启动盘】的方法  575
信刻全自动档案蓝光光盘检测一体机  484
本周热议
我的信创开放社区兼职赚钱历程 40
今天你签到了吗? 27
信创开放社区邀请他人注册的具体步骤如下 15
如何玩转信创开放社区—从小白进阶到专家 15
方德桌面操作系统 14
我有15积分有什么用? 13
用抖音玩法闯信创开放社区——用平台宣传企业产品服务 13
如何让你先人一步获得悬赏问题信息?(创作者必看) 12
2024中国信创产业发展大会暨中国信息科技创新与应用博览会 9
中央国家机关政府采购中心:应当将CPU、操作系统符合安全可靠测评要求纳入采购需求 8

添加我为好友,拉您入交流群!

请使用微信扫一扫!