更适合入门的vue3响应式原理解析(纯干货分享)
一、前言
前提:熟悉ES6的Proxy,Reflect,WeakMap,Map,Set。不熟悉的可参考:读懂Vue3响应式原理的前提(Proxy,Reflect,Set,Map,WeakMap)
版本:V3.0.0。
整个流程如图所示(图片比较大,可使用png格式保存到本地再查看):
先说结论,Vue的响应式原理:数据拦截+发布订阅者模式。Vue3使用了Proxy代替了Vue2的Object.defineProperty进行数据拦截,并使用了effect代替了watcher
为了方便理解,文本对源码做了一些删减,去除了部分其他功能
二、响应式入口
1. createApp
packages\runtime-dom\src\index.ts
createApp主要作用:1. 调用渲染器;2. 重写mount函数
1export const createApp = ((...args) => {
2 //1. 调用渲染器
3 const app = ensureRenderer().createApp(...args)
4 //2. 重写mount函数,将传入的参数使用document.querySelector进行查找
5 // ...省略
6 return app
7})
8
2. ensureRenderer
packages\runtime-dom\src\index.ts
ensureRenderer:判断是否有渲染器,有则返回,没有则调用createRenderer进行创建
1function ensureRenderer() {
2 return renderer || (renderer = createRenderer(rendererOptions))
3}
4
3. createRenderer
packages\runtime-core\src\renderer.ts
createRenderer:调用baseCreateRenderer
1export function createRenderer(options) {
2 return baseCreateRenderer(options)
3}
4
4. baseCreateRenderer
packages\runtime-core\src\renderer.ts
1. 返回值
先看baseCreateRenderer函数的返回值
1function baseCreateRenderer(
2 options,
3 createHydrationFns
4){
5 //...先省略
6 return {
7 render,
8 hydrate,
9 createApp: createAppAPI(render, hydrate)
10 }
11}
12
可以看到createApp是createAppAPI(render, hydrate)的返回值,createAppAPI方法中包含了平时用来注册插件的use以及其他功能函数。
createAppAPI不是本文的重点,接着看内部函数render
2. render
render:判断是否有虚拟节点,有则调用baseCreateRenderer内部函数patch
1const render = (vnode, container) => {
2 if (vnode !== null) patch(container._vnode || null, vnode, container)
3
4 container._vnode = vnode
5}
6
3. patch
patch:通过switch语句,处理了不同类型节点。先看与响应式相关的processComponent
1const patch= (
2 n1,
3 n2,
4 container,
5 anchor = null,
6 parentComponent = null,
7 parentSuspense = null,
8 isSVG = false,
9 optimized = false
10) => {
11 const { type, ref, shapeFlag } = n2
12 switch (type) {
13 //... 省略
14 default:
15 if (shapeFlag & ShapeFlags.COMPONENT) {
16 //处理组件
17 processComponent(
18 n1,
19 n2,
20 container,
21 anchor,
22 parentComponent,
23 parentSuspense,
24 isSVG,
25 optimized
26 )
27 }
28 }
29}
30
4. processComponent
processComponent:如果初次渲染则调用mountComponent,否则调用updateComponent
1const processComponent = (
2 n1,
3 n2,
4 container,
5 anchor,
6 parentComponent,
7 parentSuspense,
8 isSVG,
9 optimized
10 ) => {
11 if (n1 == null) {
12 //初次渲染
13 mountComponent(
14 n2,
15 container,
16 anchor,
17 parentComponent,
18 parentSuspense,
19 isSVG,
20 optimized
21 )
22 } else {
23 //更新
24 updateComponent(n1, n2, optimized)
25 }
26 }
27
5. mountComponent
mountComponent:是响应式原理的目标函数,负责初始化组件状态,包含设置响应式数据等功能
mountComponent 主要作用分为三部分:1. 创建组件实例;2. 初始化组件;3. 创建渲染effect,并执行
1const mountComponent= (
2 initialVNode,
3 container,
4 anchor,
5 parentComponent,
6 parentSuspense,
7 isSVG,
8 optimized
9) => {
10 //1. 创建组件实例
11 const instance = (initialVNode.component = createComponentInstance(
12 initialVNode,
13 parentComponent,
14 parentSuspense
15 ))
16
17 //2. 初始化组件
18 setupComponent(instance)
19
20 //3. 创建渲染effect,并执行
21 setupRenderEffect(
22 instance, // 组件实例
23 initialVNode, //vnode
24 container, // 容器元素
25 anchor,
26 parentSuspense,
27 isSVG,
28 optimized
29 )
30}
31
三、响应式原理
1. setupComponent
packages\runtime-core\src\component.ts
setupComponent 主要作用包含两部分:1. 执行setup,初始化响应式API reactive等;2. 执行compile编译模版内容,得到实例的render渲染函数
2. reactive
packages\reactivity\src\reactive.ts
在setup中,我们常用reactive定义响应式数据。reactive函数的作用就是通过createReactiveObject函数创建一个proxy,而且针对不同的数据类型给定了不同的处理方法
1export function reactive(target: object) {
2 return createReactiveObject(
3 target, //目标对象
4 false, //是否只读
5 mutableHandlers, //处理原始数据类型和引用数据类型
6 mutableCollectionHandlers //处理Set, Map, WeakMap, WeakSet类型
7 )
8}
9
3. createReactiveObject
packages\reactivity\src\reactive.ts
createReactiveObject:使用ES6的Proxy创建代理对象
1function createReactiveObject(
2 target: Target,
3 isReadonly: boolean,
4 baseHandlers: ProxyHandler<any>,
5 collectionHandlers: ProxyHandler<any>
6) {
7
8 //创建响应式对象
9 const proxy = new Proxy(target, baseHandlers)
10
11 //用来存储原对象和代理后的对象之间的映射关系
12 reactiveMap.set(target, proxy)
13
14 return proxy
15}
16
baseHandlers 就是在reactive函数中传入的用于处理原始数据类型和引用数据类型的拦截器mutableHandlers
4. mutableHandlers
packages\reactivity\src\baseHandlers.ts
mutableHandlers中包含了通常用于JavaScript对象的代理以控制对对象属性的访问和修改
1export const mutableHandlers = {
2 get, //拦截属性读取
3 set, //拦截属性赋值
4 deleteProperty,//拦截属性删除
5 has, //拦截判断属性是否存在
6 ownKeys //拦截对目标对象自身属性的枚举操作
7}
8
get是用createGetter函数创建,set是用createSetter函数创建
5. createGetter
packages\reactivity\src\baseHandlers.ts
createGetter主要负责三件事:1. 使用Reflect.get访问属性值;2. 使用track进行数据收集;3. 如果取出来的数据依旧为对象,再使用reactive进行代理(原因:Proxy只代理一层,这也提高了vue3的初始化性能,只有访问到的数据属性才进行响应式处理。Vue2是在初始化的时候就得递归遍历所有属性,使用Object.defineProperty进行响应式处理)
1function createGetter(isReadonly = false, shallow = false) {
2 return function get(target, key, receiver) {
3 if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
4 //如果已经被代理过,直接返回target
5 return target
6 }
7
8 //1.取值
9 const res = Reflect.get(target, key, receiver)
10
11 //2.数据收集
12 track(target, TrackOpTypes.GET, key)
13
14 if (isObject(res)) {
15 //3.如果取出来的数据依旧为对象,再使用reactive进行代理
16 return reactive(res)
17 }
18
19 return res
20 }
21}
22
6. track
packages\reactivity\src\effect.ts
使用track函数进行数据收集,需要先了解三个属性:1. targetMap;2. depsMap;3. dep
targetMap:一个WeakMap数据结构,用来存放目前对象和depsMap
depsMap:一个Map数据结构,用来存放属性和其对应的dep依赖项数组
dep:一个Set数据结构,存放effect,有负责渲染的effect,也有使用watchEffect自定义的effect
结构如图所示:
track函数的作用:根据target从targetMap中寻找depsMap,再从depsMap中根据对象的key,存储该key相关的effect
1export function track(target: object, type: TrackOpTypes, key: unknown) {
2 //activeEffect:当前激活的副作用函数effect,用于渲染
3 if (!shouldTrack || activeEffect === undefined) {
4 return
5 }
6
7 //targetMap:一个WeakMap数据结构,用来存放目前对象和depsMap
8 let depsMap = targetMap.get(target)
9 if (!depsMap) {
10 targetMap.set(target, (depsMap = new Map()))
11 }
12 //depsMap:一个Map数据结构,用来存放属性和其对应的dep依赖项数组
13 let dep = depsMap.get(key)
14 if (!dep) {
15 depsMap.set(key, (dep = new Set()))
16 }
17 if (!dep.has(activeEffect)) {
18 //收集依赖
19 dep.add(activeEffect)
20 //effect 也记录一下 dep
21 activeEffect.deps.push(dep)
22 }
23}
24
activeEffect.deps.push(dep):将当前dep数组添加到activeEffect.deps上。作用后面会详细讲述,先继续往下看。
例如使用reactive定义一个对象:
1const state = reactive({
2 count:0,
3})
4
那么targetMap的数据结构将变为:
1targetMap:{
2 { count:0 } : {
3 _v_isRef : [activeEffect],
4 count :[activeEffect]
5 }
6}
7
_v_isRef是当前整个对象的key,简化下来的数据结构为:
1targetMap:{
2 { count:0 } : {
3 count :[activeEffect]
4 }
5}
6
7. createSetter
packages\reactivity\src\baseHandlers.ts
createGetter主要负责两件事:1. 先使用Reflect.set赋值;2. 然后调用trigger触发更新
1function createSetter(shallow = false) {
2 return function set(
3 target,
4 key,
5 value,
6 receiver
7 ): boolean {
8 const oldValue = (target as any)[key]
9
10 //判断target中是否有对应的key
11 const hadKey =
12 isArray(target) && isIntegerKey(key)
13 ? Number(key) < target.length
14 : hasOwn(target, key)
15
16 //赋值
17 const result = Reflect.set(target, key, value, receiver)
18
19 if (target === toRaw(receiver)) {
20 if (!hadKey) {
21 //当前key不存在,说明是赋值新属性
22 trigger(target, TriggerOpTypes.ADD, key, value)
23 } else if (hasChanged(value, oldValue)) {
24 //值有改变
25 trigger(target, TriggerOpTypes.SET, key, value, oldValue)
26 }
27 }
28 return result
29 }
30}
31
8. trigger
packages\reactivity\src\effect.ts
trigger函数主要作用:1. 依据key从depsMap中找到对应的dep数组;2. 使用run方法调用effects数组中每个effect,从而进行更新
1export function trigger(
2 target,
3 type,
4 key,
5 newValue,
6 oldValue,
7 oldTarget
8) {
9 const depsMap = targetMap.get(target)
10 if (!depsMap) {
11 // never been tracked
12 return
13 }
14
15 /* effect钩子队列 */
16 const effects = new Set<ReactiveEffect>()
17
18 /* 定义add函数,将effect添加到effects中 */
19 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
20 if (effectsToAdd) {
21 effectsToAdd.forEach(effect => {
22 if (effect !== activeEffect || effect.options.allowRecurse) {
23 effects.add(effect) /* 储存effect */
24 }
25 })
26 }
27 }
28
29 //取出属性对应的dep栈作为参数传入add
30 add(depsMap.get(key))
31
32 const run = (effect: ReactiveEffect) => {
33 if (effect.options.scheduler) {
34 /* 进行调度更新*/
35 effect.options.scheduler(effect)
36 } else {
37 effect()
38 }
39 }
40
41 //依次执行effect回调
42 effects.forEach(run)
43}
44
get与set的流程如下所示:
四、编译收集
get是要访问属性时才会被触发,那么什么时候触发get,进行依赖收集的呢?接着看mountComponent函数的第三部分setupRenderEffect。
1. setupRenderEffect
packages\runtime-core\src\renderer.ts
setupRenderEffect:负责创建一个渲染effect,并把它赋值给组件实例的update方法,作为渲染更新视图用。setupRenderEffect内部有两个函数,分别为effect和componentEffect。
effect:是一个高阶函数,负责给componentEffect配置初始化参数,以及给activeEffect赋值,执行cleanup清除所有依赖该effect的dep数组。
componentEffect主要有两个部分:1. 使用renderComponentRoot将实例转为树形结构(此阶段触发get);2. 对整个树进行patch
1const setupRenderEffect: SetupRenderEffectFn = (
2 instance,
3 initialVNode,
4 container,
5 anchor,
6 parentSuspense,
7 isSVG,
8 optimized
9) => {
10 //创建一个渲染effect,并把它赋值给组件实例的update方法,作为渲染更新视图用
11 instance.update = effect(function componentEffect() {
12 //实例还未挂载
13 if (!instance.isMounted) {
14 //1. renderComponentRoot:将实例转为树形结构
15 const subTree = (instance.subTree = renderComponentRoot(instance))
16
17 //2. 对整个树进行patch
18 patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
19
20 initialVNode.el = subTree.el
21
22 //3. 实例已挂载
23 instance.isMounted = true
24 } else {
25 //自发性更新部分代码 ...
26 }
27 }, prodEffectOptions)
28}
29
2. renderComponentRoot
packages\runtime-core\src\componentRenderUtils.ts
在renderComponentRoot中,调用了之前在setupComponent中生成的render函数。此时会读取真实属性,触发get,进行依赖收集
1export function renderComponentRoot(
2 instance
3): VNode {
4 const {
5 //...
6 render,
7 } = instance
8 let result
9 //...
10 result = normalizeVNode(
11 //调用实例的render函数
12 render!.call(
13 proxyToUse,
14 proxyToUse!,
15 renderCache,
16 props,
17 setupState,
18 data,
19 ctx
20 )
21
22 return result
23}
24
3. effect
effect:1. 调用createReactiveEffect给componentEffect配置初始化参数等操作;2. 如果不是懒加载,立即执行
1export function effect<T = any>(
2 fn,
3 options
4) {
5 const effect = createReactiveEffect(fn, options)
6
7 //如果不是懒加载 立即执行由createReactiveEffect创建出来的ReactiveEffect函数
8 if (!options.lazy) {
9 effect()
10 }
11 return effect
12}
13
4. createReactiveEffect
createReactiveEffect的作用主要是配置了一些初始化的参数,然后包装了之前传进来的fn
1function createReactiveEffect<T = any>(
2 fn,
3 options
4) {
5 const effect = function reactiveEffect(): unknown {
6 if (!effectStack.includes(effect)) {
7 //清空effect的deps依赖栈,同时也删除了对应属性的dep栈中的此effect(双向删除)
8 cleanup(effect)
9 try {
10 enableTracking() //允许收集
11 effectStack.push(effect) //往effect数组中里放入当前 effect
12 activeEffect = effect // effect 赋值给当前的 activeEffect
13 return fn() // fn 为effect传进来 componentEffect(renderer.ts)
14 } finally {
15 effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect
16 resetTracking()
17 activeEffect = effectStack[effectStack.length - 1] // 替换activeEffect为新的栈顶
18 }
19 }
20 }
21 //配置初始化参数
22 effect.id = uid++
23 effect._isEffect = true
24 effect.active = true
25 effect.raw = fn
26 effect.deps = []
27 effect.options = options
28 return effect
29}
30
在了解cleanup函数的作用前,先看之前提到的track函数这两行代码:
1//收集依赖
2dep.add(activeEffect)
3//effect 也记录一下 dep
4activeEffect.deps.push(dep)
5
第一行:收集依赖,将effect添加到属性的dep栈中
第二行:将dep栈添加到effect.deps数组中,让effect知道它被哪些属性依赖了
他们是一个双向依赖的关系,dep中有effect,effect.deps中有dep,如图所示:
再看cleanup函数:
1function cleanup(effect) {
2 const { deps } = effect
3 if (deps.length) {
4 for (let i = 0; i < deps.length; i++) {
5 deps[i].delete(effect)
6 }
7 deps.length = 0
8 }
9}
10
入参是一个effect,删除所有属性dep下的effect,并使effect.deps数组置空。
例如使用reactive定义一个对象:
1const state = reactive({
2 count:0,
3 name:'zhangsan'
4})
5
经过数据收集后,targetMap的数据结构将变为:
1targetMap:{
2 {count: 0, name: 'zhangsan'}:{
3 _v_isRef:[activeEffect],
4 count:[activeEffect],
5 name:[activeEffect]
6 }
7}
8
如果修改数据,将count变为1,触发set方法,执行effect。effect中又会先执行cleanup方法,删除 dep 里面对应的 effect。此时targetMap的数据结构将变为:
1targetMap:{
2 {count: 1, name: 'zhangsan'}:{
3 _v_isRef:[],
4 count:[],
5 name:[]
6 }
7}
8
思考:为什么要去删除各个属性下的dep中effect?
答案:这是为了在更新后, 清扫用不到的属性dep。假设有这样的模板结构
1<div>
2 {{state.count}}
3 <div v-if="state.count < 1"> {{state.name}} </div>
4 <button @click="add">按钮</button>
5</div>
6
当调用add函数使count变为1,state.name将不会再被渲染。那么数据更新 -> 模板重新读取数据 -> 触发get重新收集依赖,targetMap数据结构将变为:
1targetMap:{
2 {count: 1, name: 'zhangsan'}:{
3 _v_isRef:[activeEffect],
4 count:[activeEffect],
5 name:[]
6 }
7}
8
name属性的dep依赖将置空。这里再放一个简易版供大家理解:
1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Document</title>
7 </head>
8 <body>
9 <script>
10
11 const state = {
12 count: 0,
13 name:'张三'
14 };
15
16 function cleanup(effect) {
17 const { deps } = effect;
18 if (deps.length) {
19 for (let i = 0; i < deps.length; i++) {
20 deps[i].delete(effect);
21 }
22 deps.length = 0;
23 }
24 }
25
26 const targetMap = new WeakMap();
27
28 /* effect函数 */
29 const effect = function reactiveEffect() {
30 cleanup(effect);
31 };
32
33 effect.deps = [];
34
35 let proxy = new Proxy(state, {
36 get(target, key, receiver) {
37 const res = Reflect.get(target, key, receiver);
38
39 console.log("数据收集");
40
41 let depsMap = targetMap.get(target);
42 if (!depsMap) {
43 targetMap.set(target, (depsMap = new Map()));
44 }
45
46 let dep = depsMap.get(key);
47 if (!dep) {
48 depsMap.set(key, (dep = new Set()));
49 }
50 if (!dep.has(effect)) {
51 dep.add(effect);
52 effect.deps.push(dep);
53 }
54
55 return res;
56 },
57
58 set(target, key, value, receiver) {
59 const res = Reflect.set(target, key, value, receiver);
60
61 console.log("数据更新");
62
63 const depsMap = targetMap.get(target);
64
65 const dep = depsMap.get(key);
66
67 dep.forEach((effect) => effect());
68
69 return res;
70 },
71 });
72
73 proxy.count; //触发get
74
75 proxy.name; //触发get
76
77 proxy.count = 3; //触发set
78
79 //更新完之后,再次读取数据触发get,进行依赖收集
80 if(proxy.count < 2){
81 proxy.name //不会再被访问到
82 }
83
84 console.log('targetMap',targetMap)
85 </script>
86 </body>
87</html>
88
targetMap的打印结果为:
1targetMap:{
2 {count: 3, name: '张三'}:{
3 count:[reactiveEffect],
4 name:[]
5 }
6}
7
五、小结
在vue3中:
-
使用Proxy代替Object.defineProperty实现数据劫持
-
在编译阶段,执行render,触发get
-
在get中收集依赖,使用targetMap,depsMap和dep来管理使用到的属性的依赖项
-
当数据变动时触发set,使用run方法依次执行该dep下所有effect,进行更新
六、断点调试
授人以鱼不如授人以渔,原理的理解肯定离不开源码的阅读。而使用断点调试可以帮助我们阅读源码。
-
克隆vue3源码或者下载对应的版本包:github.com/vuejs/core.…
-
编译,运行
yarn
-
建立git仓库并提交一个commit
-
修改package.json,添加 -s 或者 -sourcemap
"dev": "node scripts/dev.js -sourcemap",
-
启动,运行
yarn dev
-
VS Code 使用Live Server插件 打开packages\vue\examples\composition\grid.html
例如打开的的地址为:http://127.0.0.1:5500/
- 在vscode debug中新增Chrome 启动配置
1{
2 "name": "Launch Chrome",
3 "request": "launch",
4 "type": "chrome",
5 "url": "http://127.0.0.1:5500/",
6 "webRoot": "${workspaceFolder}"
7},
8
- 打断点,然后运行debug模式
七、结尾
对debug不熟的,可以参考:还在console.log?试试VSCode Debug!
对vue3项目搭建感兴趣的,可参考Vite4.3+Typescript+Vue3+Pinia 最新搭建企业级前端项目
参考资源:
文章有不对的,或者需要补充的,欢迎在评论区下留言,文章将持续更新。如果本篇文章点赞量和收藏量都不错的话,将继续更新Vue或React的相关源码分析