前言
希望本篇文章能帮你加深对 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
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地去定义。
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 ,使用户体验更平滑。 使用它,需在模板中声明,并包括两个命名插槽:default
和fallback
。Suspense
确保加载完异步内容时显示默认插槽,并将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
组件。
Vue3 提供Teleport
组件可将部分DOM移动到 Vue app之外的位置。比如项目中常见的Dialog
组件。
<button@click="dialogVisible=true">点击</button>
<teleportto="body">
<divclass="dialog"v-if="dialogVisible">
</div>
</teleport>
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
。
基本用法:直接在一个对象上定义新的属性或修改现有的属性,并返回对象。
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 方案:针对常用数组原型方法push
、pop
、shift
、unshift
、splice
、sort
、reverse
进行了hack处理;提供Vue.set
监听对象/数组新增属性。对象的新增/删除响应,还可以new
个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。
Tips: Object.defineOProperty
是可以监听数组已有元素,但 Vue2 没有提供的原因是性能
问题,具体可看见参考第二篇 ~。
Proxy
是ES6新特性,通过第2个参数handler
拦截目标对象的行为。相较于Object.defineProperty
提供语言全范围的响应能力,消除了局限性。但在兼容性上放弃了(IE11以下)
局限性
基本用法:创建对象的代理,从而实现基本操作的拦截和自定义操作。
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
}
}
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
]))
}
搬运 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:模块打包webpack
、rollup
等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于import
和export
语句,用来检测代码模块是否被导出、导入,且被 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有如下。
内部 API 也有诸如 transition、v-model等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。
根据 尤大 直播可以知道如今 Vue3 将所有运行功能打包也只有22.5kb
,比 Vue2 轻量很多。
Vue3 提供的createApp
默认是将 template 映射成 html。但若想生成canvas
时,就需要使用custom renderer api
自定义render生成函数。
//自定义runtime-render函数
import{createApp}from'./runtime-render'
importAppfrom'./src/App'
createApp(App).mount('#app')
Vue3 由TS重写,相对于 Vue2 有更好地TypeScript
支持。
Option API
中 option 是个简单对象,而TS是一种类型系统,面向对象的语法,不是特别匹配。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
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
添加我为好友,拉您入交流群!
请使用微信扫一扫!