[建议收藏]你想知道的Vue3核心源码这里都有
Effect 和 Reactive
effect 作为 Vue 响应式原理中的核心,在 Computed、Watch、Reactive 中都有出现
主要和 Reactive(Proxy)、track、trigger 等函数配合实现收集依赖,触发依赖更新
- Effect
- 副作用依赖函数
- Track
- 依赖收集
- Trigger
- 依赖触发
Effect
effect 可以被理解为一个副作用函数,被当做依赖收集,在响应式数据更新后被触发。
Vue 的响应式 API 例如 Computed、Watch 都有用到 effect 来实现
- 先来看看入口函数
- 入口函数主要是一些逻辑处理,核心逻辑位于 createReactiveEffect
function effect<T = any>( |
- createReactiveEffect
const effectStack: ReactiveEffect[] = []; |
Track
Track 这个函数常出现在 reactive 的 getter 函数中,用于依赖收集
源码详解见注释
function track(target: object, type: TrackOpTypes, key: unknown) { |
Trigger
Trigger 常出现在 reactive 中的 setter 函数中,用于触发依赖更新
源码详解见注释
function trigger( |
Reactive
了解了 Track 用于依赖收集,Trigger 用于依赖触发,那么他们的调用时机是什么时候呢?来看看 Reactive 的源码就清楚了,源码详解见注释。
注:源码结构较为复杂(封装),为便于理解原理,以下为简化源码。
- 总结来说
- 在 getter 时进行依赖收集
- 在 setter 时触发依赖更新
function reactive(target:object){ |
Computed
Computed 是 Vue 中常用且好用的一个属性,这个属性的值在依赖改变后同步进行改变,在依赖未改变时使用缓存的值。
- Vue2
- 在 Vue2 中 Computed 的实现通过嵌套 watcher,实现响应式数据的依赖收集,间接链式触发依赖更新。
- Vue3 中出现了 effect,重新实现了 Computed 属性
- effect 可以被理解为副作用函数,被当做依赖收集,在响应式数据更新后被触发。
Show me the Code
- 读完这段 computed 函数会发现,这里只是做了简要的 getter 和 setter 的赋值处理
- computed 支持两种写法
- 函数
- getter、setter
- computed 支持两种写法
function computed<T>( |
- 核心逻辑都在 ComputedRefImpl 中,我们接着往下看
- 通过 dirty 变量标记数据是否为旧数据
- 在响应式数据更新后将 dirty 赋值为 true
- 在下一次 get 时,dirty 为 true 时进行重新计算,并将 dirty 赋值为 false
class ComputedRefImpl<T> { |
Watch
Watch 主要用于对某个变量的监听,并做相应的处理
Vue3 中不仅重构了 watch,还多了一个 WatchEffect API
- Watch
用于对某个变量的监听,同时可以通过 callBack 拿到新值和旧值
watch(state, (state, prevState) => {}); |
- WatchEffect
每次更新都会执行,自动收集使用到的依赖
无法获取到新值和旧值,可手动停止监听
onInvalidate(fn)
传入的回调会在watchEffect
重新运行或者watchEffect
停止的时候执行
const stop = watchEffect((onInvalidate) => { |
watch 和 watchEffect 的不同点
- watch 惰性执行,watchEffect 每次代码加载都会执行
- watch 可指定监听变量,watchEffect 自动依赖收集
- watch 可获取新旧值,watchEffect 不行
- watchEffect 有 onInvalidate 功能,watch 没有
- watch 只可监听 ref、reactive 等对象,watchEffect 只可监听具体属性
Source Code
Show me the Code
- 这里可以看到 watch 和 watchEffet 的核心逻辑都封装到了 doWatch 中
// watch |
- doWatch
以下为删减版源码,理解核心原理即可
详情见注释
function doWatch( |
Mixin
Mixin 意为混合,是公共逻辑封装利器。
原理比较简单,那就是合并。
- 合并分为对象的合并和生命周期的合并
- 对象,mergeOption
- 类型 Object.assign 的合并,会出现覆盖现象
- 生命周期,mergeHook
- 合并会将两个生命周期放入一个队列,依次调用
- 对象,mergeOption
- mergeOptions
function mergeOptions( |
- mergeHook
简单粗暴放进 Set,调用时依次调用
function mergeHook( |
Diff 算法优化
了解 Vue3 的 Diff 算法优化前,可以先了解一下Vue2 的 Diff 算法
本部分注重把算法讲清楚,将不进行逐行源码分析
- Vue3 中的主要优化点为
- 在 updateChildren 时双端比较 -> 最长递增子序列
- 全量 Diff -> 静态标记 + 非全量 Diff
- 静态提升
updateChildren
- Vue2
- 头 - 头比较
- 尾 - 尾比较
- 头 - 尾比较
- 尾 - 头比较
- Vue3
- 头 - 头比较
- 尾 - 尾比较
- 基于最长递增子序列进行移动 / 删除 / 新增
举个 🌰
- oldChild [a,b,c,d,e,f,g]
- newChild [a,b,f,c,d,e,h,g]
- 首先进行头 - 头比较,比较到不一样的节点时跳出循环
- 得到[a,b]
- 然后进行尾 - 尾比较,比较到不一样的节点时跳出循环
- 得到[g]
- 剩余[f,c,d,e,h]
- 通过 newIndexToOldIndexMap 生成数组[5, 2, 3, 4, -1]
- 得出最长递增子序列[2, 3, 4]对应节点为[c, d, e]
- 剩余的节点基于[c, d, e]进行移动 / 新增 / 删除
最长递增子序列 减少 Dom 元素的移动,达到最少的 dom 操作以减小开销。
关于最长递增子序列算法可以看看最长递增子序列
静态标记
Vue2 中对 vdom 进行全量 Diff,Vue3 中增加了静态标记进行非全量 Diff
对 vnode 打了像以下枚举内的静态标记
- patchFlag
export const enum PatchFlags { |
举个 🌰
- 模板长这样
<div> |
- 生成 vdom 源码
对 msg 变量进行了标记
import { |
总结
- 对 vnode 进行标记,将需要动态更新和不需要动态更新的节点进行分类
- 静态节点仅需创建一次,渲染直接复用,不参与 diff 算法流程。
静态提升
-
Vue2 中无论是元素是否参与更新,每次都会重新创建
-
Vue3 中对于不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停地复用
-
以后每次进行 render 的时候,就不会重复创建这些静态的内容,而是直接从一开始就创建好的常量中取就行了。
import { |
cacheHandlers 事件侦听器缓存
-
默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪它的变化
-
但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可。
// 模板 |
它的意思很明显,onClick 方法被存入 cache。
在使用的时候,如果能在缓存中找到这个方法,那么它将直接被使用。
如果找不到,那么将这个方法注入缓存。
总之,就是把方法给缓存了。
掘金:前端 LeBron
知乎:前端 LeBron
持续分享技术博文,关注微信公众号 👇🏻