Vue3新特性
-
composition API。
不同于Vue2的选项式API,组合式API将组件初始化时使用setup方法,包含所有数据、方法,并统一返回。
举个例子:
Vue3 composition API: <script> export default{ setup(){ const count =ref (0); const add=()=>{ count.value++; } return { count, add } } } </script>
-
基于Proxy的响应式。
Vue2中响应式数据是通过ES5的数据劫持setter/getter 来实现的,使用的是object.defineproperty,但只能对现有的对象属性进行劫持,针对的是对象上的属性,尽管做一些重写也不能完全覆盖数据变更的地方(比如动态向对象上添加Key,或通过下标的方式修改数组。)
在Vue3中响应式数据使用的是Proxy,针对的是整个对象,直接代理对象,修改代理对象时拦截数据的变化。
const data={} const dataProxy=new Proxy(data,{ set(target,key,val){ //代理对象 Reflact.set() } get(target,key,val){ // Reflact.get() } })
Proxy.revocable()方法可以用来创建一个可撤销的代理对象。
const target = { name: 'vuejs'} const {proxy, revoke} = Proxy.revocable(target, handler) proxy.name // 正常取值输出 vuejs revoke() // 取值完成对proxy进行封闭,撤消代理 proxy.name // TypeError: Revoked
但Proxy只有在ES6的环境使用,还无法编译降级。
-
Tree-shaking的优化。
在使用Vue3时可以选择按需选择引入相应的模块,而不是一次性引入所有代码,这样打包时Vue3可以将没有引用的源码移除,从而减少体积。
-
渲染性能的优化。
Vue3将静态节点、子树等渲染代码移到渲染函数之外,这样可以避免每次渲染时重新创建这些不会变化的对象。将元素的更新类型进行细分,例如动态绑定的部分如果只涉及到 class,则在对比时只需要对比 class 即可,不需要对比它的内容。此外,Vue3还有不少变化,比如模板中支持了多根节点的组件等,这边就不一一介绍。
代码结构
Vue3的代码主要分packages和scripts两个目录,script主要用于代码检查、打包等工程操作,真正的源码位于packages目录下,一共有13个包:
compiler-core
模板解析核心,与具体环境无关,主要生成 AST,并根据 AST 生成render()
方法compiler-dom
浏览器环境中的模板解析逻辑,如处理 HTML 转义、处理 v-model 等指令compiler-sfc
负责解析 Vue 单文件组件,在前面 vue-loader 的解析中有讲解过compiler-ssr
服务端渲染环境中的模板解析逻辑reactivity
响应式数据相关逻辑runtime-core
与平台无关的运行时核心,包括 renderruntime-dom
浏览器环境中的运行时核心runtime-test
用于自动化测试的相关配套server-renderer
用于 SSR 服务端渲染的逻辑shared
一些各个包之间共享的公共工具size-check
一个用于测试 tree shaking 后代码大小的示例库template-explorer
用于检查模板编译后的输出,主要用于开发调试vue
Vue 3 的主要入口,包括运行时和编译器,包括几个不同的入口(开发版本、runtime 版本、full 版本)
reactivity
整体流程
ReactiveFlags
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}
- 代理对象会通过
ReactiveFlags.raw
引用原始对象 - 原始对象会通过
ReactiveFlags.reactive
或ReactiveFlags.readonly
引用代理对象 - 代理对象根据它是
reactive
或readonly
的, 将ReactiveFlags.isReactive
或ReactiveFlags.isReadonly
属性值设置为true
Track与Trigger
track()
和 trigger()
是依赖收集的核心,track()
用来跟踪收集依赖(收集 effect
),trigger()
用来触发响应(执行 effect
),它们需要配合 effect()
函数使用
const obj = { foo: 1 }
effect(() => {
console.log(obj.foo)
track(obj, TrackOpTypes.GET, 'foo')
})
obj.foo = 2
trigger(obj, TriggerOpTypes.SET, 'foo')
track与trigger函数接受三个参数:
- target:要跟踪的目标对象,这里就是
obj
- 跟踪操作的类型:
obj.foo
是读取对象的值,因此是'get'
- key:要跟踪目标对象的
key
,我们读取的是foo
,因此key
是foo
本质上建立一种数据结构:
// 伪代码
map : {
[target]: {
[key]: [effect1, effect2....]
}
}
简单的理解,effect
与对象和具体操作的 key
,是以这种映射关系建立关联的:
[target]`---->`key1`---->`[effect1, effect2...]
[target]`---->`key2`---->`[effect1, effect3...]
[target2]`---->`key1`---->`[effect5, effect6...]
既然 effect
与目标对象 target
已经建立了联系,那么当然就可以想办法通过 target
----> key
进而取到 effect
,然后执行它们,而这就是 trigger()
函数做的事情,所以在调用 trigger
函数时我们要指定目标对象和相应的key
值。
toRef
使用reactive 声明响应式对象有时出现对象的二次引用,造成响应丢失(对象解构返回渲染环境也会丢失响应)。
const obj = reactive({ foo: 1 }) // obj 是响应式数据
const obj2 = { foo: obj.foo }
effect(() => {
console.log(obj2.foo) // 这里读取 obj2.foo
})
obj.foo = 2 // 设置 obj.foo 显然无效
解决问题可以使用toRef()
函数把响应式对象的某个Key值转换成ref。它的实现就直接set、get返回,因为target本身就是响应式的,所以无需track和trigger。
function toRef(target, key) {
return {
isRef: true,
get value() {
return target[key]
},
set value(newVal){
target[key] = newVal
}
}
}
但是ref的值需要.value访问值才行。如果不想要.value来取值,我们可以直接再包一层reactive。
const obj = reactive({ foo: 1 })
// const obj2 = { foo: toRef(obj, 'foo') }
const obj2 = reactive({ ...toRefs(obj) }) // 让 obj2 也是 reactive
effect(() => {
console.log(obj2.foo) // 即使 obj2.foo 是 ref,我们也不需要 .value 来取值
})
obj.foo = 2 // 有效
它的实现,发现值如果是ref,则返回.value。但这对于ref组成的数组,仍然需要.value来访问。
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
通常使用ref()函数时,我们是为了引用原始数据类型,但引用非基本类型也是可以,比如:
const refObj=ref({foo:1})
refObj.value是一个对象,这对象是响应式的,修改refObj.value.foo会触发响应。而shallowRef是浅代理,只代理ref对象本身,也就是.value是被代理的,而.value所引用的对象并没有被代理,修改refObj.value.foo不会触发响应。那如果也要触发响应呢?Vue3提供triggerRef函数,可以让我们强制触发响应。
triggerRef(refObj)
Vue3 Diff
大体流程:
-
从头对比找到有相同的节点 patch ,发现不同,立即跳出。
-
如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环。
-
如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode )。
-
对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )。
-
不确定的元素( 这种情况说明没有patch完相同的vnode ) 与 3 ,4对立关系。如下情况:
// [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5
-
把没有比较过的新的vnode节点,通过map保存,记录已经patch的新节点的数量 patched,没有经过 path 新的节点的数量 toBePatched
-
建立一个数组newIndexToOldIndexMap,每个子元素都是[ 0, 0, 0, 0, 0, 0, ] 里面的数字记录老节点的索引 ,数组索引就是新节点的索引
-
遍历老节点:
- 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点
- 如果,老节点的key存在 ,通过key找到对应的index
- 如果,老节点的key不存在:遍历剩下的所有新节点,如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex
- 没有找到与老节点对应的新节点,卸载当前老节点
- 如果找到与老节点对应的新节点,把老节点的索引,记录在存放新节点的数组中
-
如果发生移动:
- 根据 newIndexToOldIndexMap 新老节点索引列表找到最长稳定序列
- 对于 newIndexToOldIndexMap[i] =0 证明不存在老节点 ,从新形成新的vnode
- 对移动的节点进行移动处理。
Diff算法对比:
- React:遍历新节点序列在旧节点序列出现的位置,如果位置递增,则新节点不需要移动,否则节点后移。
- Vue 2:双端比较。分别用新、旧节点序列的start跟end相互比较,查找复用,直到指针重合,退出循环。若四次对比都没找到复用节点,只能拿新序列的节点去旧序列依次对比。
- Vue 3:最长递增子序列。生成List代表新节点序列不需要移动的index数组,从后向前遍历,若List[j]==index 说明节点不需要移动,否则节点插入队尾。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!