likes
comments
collection
share

更适合入门的vue3响应式原理解析

作者站长头像
站长
· 阅读数 7

一、前言

版本:V3.0.0。

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

更适合入门的vue3响应式原理解析

先说结论,Vue的响应式原理:数据拦截+发布订阅者模式。Vue3使用了Proxy代替了Vue2的Object.defineProperty进行数据拦截,并使用了effect代替了watcher

为了方便理解,文本对源码做了一些删减,去除了部分其他功能

二、响应式入口

1. createApp

packages\runtime-dom\src\index.ts

createApp主要作用:1. 调用渲染器;2. 重写mount函数

export const createApp = ((...args) => {
  //1. 调用渲染器
  const app = ensureRenderer().createApp(...args)
  //2. 重写mount函数,将传入的参数使用document.querySelector进行查找
  // ...省略
  return app
})

2. ensureRenderer

packages\runtime-dom\src\index.ts

ensureRenderer:判断是否有渲染器,有则返回,没有则调用createRenderer进行创建

function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}

3. createRenderer

packages\runtime-core\src\renderer.ts

createRenderer:调用baseCreateRenderer

export function createRenderer(options) {
  return baseCreateRenderer(options)
}

4. baseCreateRenderer

packages\runtime-core\src\renderer.ts

1. 返回值

先看baseCreateRenderer函数的返回值

function baseCreateRenderer(
  options,
  createHydrationFns
){
    //...先省略
   return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

可以看到createApp是createAppAPI(render, hydrate)的返回值,createAppAPI方法中包含了平时用来注册插件的use以及其他功能函数。

createAppAPI不是本文的重点,接着看内部函数render

2. render

render:判断是否有虚拟节点,有则调用baseCreateRenderer内部函数patch

const render = (vnode, container) => {
  if (vnode !== null) patch(container._vnode || null, vnode, container)
  
  container._vnode = vnode
}

3. patch

patch:通过switch语句,处理了不同类型节点。先看与响应式相关的processComponent

const patch= (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  optimized = false
) => {
  const { type, ref, shapeFlag } = n2
  switch (type) {
    //... 省略
    default:
      if (shapeFlag & ShapeFlags.COMPONENT) {
        //处理组件  
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
  }
}

4. processComponent

processComponent:如果初次渲染则调用mountComponent,否则调用updateComponent

const processComponent = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    if (n1 == null) {
      //初次渲染  
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    } else {
      //更新  
      updateComponent(n1, n2, optimized)
    }
  }

5. mountComponent

mountComponent:是响应式原理的目标函数,负责初始化组件状态,包含设置响应式数据等功能

mountComponent 主要作用分为三部分:1. 创建组件实例;2. 初始化组件;3. 创建渲染effect,并执行

const mountComponent= (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  //1. 创建组件实例
  const instance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ))

  //2. 初始化组件
  setupComponent(instance)

  //3. 创建渲染effect,并执行
  setupRenderEffect(
    instance, // 组件实例
    initialVNode, //vnode 
    container,  // 容器元素
    anchor,
    parentSuspense,
    isSVG,
    optimized
  )
}

三、响应式原理

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,而且针对不同的数据类型给定了不同的处理方法

export function reactive(target: object) {
  return createReactiveObject(
    target, //目标对象
    false, //是否只读
    mutableHandlers, //处理原始数据类型和引用数据类型
    mutableCollectionHandlers //处理Set, Map, WeakMap, WeakSet类型
  )
}

3. createReactiveObject

packages\reactivity\src\reactive.ts

createReactiveObject:使用ES6的Proxy创建代理对象

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {

  //创建响应式对象
  const proxy = new Proxy(target, baseHandlers)

  //用来存储原对象和代理后的对象之间的映射关系
  reactiveMap.set(target, proxy)

  return proxy
}

baseHandlers 就是在reactive函数中传入的用于处理原始数据类型和引用数据类型的拦截器mutableHandlers

4. mutableHandlers

packages\reactivity\src\baseHandlers.ts

mutableHandlers中包含了通常用于JavaScript对象的代理以控制对对象属性的访问和修改

export const mutableHandlers = {
  get, //拦截属性读取
  set, //拦截属性赋值
  deleteProperty,//拦截属性删除
  has, //拦截判断属性是否存在
  ownKeys //拦截对目标对象自身属性的枚举操作
}

get是用createGetter函数创建,set是用createSetter函数创建

5. createGetter

packages\reactivity\src\baseHandlers.ts

createGetter主要负责三件事:1. 使用Reflect.get访问属性值;2. 使用track进行数据收集;3. 如果取出来的数据依旧为对象,再使用reactive进行代理(原因:Proxy只代理一层,这也提高了vue3的初始化性能,只有访问到的数据属性才进行响应式处理。Vue2是在初始化的时候就得递归遍历所有属性,使用Object.defineProperty进行响应式处理)

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
      //如果已经被代理过,直接返回target
      return target
    }

    //1.取值
    const res = Reflect.get(target, key, receiver)

    //2.数据收集
    track(target, TrackOpTypes.GET, key)

    if (isObject(res)) {
      //3.如果取出来的数据依旧为对象,再使用reactive进行代理
      return reactive(res)
    }

    return res
  }
}

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

结构如图所示:

更适合入门的vue3响应式原理解析

track函数的作用:根据target从targetMap中寻找depsMap,再从depsMap中根据对象的key,存储该key相关的effect

export function track(target: object, type: TrackOpTypes, key: unknown) {
  //activeEffect:当前激活的副作用函数effect,用于渲染
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  //targetMap:一个WeakMap数据结构,用来存放目前对象和depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  //depsMap:一个Map数据结构,用来存放属性和其对应的dep依赖项数组
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    //收集依赖
    dep.add(activeEffect)
    //effect 也记录一下 dep
    activeEffect.deps.push(dep)
  }
}

activeEffect.deps.push(dep):将当前dep数组添加到activeEffect.deps上。作用后面会详细讲述,先继续往下看。

例如使用reactive定义一个对象:

const state = reactive({
  count:0,
})

那么targetMap的数据结构将变为:

targetMap:{
  { count:0 } : {
    count :[activeEffect]
  }
}

7. createSetter

packages\reactivity\src\baseHandlers.ts

createGetter主要负责两件事:1. 先使用Reflect.set赋值;2. 然后调用trigger触发更新

function createSetter(shallow = false) {
  return function set(
    target,
    key,
    value,
    receiver
  ): boolean {
    const oldValue = (target as any)[key]

    //判断target中是否有对应的key
    const hadKey = 
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    
    //赋值    
    const result = Reflect.set(target, key, value, receiver)

    if (target === toRaw(receiver)) {
      if (!hadKey) {
        //当前key不存在,说明是赋值新属性
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        //值有改变  
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

8. trigger

packages\reactivity\src\effect.ts

trigger函数主要作用:1. 依据key从depsMap中找到对应的dep数组;2. 使用run方法调用effects数组中每个effect,从而进行更新

export function trigger(
  target,
  type,
  key,
  newValue,
  oldValue,
  oldTarget
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  /* effect钩子队列 */
  const effects = new Set<ReactiveEffect>()

  /* 定义add函数,将effect添加到effects中 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.options.allowRecurse) {
          effects.add(effect) /* 储存effect */
        }
      })
    }
  }

  //取出属性对应的dep栈作为参数传入add
  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      /* 进行调度更新*/
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  //依次执行effect回调
  effects.forEach(run)
}

get与set的流程如下所示:

更适合入门的vue3响应式原理解析

四、编译收集

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  //创建一个渲染effect,并把它赋值给组件实例的update方法,作为渲染更新视图用
  instance.update = effect(function componentEffect() {
    //实例还未挂载
    if (!instance.isMounted) {
      //1. renderComponentRoot:将实例转为树形结构
      const subTree = (instance.subTree = renderComponentRoot(instance))

      //2. 对整个树进行patch
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)

      initialVNode.el = subTree.el
      
      //3. 实例已挂载
      instance.isMounted = true
    } else {
      //自发性更新部分代码 ...
    }
  }, prodEffectOptions)
}

2. renderComponentRoot

packages\runtime-core\src\componentRenderUtils.ts

在renderComponentRoot中,调用了之前在setupComponent中生成的render函数。此时会读取真实属性,触发get,进行依赖收集

export function renderComponentRoot(
  instance
): VNode {
  const {
    //...  
    render,
  } = instance
  let result
  //...
  result = normalizeVNode(
    //调用实例的render函数  
    render!.call(
      proxyToUse,
      proxyToUse!,
      renderCache,
      props,
      setupState,
      data,
      ctx
    )

  return result
}

3. effect

effect:1. 调用createReactiveEffect给componentEffect配置初始化参数等操作;2. 如果不是懒加载,立即执行

export function effect<T = any>(
  fn,
  options
) {
  const effect = createReactiveEffect(fn, options)

  //如果不是懒加载 立即执行由createReactiveEffect创建出来的ReactiveEffect函数
  if (!options.lazy) {
    effect()
  }
  return effect
}

4. createReactiveEffect

createReactiveEffect的作用主要是配置了一些初始化的参数,然后包装了之前传进来的fn

function createReactiveEffect<T = any>(
  fn,
  options
) {
  const effect = function reactiveEffect(): unknown {
    if (!effectStack.includes(effect)) {
      //清空effect的deps依赖栈,同时也删除了对应属性的dep栈中的此effect(双向删除)
      cleanup(effect)
      try {
        enableTracking() //允许收集
        effectStack.push(effect) //往effect数组中里放入当前 effect
        activeEffect = effect // effect 赋值给当前的 activeEffect
        return fn() // fn 为effect传进来 componentEffect(renderer.ts)
      } finally {
        effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1] // 替换activeEffect为新的栈顶
      }
    }
  }
  //配置初始化参数
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

在了解cleanup函数的作用前,先看之前提到的track函数这两行代码:

//收集依赖
dep.add(activeEffect)
//effect 也记录一下 dep
activeEffect.deps.push(dep)

第一行:收集依赖,将effect添加到属性的dep栈中

第二行:将dep栈添加到effect.deps数组中,让effect知道它被哪些属性依赖了

他们是一个双向依赖的关系,dep中有effect,effect.deps中有dep,如图所示:

更适合入门的vue3响应式原理解析

再看cleanup函数:

function cleanup(effect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

入参是一个effect,删除所有属性dep下的effect,并使effect.deps数组置空。

例如使用reactive定义一个对象:

const state = reactive({
  count:0,
  name:'zhangsan'
})

经过数据收集后,targetMap的数据结构将变为:

targetMap:{
  {count: 0, name: 'zhangsan'}:{
    count:[activeEffect],
    name:[activeEffect]
  }
}

如果修改数据,将count变为1,触发set方法,执行effect。effect中又会先执行cleanup方法,删除 dep 里面对应的 effect。此时targetMap的数据结构将变为:

targetMap:{
  {count: 1, name: 'zhangsan'}:{
    count:[],
    name:[]
  }
}

思考:为什么要去删除各个属性下的dep中effect?

答案:这是为了在更新后, 清扫用不到的属性dep。假设有这样的模板结构

<div>
  {{state.count}}
  <div v-if="state.count < 1">  {{state.name}} </div>
  <button @click="add">按钮</button>
</div>

当调用add函数使count变为1,state.name将不会再被渲染。那么数据更新 -> 模板重新读取数据 -> 触发get重新收集依赖,targetMap数据结构将变为:

targetMap:{
  {count: 1, name: 'zhangsan'}:{
    count:[activeEffect],
    name:[]
  }
}

name属性的dep依赖将置空。这里再放一个简易版供大家理解:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
        
      const state = {
        count: 0,
        name:'张三'
      };

      function cleanup(effect) {
        const { deps } = effect;
        if (deps.length) {
          for (let i = 0; i < deps.length; i++) {
            deps[i].delete(effect);
          }
          deps.length = 0;
        }
      }

      const targetMap = new WeakMap();

      /* effect函数 */
      const effect = function reactiveEffect() {
        cleanup(effect);
      };

      effect.deps = [];

      let proxy = new Proxy(state, {
        get(target, key, receiver) {
          const res = Reflect.get(target, key, receiver);

          console.log("数据收集");

          let depsMap = targetMap.get(target);
          if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
          }

          let dep = depsMap.get(key);
          if (!dep) {
            depsMap.set(key, (dep = new Set()));
          }
          if (!dep.has(effect)) {
            dep.add(effect);
            effect.deps.push(dep);
          }

          return res;
        },

        set(target, key, value, receiver) {
          const res = Reflect.set(target, key, value, receiver);
          
           console.log("数据更新");

          const depsMap = targetMap.get(target);

          const dep = depsMap.get(key);

          dep.forEach((effect) => effect());

          return res;
        },
      });

      proxy.count; //触发get

      proxy.name; //触发get

      proxy.count = 3; //触发set

      //更新完之后,再次读取数据触发get,进行依赖收集
      if(proxy.count < 2){
        proxy.name //不会再被访问到
      }

      console.log('targetMap',targetMap)
    </script>
  </body>
</html>

targetMap的打印结果为:

targetMap:{
  {count: 3, name: '张三'}:{
    count:[reactiveEffect],
    name:[]
  }
}

五、小结

在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 启动配置
{
  "name": "Launch Chrome",
  "request": "launch",
  "type": "chrome",
  "url": "http://127.0.0.1:5500/",
  "webRoot": "${workspaceFolder}"
},
  1. 打断点,然后运行debug模式

七、结尾

参考资源:

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

转载自:https://juejin.cn/post/7313509012016103463
评论
请登录