vue30-源码解析二-数据绑定原理下

2次阅读

共计 10445 个字符,预计需要花费 27 分钟才能阅读完成。

回顾上文

上节我们讲了数据绑定 proxy 原理,vue3.0 用到的基本的拦截器,以及 reactive 入口等等。调用 reactive 建立响应式,首先通过判断数据类型来确定使用的 hander,然后创建 proxy 代理对象 observed。这里的疑惑点就是 hander 对象具体做了什么?本文我们将已 baseHandlers 为着手点,继续分析响应式原理。

连载文章是大致是这样的,可能会根据变化随时更改:
1 数据绑定原理(上)
2 数据绑定原理(下)
3 computed 和 watch 原理
4 事件系统
5 ceateApp
6 初始化 mounted 和 patch 流程。
7 diff 算法与 2.0 区别
8 编译 compiler 系列

一 拦截器对象 baseHandlers -> mutableHandlers

之前我们介绍过 baseHandlers 就是调用 reactive 方法 createReactiveObject 传进来的 mutableHandlers 对象。
我们先来看一下 mutableHandlers 对象

mutableHandlers

拦截器的作用域

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

vue3.0 用到了以上几个拦截器,我们在上节已经介绍了这几个拦截器的基本用法, 首先我们对几个基本用到的拦截器在做一下回顾。

①get, 对数据的读取属性进行拦截,包括 target. 点语法 和 target[]

②set,对数据的存入属性进行拦截。

③deleteProperty delete 操作符进行拦截。

vue2.0不能对对象的 delete 操作符 进行属性拦截。

例子????:

delete object.a

是无法监测到的。

vue3.0proxy 中deleteProperty 可以拦截 delete 操作符,这就表述 vue3.0 响应式可以监听到属性的删除操作。

④has,对 in 操作符进行属性拦截。

vue2.0不能对对象的 in 操作符 进行属性拦截。

例子

a in object

has 是为了解决如上问题。这就表示了 vue3.0 可以对 in 操作符 进行拦截。

⑤ownKeys Object.keys(proxy) ,for…in… 循环 Object.getOwnPropertySymbols(proxy)Object.getOwnPropertyNames(proxy) 拦截器

例子

Object.keys(object)

说明 vue3.0 可以对以上这些方法进行拦截。

二 组件初始化阶段

如果我们想要弄明白整个响应式原理。那么组件初始化,到初始化过程中 composition-api 的 reactive 处理 data,以及编译阶段对 data 属性进行依赖收集是分不开的。vue3.0 提供了一套从初始化,到 render 过程中依赖收集,到组件更新, 到组件销毁完整响应式体系,我们很难从一个角度把东西讲明白,所以在正式讲拦截器对象如何收集依赖,派发更新之前,我们看看 effect 做了些什么操作。

1 effect -> 新的渲染 watcher

vue3.0 用 effect 副作用钩子来代替 vue2.0watcher。我们都知道在 vue2.0 中,有渲染 watcher 专门负责数据变化后的从新渲染视图。vue3.0 改用 effect 来代替 watcher 达到同样的效果。

我们先简单介绍一下 mountComponent 流程,后面的文章会详细介绍 mount 阶段的

1 mountComponent 初始化 mountComponent

  // 初始化组件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 第一步: 创建 component 实例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    /* 第二步:TODO: 初始化 初始化组件, 建立 proxy , 根据字符窜模版得到 */
    setupComponent(instance)
    /* 第三步:建立一个渲染 effect,执行 effect */
    setupRenderEffect(
      instance,     // 组件实例
      initialVNode, //vnode  
      container,    // 容器元素
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )   
  }

上面是整个 mountComponent 的主要分为了三步,我们这里分别介绍一下每个步骤干了什么:
① 第一步: 创建 component 实例。
② 第二步:初始化组件, 建立 proxy , 根据字符窜模版得到 render 函数。生命周期钩子函数处理等等
③ 第三步:建立一个渲染 effect,执行 effect。

从如上方法中我们可以看到,在 setupComponent 已经构建了响应式对象,但是还没有 初始化收集依赖

2 setupRenderEffect 构建渲染 effect

 const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 创建一个渲染 effect */
    instance.update = effect(function componentEffect() {//... 省去的内容后面会讲到},{scheduler: queueJob})
  }

为了让大家更清楚的明白响应式原理,我这只保留了和响应式原理有关系的部分代码。

setupRenderEffect 的作用

① 创建一个 effect,并把它赋值给组件实例的 update 方法,作为渲染更新视图用。
② componentEffect 作为回调函数形式传递给 effect 作为第一个参数

3 effect 做了些什么

export function effect<T = any>(fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {const effect = createReactiveEffect(fn, options)
  /* 如果不是懒加载 立即执行 effect 函数 */
  if (!options.lazy) {effect()
  }
  return effect
}

effect 作用如下

① 首先调用。createReactiveEffect
② 如果不是懒加载 立即执行 由 createReactiveEffect 创建出来的 ReactiveEffect 函数

4 ReactiveEffect

function createReactiveEffect<T = any>(fn: (...args: any[]) => T, /** 回调函数 */
  options: ReactiveEffectOptions
): ReactiveEffect<T> {const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {enableTracking()
        effectStack.push(effect) // 往 effect 数组中里放入当前 effect
        activeEffect = effect //TODO: effect 赋值给当前的 activeEffect
        return fn(...args) //TODO:    fn 为 effect 传进来 componentEffect
      } finally {effectStack.pop() // 完成依赖收集后从 effect 数组删掉这个 effect
        resetTracking() 
        /* 将 activeEffect 还原到之前的 effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化参数 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO: 用于收集相关依赖 */
  effect.options = options
  return effect
}

createReactiveEffect

createReactiveEffect的作用主要是配置了一些初始化的参数,然后包装了之前传进来的 fn,重要的一点是把当前的 effect 赋值给了 activeEffect, 这一点非常重要,和收集依赖有着直接的关系

在这里留下了一个疑点,

①为什么要用 effectStack 数组来存放这里 effect

总结

我们这里个响应式初始化阶段进行总结

① setupComponent 创建组件,调用 composition-api, 处理 options(构建响应式)得到 Observer 对象。

② 创建一个渲染 effect,里面包装了真正的渲染方法 componentEffect,添加一些 effect 初始化属性。

③ 然后立即执行 effect,然后将当前渲染 effect 赋值给 activeEffect

最后我们用一张图来解释一下整个流程。

三 依赖收集,get 做了些什么?

1 回归 mutableHandlers 中的 get 方法

1 不同类型的 get

/* 深度 get */
const get = /*#__PURE__*/ createGetter()
/* 浅 get */
const shallowGet = /*#__PURE__*/ createGetter(false, true)
/* 只读的 get */
const readonlyGet = /*#__PURE__*/ createGetter(true)
/* 只读的浅 get */
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

上面我们可以知道,对于之前讲的四种不同的建立响应式方法,对应了四种不同的 get, 下面是一一对应关系。

reactive ———> get

shallowReactive ——–> shallowGet

readonly ———-> readonlyGet

shallowReadonly —————> shallowReadonlyGet

四种方法都是调用了 createGetter 方法,只不过是参数的配置不同,我们这里那第一个 get 方法做参考,接下来探索一下 createGetter。

createGetter

function createGetter(isReadonly = false, shallow = false) {return function get(target: object, key: string | symbol, receiver: object) {const res = Reflect.get(target, key, receiver)
    /* 浅逻辑 */
    if (shallow) {!isReadonly && track(target, TrackOpTypes.GET, key)
      return res
    }
    /* 数据绑定 */
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ?
          /* 只读属性 */
          readonly(res)
          /*  */
        : reactive(res)
      : res
  }
}

这就是 createGetter 主要流程,特殊的数据类型 ref我们暂时先不考虑。
这里用了一些流程判断,我们用流程图来说明一下这个函数主要做了什么?

我们可以得出结论:
在 vue2.0 的时候。响应式是在初始化的时候就深层次递归处理了
但是

与 vue2.0 不同的是, 即便是深度响应式我们也只能在获取上一级 get 之后才能触发下一级的深度响应式。
比如

setup(){const state = reactive({ a:{ b:{} } })
 return {state}
}

在初始化的时候,只有 a 的一层级建立了响应式,b 并没有建立响应式,而当我们用 state.a 的时候,才会真正的将 b 也做响应式处理,也就是说我们访问了上一级属性后,下一代属性才会真正意义上建立响应式

这样做好处是,
1 初始化的时候不用递归去处理对象,造成了不必要的性能开销。
*2 有一些没有用上的 state,这里就不需要在深层次响应式处理。

2 track-> 依赖收集器

我们先来看看 track 源码:

track 做了些什么


/* target 对象本身,key 属性值  type 为 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {/* 当打印或者获取属性的时候 console.log(this.a) 是没有 activeEffect 的 当前返回值为 0  */
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    /*  target -map-> depsMap  */
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    /* key : dep dep 观察者 */
    depsMap.set(key, (dep = new Set()))
  }
   /* 当前 activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep 添加 activeEffect */
    dep.add(activeEffect)
    /* 每个 activeEffect 的 deps 存放当前的 dep */
    activeEffect.deps.push(dep)
  }
}

里面主要引入了两个概念 targetMapdepsMap

targetMap
键值对 proxy : depsMap
proxy:为 reactive 代理后的 Observer 对象。
depsMap:为存放依赖 dep 的 map 映射。

depsMap
键值对:key : deps
key 为当前 get 访问的属性名,
deps 存放 effect 的 set 数据类型。

我们知道 track 作用大致是,首先根据 proxy 对象,获取存放 deps 的 depsMap,然后通过访问的属性名 key 获取对应的 dep, 然后将当前激活的 effect 存入当前 dep 收集依赖。

主要作用
①找到与当前 proxy 和 key 对应的 dep。
②dep 与当前 activeEffect 建立联系,收集依赖。

为了方便理解,targetMapdepsMap的关系,下面我们用一个例子来说明:
例子:
父组件 A


<div id="app" >
  <span>{{state.a}}</span>
  <span>{{state.b}}</span>
<div>
<script>
const {createApp, reactive} = Vue

/* 子组件 */
const Children ={template="<div> <span>{{ state.c}}</span> </div>",
    setup(){
       const state = reactive({c:1})
       return {state}
    }
}
/* 父组件 */
createApp({
   component:{Children} 
   setup(){
       const state = reactive({
           a:1,
           b:2
       })
       return {state}
   }
})mount('#app')

</script>

我们用一幅图表示如上关系:

渲染 effect 函数如何触发 get

我们在前面说过,创建一个渲染 renderEffect,然后把赋值给 activeEffect,最后执行 renderEffect,在这个期间是怎么做依赖收集的呢,让我们一起来看看,update 函数中做了什么,我们回到之前讲的 componentEffect 逻辑上来

function componentEffect() {if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const {el, props} = initialVNode
        const {bm, m, a, parent} = instance
        /* TODO: 触发 instance.render 函数,形成树结构 */
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (bm) {
          // 触发 beforeMount 声明周期钩子
          invokeArrayFns(bm)
        }
        patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
        )
        /* 触发声明周期 mounted 钩子 */
        if (m) {queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // 更新组件逻辑
        // ......
      }
}

这边代码大致首先会通过 renderComponentRoot 方法形成树结构,这里要注意的是,我们在最初 mountComponent 的 setupComponent 方法中,已经通过编译方法 compile 编译了 template 模版的内容,state.a state.b 等抽象语法树,最终返回的 render 函数在这个阶段会被触发,在 render 函数中在模版中的表达式 state.a state.b 点语法会被替换成 data 中真实的属性,这时候就进行了真正的依赖收集,触发了 get 方法。接下来就是触发生命周期 beforeMount , 然后对整个树结构重新 patch,patch 完毕后,调用 mounted 钩子

依赖收集流程总结

① 首先执行 renderEffect,赋值给 activeEffect,调用 renderComponentRoot 方法,然后触发 render 函数。

② 根据 render 函数,解析经过 compile,语法树处理过后的模版表达式,访问真实的 data 属性,触发 get。

③ get 方法首先经过之前不同的 reactive,通过 track 方法进行依赖收集。

④ track 方法通过当前 proxy 对象 target, 和访问的属性名 key 来找到对应的 dep。

⑤ 将 dep 与当前的 activeEffect 建立起联系。将 activeEffect 压入 dep 数组中,(此时的 dep 中已经含有当前组件的渲染 effect, 这就是响应式的根本原因)如果我们触发 set,就能在数组中找到对应的 effect,依次执行。

最后我们用一个流程图来表达一下依赖收集的流程。

四 set 派发更新

接下来我们 set 部分逻辑。


const set = /*#__PURE__*/ createSetter()
/* 浅逻辑 */
const shallowSet = /*#__PURE__*/ createSetter(true)

set 也是分两个逻辑,set 和 shallowSet, 两种方法都是由 createSetter 产生,我们这里主要以 set 进行剖析。

createSetter 创建 set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {const oldValue = (target as any)[key]
    /* shallowSet 逻辑 */

    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    /* 判断当前对象,和存在 reactiveToRaw 里面是否相等 */
    if (target === toRaw(receiver)) {if (!hadKey) { /* 新建属性 */
        /*  TriggerOpTypes.ADD -> add */
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        /* 改变原有属性 */
        /*  TriggerOpTypes.SET -> set */
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

createSetter 的流程大致是这样的

① 首先通过 toRaw 判断当前的 proxy 对象和建立响应式存入 reactiveToRaw 的 proxy 对象是否相等。
② 判断 target 有没有当前 key, 如果存在的话,改变属性,执行 trigger(target, TriggerOpTypes.SET, key, value, oldValue)。
③ 如果当前 key 不存在,说明是赋值新属性,执行 trigger(target, TriggerOpTypes.ADD, key, value)。

trigger

/* 根据 value 值的改变,从 effect 和 computer 拿出对应的 callback,然后依次执行 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  /* 获取 depssMap */
  const depsMap = targetMap.get(target)
  /* 没有经过依赖收集的,直接返回 */
  if (!depsMap) {return}
  const effects = new Set<ReactiveEffect>()        /* effect 钩子队列 */
  const computedRunners = new Set<ReactiveEffect>() /* 计算属性队列 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {if (effectsToAdd) {
      effectsToAdd.forEach(effect => {if (effect !== activeEffect || !shouldTrack) {if (effect.options.computed) { /* 处理 computed 逻辑 */
            computedRunners.add(effect)  /* 储存对应的 dep */
          } else {effects.add(effect)  /* 储存对应的 dep */
          }
        }
      })
    }
  }

  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {if (effect.options.scheduler) { /* 放进 scheduler 调度 */
      effect.options.scheduler(effect)
    } else {effect() /* 不存在调度情况,直接执行 effect */
    }
  }

  //TODO: 必须首先运行计算属性的更新,以便计算的 getter
  // 在任何依赖于它们的正常更新 effect 运行之前,都可能失效。computedRunners.forEach(run) /* 依次执行 computedRunners 回调 */
  effects.forEach(run) /* 依次执行 effect 回调(TODO: 里面包括渲染 effect)*/
}

我们这里保留了 trigger 的核心逻辑

① 首先从 targetMap 中,根据当前 proxy 找到与之对应的 depsMap。
② 根据 key 找到 depsMap 中对应的 deps,然后通过 add 方法分离出对应的 effect 回调函数和 computed 回调函数。
③ 依次执行 computedRunners 和 effects 队列里面的回调函数,如果发现需要调度处理, 放进 scheduler 事件调度

值得注意的的是:

此时的 effect 队列中有我们上述负责渲染的 renderEffect,还有通过 effectAPI 建立的 effect,以及通过 watch 形成的 effect。我们这里只考虑到渲染 effect。至于后面的情况会在接下来的文章中和大家一起分享。

我们用一幅流程图说明一下 set 过程。

五 总结

我们总结一下整个数据绑定建立响应式大致分为三个阶段

1 初始化阶段:初始化阶段通过组件初始化方法形成对应的 proxy 对象,然后形成一个负责渲染的 effect。

2 get 依赖收集阶段:通过解析 template,替换真实 data 属性,来触发 get, 然后通过 stack 方法,通过 proxy 对象和 key 形成对应的 deps,将负责渲染的 effect 存入 deps。(这个过程还有其他的 effect,比如 watchEffect 存入 deps 中)。

3 set 派发更新阶段:当我们 this[key] = value 改变属性的时候,首先通过 trigger 方法,通过 proxy 对象和 key 找到对应的 deps,然后给 deps 分类分成 computedRunners 和 effect, 然后依次执行,如果需要 调度 的,直接放入调度。

还有一些问题没有解决,比如:

① 为什么要用 effectStack 数组来存放这里 effect。
② 什么时候向 deps 存入其他的 effect。
等等 …

带着这些问题,希望我们在接下来的文章中,一起探讨。

微信扫码关注公众号,定期分享技术文章

正文完
 0