响应式原理

开篇

我们以一段代码为例子:

<template>
  <img src="./logo.png">
  <h1>Hello Vue 3!</h1>
  <button @click="add">Clicked {{ state.observe }} times.</button>
</template>

<script>
import { ref } from 'vue'
import {computed, reactive} from "@vue/reactivity";
import {onMounted, watchEffect} from "@vue/runtime-core";

export default {
  setup() {
    const state = reactive({
      observe: 0,
      other: 2
    })

    const add = () => {
      state.observe++
    }

    return {
      add,
      state,
    }
  }
}
</script>

<style scoped>
img {
  width: 200px;
}
h1 {
  font-family: Arial, Helvetica, sans-serif;
}
</style>

Effect安装之前的流程

setupComoponet

这里就会执行我们写的setup函数,从源码来看。

这里主要做了几件事情:

  1. 执行setup函数,setup函数有我们的reactive()函数让数据成为一个new Proxy。我们的传入reactive函数的参数是这样的,记住为这个参数对象设置了一个new Proxy代理!!

     const state = reactive({
           observe: 0,
           other: 2
     })

     

  2. 拿到setup函数返回的结果就是下面这个对象,并且使用new Proxy对他进行代理,然后存储在instance中

     {
         add,
         state,
     }

setupRenderEffect函数

setupRenderEffect函数创建一个effect为后面的响应式开启之路作铺垫。下面试这个函数的源码

 const setupRenderEffect: SetupRenderEffectFn = (
     instance,
     initialVNode,
     container,
     anchor,
     parentSuspense,
     isSVG,
     optimized
   ) => {
     // create reactive effect for rendering
     instance.update = effect(function componentEffect() {
       ...
     }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
   }

函数的的第一个马上执行effect函数,传入的参数为一个 componentEffect的函数。第一个参数执行完之后是一个对象,对象的形式是这样的:

  {
    scheduler: queueJob,
    allowRecurse: true,
    onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
    onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
 }

  

下面来看下这个effect函数做了什么事情,这个是effect函数的源码:

 export function effect<T = any>(
   fn: () => T,
   options: ReactiveEffectOptions = EMPTY_OBJ
 ): ReactiveEffect<T> {
   if (isEffect(fn)) {
     fn = fn.raw
   }
   //这个effect是reactiveEffect这个函数
   const effect = createReactiveEffect(fn, options)
   if (!options.lazy) {
     effect()
   }
   return effect
 }

  

目前的步骤中不会执行到第一个if语句的内容,我们直接看第二句话const effect = createReactiveEffect(fn, options)下面是这个createReactiveEffect的源码

 function createReactiveEffect<T = any>(
   fn: () => T,
   options: ReactiveEffectOptions
 ): ReactiveEffect<T> {
   const effect = function reactiveEffect(): unknown {
     if (!effect.active) {
       return options.scheduler ? undefined : fn()
     }
     if (!effectStack.includes(effect)) {
       cleanup(effect)
       try {
         enableTracking()
         effectStack.push(effect)
         activeEffect = effect
         return fn()
       } finally {
         effectStack.pop()
         resetTracking()
         activeEffect = effectStack[effectStack.length - 1]
       }
     }
   } as ReactiveEffect
   effect.id = uid++
   effect._isEffect = true
   effect.active = true
   effect.raw = fn
   effect.deps = []
   effect.options = options
   return effect
 }

  

这里做了几件事情:

  1. effect是要给叫做reactiveEffect()的函数

  2. 然后在effect上定义了十分多的属性,其中有一个属性叫做deps。我们马上联想到2.x的deps属性

  3. 传入参数fn,不要忘记这个参数fn是一个叫做componentEffect()函数

  4. 最终把这个effect返回出去

 

 

我们回到上面的effect函数。接下来执行的是下面这段代码,看看上面这个options。并灭有这个lazy属性,那么下面就执行执行刚刚返回的effect函数,effect本质上是reactiveEffect函数,下面看看reactiveEffect函数做了什么事情

 export function effect<T = any>(
   fn: () => T,
   options: ReactiveEffectOptions = EMPTY_OBJ
 ): ReactiveEffect<T> {
   ...
   if (!options.lazy) {
     effect()
   }
   return effect
 }
 //effectStack是在本文件上面定义的一个全局变量。是一个数组
 const effectStack: ReactiveEffect[] = []
 ​
 function reactiveEffect(): unknown {
     if (!effect.active) {
       return options.scheduler ? undefined : fn()
     }
     if (!effectStack.includes(effect)) {
       cleanup(effect)
       try {
         enableTracking()
         effectStack.push(effect)
         activeEffect = effect
         return fn()
       } finally {
         effectStack.pop()
         resetTracking()
         activeEffect = effectStack[effectStack.length - 1]
       }
     }
   } as ReactiveEffect

  

在上面初始化effect属性的时候effect.active是被设置为false的,所以开始的时候不会执行if语句里面的代码。

然后判断effectStack里面是否函数这个effect。effectStack是在本文件上面定义的一个全局变量。是一个数组,初始化是没有任何东西的,那么代码就会执行进入if语句里面。

if语句第一句代码执行 cleanup(effect),cleanup() 的逻辑其实在Vue 2x的源码中也有的,避免依赖的重复收集。

然后,执行enableTracking()effectStack.push(effect),前者的逻辑很简单,即可以追踪,用于后续触发 track 的判断,:

 //trackStack也是本文件定义的一个全局数组
 const trackStack: boolean[] = []
 ​
 function enableTracking() {
     trackStack.push(shouldTrack);
     shouldTrack = true;
 }

  

然后把这个effct push 进入这个effectStack.那么此时effectStack就有一个元素,是一个effect,effect就是一个叫做reactiveEffect的函数,trackStack也有一个元素,是一个布尔值,他们两个分别是这样的:

effect [ ƒ reactiveEffect() ]

trackStack: [true]

 

做完这些工作之后把effect赋值给一个叫做activeEffect,最后执行fn,并且返回fn的执行结果。不要忘记fn是什么了,fn是一个叫做componentEffect()函数。

现在我们回头看会最初的setupRenderEffect函数的代码看看这个componentEffect()函数的作用。这个函数的的源码如下:

 function componentEffect() {
       if (!instance.isMounted) {
         let vnodeHook: VNodeHook | null | undefined
         const { el, props } = initialVNode
         const { bm, m, parent } = instance
 ​
         ...
         
         const subTree = (instance.subTree = renderComponentRoot(instance))
         
         ...
       } else {
        ...
       }
 }

  

我们只需要看第一个if语句块里面执行的结果,接下来进入组件的渲染阶段,我们直接看到renderComponentRoot函数。

下面是这个函数的源码:

 export function renderComponentRoot(
   instance: ComponentInternalInstance
 ): VNode {
   const {
     type: Component,
     vnode,
     proxy,
     withProxy,
     props,
     propsOptions: [propsOptions],
     slots,
     attrs,
     emit,
     render,
     renderCache,
     data,
     setupState,
     ctx
   } = instance
 ​
   let result
   currentRenderingInstance = instance
   if (__DEV__) {
     accessedAttrs = false
   }
   try {
     let fallthroughAttrs
     if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
       // withProxy is a proxy with a different `has` trap only for
       // runtime-compiled render functions using `with` block.
       const proxyToUse = withProxy || proxy
   
       result = normalizeVNode(
         //在这里就真正触发响应式依赖的收集和2.x类似
         render!.call(
           proxyToUse,
           proxyToUse!,
           renderCache,
           props,
           setupState,
           data,
           ctx
         )
       )
       fallthroughAttrs = attrs
     }
     ...
   return result
 }

 

开头这个函数从instance上解构出十分多的属性,instance是在前面的阶段产生的一个对象。关键我们来看这段代码:

const proxyToUse = withProxy || proxy

 const proxyToUse = withProxy || proxy 
 ​
 result = normalizeVNode(
     render!.call(
       proxyToUse,
       proxyToUse!,
       renderCache,
       props,
       setupState,
       data,
       ctx
     )
 )

  

withProxy 和 proxy都是从instance上解构出来的的。在我们这个例子中他们两个分别是这样的:withProxy是一个null,proxy是一个new Proxy代理

 withProxy: null
 proxy: Proxy

那很显然执行完const proxyToUse = withProxy || proxy这个之后,const proxyToUse = proxy。在本例子中它大概长这样:

 

 

然后接下来执行:

 result = normalizeVNode(
     render!.call(
       proxyToUse,
       proxyToUse!,
       renderCache,
       props,
       setupState,
       data,
       ctx
     )
 )

  

先执行render!.call这段代码,这段代码就是真正开始进行响应式响应式收集的入口。

这里会读取这个我们在开始定义的响应式数据即(下面代码的)state.observe,原因在于我们在模板中使用了这个state.observe。

但是注意这里其实会触发两个get。为什么?原因在于在开始的时候setup整一个返回的对象即在本列子中(如下)这个对象也设置了一个new Proxy代理。所以我们在模板首先访问的是state。会触发一次get。然后在访问state.observe.触发第二次get。并且在第一个次get的时候会去校验isRef。我们直接跳过第一个get看第二个get,即当我们访问state.observe的时候出发的get函数。

 {
     add,
     state,
 }

  

在开头的setupComoponet函数中,我们已经把这个reactive执行并且为里面的参数对象设置了一个new Proxy代理。

 const state = reactive({
   observe: 0,
   other: 2
 })

  

那么我们读取这个state.observe就会掉入new Proxy的get函数里面。我们现在来看看在reactive函数中为这个参数对象设置的get函数是长什么样子的。

 function get(target: Target, key: string | symbol, receiver: object) {
     ...
     const targetIsArray = isArray(target)
     if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
       //Reflect.get保证了Proxy的原生默认行为
       return Reflect.get(arrayInstrumentations, key, receiver)
     }
 ​
     const res = Reflect.get(target, key, receiver)
 ​
     const keyIsSymbol = isSymbol(key)
     if (
       keyIsSymbol
         ? builtInSymbols.has(key as symbol)
         : key === `__proto__` || key === `__v_isRef`
     ) {
       return res
     }
 ​
     if (!isReadonly) {
       track(target, TrackOpTypes.GET, key)
     }
 ​
     if (shallow) {
       return res
     }
 ​
     if (isRef(res)) {
       // ref unwrapping - does not apply for Array + integer key.
       const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
       return shouldUnwrap ? res.value : res
     }
 ​
     if (isObject(res)) {
       // Convert returned value into a proxy as well. we do the isObject check
       // here to avoid invalid value warning. Also need to lazy access readonly
       // and reactive here to avoid circular dependency.
       return isReadonly ? readonly(res) : reactive(res)
     }
 ​
     return res
   }

  

这个get函数有三个参数,第一个是target就是我们在reactive传入的对象参数(如下),第二个参数key是就是我是使用的observe

 {
     observe: 0,
     other: 2
 }

  

我省略了代码开头的部分if语句内容,因为他们在此时不会被执行。

目前函数第一句话执行判断target是不是要给Array,显然不是返回结果是false,那么自然不会走到下面的if语句。然后执行

const res = Reflect.get(target, key, receiver)

这里就是读取observe的值,那么读取后res的值为0.

读取完值后,判断下我们的key是不是Symbol。显然返回false。然后进入到了收集依赖最重要的这句话

 if (!isReadonly) {
       track(target, TrackOpTypes.GET, key)
 }

  

isReadonly在我的代码中会是false。所以会执行track函数。他三个参数,第一个参数就是我们reative的对象。第二个参数是一个TS的枚举类型,本质就是一个字符串’get‘,第三个参数就是我们的key observe。下面看看track的执行内容。

 //targetMap是本文件里面开头的一个全局变量,开始的时候没有内容
 const targetMap = new WeakMap<any, KeyToDepMap>()
 ​
 function track(target: object, type: TrackOpTypes, key: unknown) {
   if (!shouldTrack || activeEffect === undefined) {
     return
   }
 ​
   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(activeEffect)) {
     dep.add(activeEffect)
     activeEffect.deps.push(dep)
     if (__DEV__ && activeEffect.options.onTrack) {
       activeEffect.options.onTrack({
         effect: activeEffect,
         target,
         type,
         key
       })
     }
   }
 }

  

直接跳过开头的判定语句,来到这句话let depsMap = targetMap.get(target)因为开始的时候这个targetMap是没有东西的,然后它尝试去获取这个target对象对应的键值。显示获取不到,所以depsMap 是一个undefined。

然后下面的if语句就触发了,它为这个targetMap set了一个值。键就是target对象,值是一个Map。顺带把这个Map赋值给了depsMap。

那现在targetMap就有东西了,如下:

 targetMap
 0: {Object => Map(0)}
 ​
 键 Object就是 
 {
     observe: 0,
     other: 2
 }
   

  

接下来执行这句话let dep = depsMap.get(key)在上面depsMap已经得到了初始化,那么这句话尝试在这个depsMap Map中获取observe这个键的值,由于刚开始的时候这个Map并没有东西。所以dep也是一个undefined。

接下来就进入了if语句给这个dep和depsMap赋值

 if (!dep) {
     depsMap.set(key, (dep = new Set()))
 }
 ​
 ​
 //执行完这句话后,depsMap长这样
 depsMap: Map(1) {"observe" => Set(0)}
 ​
 dep同时变成了一个Set

  

接下来执行这句话,这句话(如下)就是正式收集依赖

 if (!dep.has(activeEffect)) {
     dep.add(activeEffect)
     activeEffect.deps.push(dep)
     if (__DEV__ && activeEffect.options.onTrack) {
       activeEffect.options.onTrack({
         effect: activeEffect,
         target,
         type,
         key
       })
     }
   }

  

还记得这个activeEffect是什么吗。这个activeEffect是一个函数名字叫做reactiveEffect()的函数。大家可以翻到上面去看看。

现在就把这个函数正式收录进入这个dep中。并且将当前 dep 添加到 activeEffectdeps 数组中。和2.x十分得相似。2.x代码是这样的:

 if (Dep.target) {
   dep.depend()
   ...
 }

  

那么整个track的收集依赖的过程结束了。

最后get函数把访问属性的结果的值返回出去。就完成了这个get函数的过程。

上面介绍完怎么收集依赖。后面的执行过程就不细讲。总结一下整个get的流程(借用别人的一张图片):

 

 

感觉其实和2.x的思想有相同的地方。只是一些细节表现上不同。

触发set方法收集依赖的过程

假如我们尝试点击按钮,然这个state.observe进行++。现在就触发了set方法

我看看set函数的代码:

 
function set(
   target: object,
   key: string | symbol,
   value: unknown,
   receiver: object
 ): boolean {
   const oldValue = (target as any)[key]
   if (!shallow) {
     value = toRaw(value)
     if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
       oldValue.value = value
       return true
     }
   } else {
     // in shallow mode, objects are set as-is regardless of reactive or not
   }
 ​
   const hadKey =
     isArray(target) && isIntegerKey(key)
       ? Number(key) < target.length
       : hasOwn(target, key)
   const result = Reflect.set(target, key, value, receiver)
   // don't trigger if target is something up in the prototype chain of original
   if (target === toRaw(receiver)) {
     if (!hadKey) {
       trigger(target, TriggerOpTypes.ADD, key, value)
     } else if (hasChanged(value, oldValue)) {
       trigger(target, TriggerOpTypes.SET, key, value, oldValue)
     }
   }
   return result
 }

  

第一句话就是重新获取旧的值,然后执行toRaw方法。toRaw方法源码如下,先判断你传入的参数是否有东西。然后继续调用toRaw,但是这次传入的值是从observed['__v_raw']但是显然我们的value只是一个简单的数字并没有这个属性。显然就是直接返回这个值了。

那最后执行完这个toRaw方法后就是简单把值返回一下

   // export const enum ReactiveFlags {
   //   SKIP = '__v_skip',
   //   IS_REACTIVE = '__v_isReactive',
   //   IS_READONLY = '__v_isReadonly',
   //   RAW = '__v_raw'
   // }
 ​
 export function toRaw<T>(observed: T): T {
   return (
     (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
   )
 }

  

然后执行这段代码:

 const hadKey =
     isArray(target) && isIntegerKey(key)
       ? Number(key) < target.length
       : hasOwn(target, key)
 ​
 //显然target不是一个数组。那么就执行 hasOwn(target, key)
 //下面是hasOwn源码
 ​
 const hasOwnProperty = Object.prototype.hasOwnProperty
 export const hasOwn = (
   val: object,
   key: string | symbol
 ): key is keyof typeof val => hasOwnProperty.call(val, key)
 ​
 //在target中显然有这个key属性。所以这里返回true
 //即hadKey==true

  

然后接下来继续往下看set这段代码:

   // don't trigger if target is something up in the prototype chain of original
   if (target === toRaw(receiver)) {
     if (!hadKey) {
       trigger(target, TriggerOpTypes.ADD, key, value)
     } else if (hasChanged(value, oldValue)) {
       trigger(target, TriggerOpTypes.SET, key, value, oldValue)
     }
   }
   return result

  

先进行一个判断。根据刚刚的分析。这里执行结果是true。上面看到hadKey的值为true。那么就进入了else分支trigger(target, TriggerOpTypes.SET, key, value, oldValue)

trigger函数就是派发更新的方法,下面是trigger函数的源码:

 export function trigger(
   target: object,
   type: TriggerOpTypes,
   key?: unknown,
   newValue?: unknown,
   oldValue?: unknown,
   oldTarget?: Map<unknown, unknown> | Set<unknown>
 ) {
   const depsMap = targetMap.get(target)
   if (!depsMap) {
     // never been tracked
     return
   }
 ​
   const effects = new Set<ReactiveEffect>()
   const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
     ...
   }
 ​
   if (type === TriggerOpTypes.CLEAR) {
     ...
   } else if (key === 'length' && isArray(target)) {
     ...
   } else {
     // schedule runs for SET | ADD | DELETE
     if (key !== void 0) {
       add(depsMap.get(key))
     }
 ​
     // also run for iteration key on ADD | DELETE | Map.SET
     switch (type) {
       case TriggerOpTypes.ADD:
         ...
       case TriggerOpTypes.DELETE:
         ...
       case TriggerOpTypes.SET:
         if (isMap(target)) {
           add(depsMap.get(ITERATE_KEY))
         }
         break
     }
   }
 ​
   const run = (effect: ReactiveEffect) => {
     if (__DEV__ && effect.options.onTrigger) {
       effect.options.onTrigger({
         effect,
         target,
         key,
         type,
         newValue,
         oldValue,
         oldTarget
       })
     }
     if (effect.options.scheduler) {
       effect.options.scheduler(effect)
     } else {
       effect()
     }
   }
 ​
   effects.forEach(run)
 }

  

第一句话执行const depsMap = targetMap.get(target)从上面的分析知道targetMap是这样的:

 

 

那从Map中获取这个object的键值。所以const depsMap = Map(1) {"observe" => Set(1)}.

然后下面定义了一个set和一个add函数,暂且不看add函数,紧接着进入一系列的判断语句,我们传入的type参数是一个'set'字符串并且我们的key不是'length'。所以最后进入了else分支,裁剪出else分支代码如下:

  {
     // schedule runs for SET | ADD | DELETE
     if (key !== void 0) {
       add(depsMap.get(key))
     }
 ​
     // also run for iteration key on ADD | DELETE | Map.SET
     switch (type) {
       case TriggerOpTypes.ADD:
         ...
       case TriggerOpTypes.DELETE:
         ...
       //这个case条件等同于case 'set
       case TriggerOpTypes.SET:
         if (isMap(target)) {
           add(depsMap.get(ITERATE_KEY))
         }
         break
     }
   }

  

我们的key不是undefined。所以进入if语句,if语句执行add方法,看看add源码:

 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
   if (effectsToAdd) {
     effectsToAdd.forEach(effect => {
       //const effects = new Set<ReactiveEffect>() 在set开头源码定义
       if (effect !== activeEffect || effect.options.allowRecurse) {
         effects.add(effect)
       }
     })
   }
 }

  

上面传入的参数是depsMap.get(key),这个执行结果就是获取到一个set(set的截图如下)。set里的内容就是function reactiveEffect()就是我们在get收集到的依赖。

 //depsMap的样子
 0: {"observe" => Set(1)}
 //key就是observe

 

 

 

 

在add方法中,先判断参数是否存在。存在的话就遍历这个Set.并且添加到这个effects的Set中。

接下来进入switch语句,switch语句我们会进入最后那个:

 switch (type) {
       case TriggerOpTypes.ADD:
         ...
       case TriggerOpTypes.DELETE:
         ...
       //这个case条件等同于case 'set
       case TriggerOpTypes.SET:
         if (isMap(target)) {
           add(depsMap.get(ITERATE_KEY))
         }
         break
 }

  

target显示不是一个Map,是一个对象。直接break。

最后的一段代码如下:

 const run = (effect: ReactiveEffect) => {
     if (__DEV__ && effect.options.onTrigger) {
       effect.options.onTrigger({
         effect,
         target,
         key,
         type,
         newValue,
         oldValue,
         oldTarget
       })
     }
     if (effect.options.scheduler) {
       effect.options.scheduler(effect)
     } else {
       effect()
     }
   }
   
   effects.forEach(run)
 }

  

最后定义了一个run方法。然后遍历effects Set里面的内容。逐个去执行run方法。看看run方法。

在执行run方法之前我们需要回顾一下上面定义的effect(非effects)是个什么东西,因为这个effect就是run方法的参数,同时也是effects的Set里面的元素,它长这样的:

 const effect = function reactiveEffect(): unknown {
     ...
 } as ReactiveEffect
 ​
   effect.id = uid++
   effect._isEffect = true
   effect.active = true
   effect.raw = fn
   effect.deps = []
   effect.options = {
     allowRecurse: true
     onTrack: undefined
     onTrigger: undefined
     scheduler: ƒ queueJob(job)
   }

  

根据它的长相,显然进入第一个分支:

 if (effect.options.scheduler) {
    effect.options.scheduler(effect)
 }

  

effect.options.scheduler是一个叫做queueJob的函数,看看它的源码

 //本文件开头定义的一个全局变量
 const queue: (SchedulerJob | null)[] = []
 ​
 export function queueJob(job: SchedulerJob) {
   if (
     (!queue.length ||
       !queue.includes(
         job,
         isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
       )) &&
     job !== currentPreFlushParentJob
   ) {
     queue.push(job)
     queueFlush()
   }
 }

  

显然这段代码先把effect推入这个数组里面。在执行queueFlush函数,queueFlush函数如下:

 function queueFlush() {
   if (!isFlushing && !isFlushPending) {
     isFlushPending = true
     currentFlushPromise = resolvedPromise.then(flushJobs)
   }
 }

  

到这里其实和Vue 2.x已经很相似了。在 Vue 2x 中的 watcher 也是在下一个 tick 中执行,而 Vue 3.0 也是一样。而 flushJobs 中就会对 queue 队列中的 effect() 进行执行。后面就是最后执行准备渲染的逻辑。就不细说了。

把整个set整理为一个简单的流程图:

 

 

 

 

参考链接:https://segmentfault.com/a/1190000022198316

 

posted on 2020-10-11 17:11  余圣源  阅读(418)  评论(0编辑  收藏  举报