Jansiel Notes

更适合入门的vue3响应式原理解析(纯干货分享)

一、前言

前提:熟悉ES6的Proxy,Reflect,WeakMap,Map,Set。不熟悉的可参考:读懂Vue3响应式原理的前提(Proxy,Reflect,Set,Map,WeakMap)

版本:V3.0.0。

整个流程如图所示(图片比较大,可使用png格式保存到本地再查看):

1702802130.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

结构如图所示:

1702802708.png

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的流程如下所示:

1702803584.png

四、编译收集

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,如图所示:

1702803767.png

再看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中:

  1. 使用Proxy代替Object.defineProperty实现数据劫持

  2. 在编译阶段,执行render,触发get

  3. 在get中收集依赖,使用targetMap,depsMap和dep来管理使用到的属性的依赖项

  4. 当数据变动时触发set,使用run方法依次执行该dep下所有effect,进行更新

六、断点调试

授人以鱼不如授人以渔,原理的理解肯定离不开源码的阅读。而使用断点调试可以帮助我们阅读源码。

  1. 克隆vue3源码或者下载对应的版本包:github.com/vuejs/core.…

  2. 编译,运行 yarn

  3. 建立git仓库并提交一个commit

  4. 修改package.json,添加 -s 或者 -sourcemap

"dev": "node scripts/dev.js -sourcemap",

  1. 启动,运行 yarn dev

  2. VS Code 使用Live Server插件 打开packages\vue\examples\composition\grid.html

例如打开的的地址为:http://127.0.0.1:5500/

  1. 在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
  1. 打断点,然后运行debug模式

七、结尾

对debug不熟的,可以参考:还在console.log?试试VSCode Debug!

对vue3项目搭建感兴趣的,可参考Vite4.3+Typescript+Vue3+Pinia 最新搭建企业级前端项目

参考资源:

  1. vue源码

  2. 六千字详解!vue3 响应式是如何实现的?

  3. Vue3最啰嗦的Reactivity数据响应式原理解析

文章有不对的,或者需要补充的,欢迎在评论区下留言,文章将持续更新。如果本篇文章点赞量和收藏量都不错的话,将继续更新Vue或React的相关源码分析