vue3响应式--proxy

vue3.0快发布了,也带来了很多新的特性,如新的监测设计,PWA,TS支持等,本节一起了解下新的监测原理。

旧的响应式原理

vue2利用Object.defineProperty来劫持data数据的getter和setter操作。这使得data在被访问或赋值时,动态更新绑定的template模块。

对象:会递归得去循环vue得每一个属性,(这也是浪费性能的地方)会给每个属性增加getter和setter,当属性发生变化的时候会更新视图。

数组:重写了数组的方法,当调用数组方法时会触发更新,也会对数组中的每一项进行监控。

缺点:对象只监控自带的属性,新增的属性不监控,也就不生效。若是后续需要这个自带属性,就要再初始化的时候给它一个undefined值,后续再改这个值

数组的索引发生变化或者数组的长度发生变化不会触发实体更新。可以监控引用数组中引用类型值,若是一个普通值并不会监控,例如:[1, 2, {a: 3}] ,只能监控a

Proxy消除了之前 Vue2.x 中基于 Object.defineProperty 的实现所存在的这些限制:无法监听 属性的添加和删除、数组索引和长度的变更,并可以支持 Map、Set、WeakMap 和 WeakSet!

Proxy及使用

MDN 上是这么描述的——Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~

一个例子:

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 
let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        return target[key]
    },
    set (target,key,value) {
        console.log('触发更新')
        target[key] = value
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
proxy.arr   //收集依赖
proxy.name = '123'  //触发更新

定义了一个对象obj,通过代理后的对象(上面的proxy)来操作原对象。当取值的时候会走get方法,返回对应的值,当设置值的时候会走set方法,触发更新。

但这是老的写法,新的写法是使用Reflect。

Reflect是内置对象,为操作对象而提供的新API,将Object对象的属于语言内部的方法放到Reflect对象上,即从Reflect对象上拿Object对象内部方法。 如果出错将返回false

简单改写上面这个例子:

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        // return target[key]
        //Reflect 反射 这个方法里面包含了很多api
        return Reflect.get(target,key)
    },
    set (target,key,value) {
        console.log('触发更新')
        // target[key] = value //这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        Reflect.set(target,key,value)
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
proxy.arr  //收集依赖
proxy.name.name //收集依赖(只有一个)
proxy.name = '123' //触发更新

效果依旧和上面一样。

但是有一个问题,这个对象是多层对象,它并不会取到里面的那个name的值。

这是因为之前Object.defineProperty方法是一开始就会对这个多层对象进行递归处理,所以可以拿到,而Proxy不会。它是懒代理。如果对这个对象里面的值进行代理就取不到值。就像上面我们只对name进行了代理,但并没有对name.name进行代理,所以他就取不到这个值,需要代理之后才能取到。

改进如下:

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        if(typeof target[key] === 'object' && target[key] !== null){
            //递归代理,只有取到对应值的时候才会代理
            return new Proxy(target[key],handler)
        }
        
        // return target[key]
        //Reflect 反射 这个方法里面包含了很多api
        return Reflect.get(target,key)
    },
    set (target,key,value) {
        console.log('触发更新')
        // target[key] = value //这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        Reflect.set(target,key,value)
    }
}

let proxy = new Proxy(obj,handler)
proxy.arr  //收集依赖
proxy.name.name //收集依赖(2个)

接下来看看数组的代理过程:

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        if(typeof target[key] === 'object' && target[key] !== null){
            //递归代理,只有取到对应值的时候才会代理
            return new Proxy(target[key],handler)
        }
        
        // return target[key]
        //Reflect 反射 这个方法里面包含了很多api
        return Reflect.get(target,key)
    },
    set (target,key,value) {
        console.log('触发更新')
        // target[key] = value //这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        return Reflect.set(target,key,value)
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
// proxy.name.name = '123' //设置值,取一次,设置一次
proxy.arr.push(456)
//输出
收集依赖 (proxy.arr)
收集依赖 (proxy.arr.push)
收集依赖 (proxy.arr.length)
触发更新 写入新值
触发更新 长度改变
proxy.arr[0]=456
//输出
收集依赖 (proxy.arr)
触发更新 写入新值

这里面它会走两次触发更新的操作,因为第一次需要修改数组的长度,第二次再把元素放进数组里。所以我们需要判断一下它是新增操作还是修改操作

判断新旧属性:

set (target,key,value) {
        let oldValue = target[key]
        console.log(key, oldValue, value)
        if(!oldValue){
            console.log('新增属性')
        }else if(oldValue !== value){
            console.log('修改属性')
        }
        return Reflect.set(target,key,value)
    }

首先拿到它的旧值,如果这个值不存在就是新增,如果存在但不相等就是修改操作

vue3的响应式设计

Vue 3.0 的想法是引入灵感来自于 React Hook 的 Function-based API,作为主要的组件声明方式。

意思就是所有组件的初始状态、computed、watch、methods 都要在一个叫做 setup 的方法中定义,抛弃(暂时会继续兼容)原有的基于对象的组件声明方式。

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

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

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

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

整个mountComponent的主要分为了三步,我们这里分别介绍一下每个步骤干了什么:

① 第一步: 创建component 实例 。
② 第二步:初始化组件,建立proxy ,根据字符窜模版得到render函数。生命周期钩子函数处理等等
③ 第三步:建立一个渲染effect,执行effect。
从如上方法中我们可以看到,在setupComponent已经构建了响应式对象,但是还没有初始化收集依赖。

setupRenderEffect 构建渲染effect

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

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

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函数。

接着看createReactiveEffect(这就是vue2.x中的Watcher)

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的作用主要是配置了一些初始化的参数,然后包装了之前传进来的fn重要的一点是把当前的effect赋值给了activeEffect,这一点非常重要,和收集依赖有着直接的关系.
整个响应式初始化阶段进行总结
① setupComponent创建组件,调用composition-api,处理options(构建响应式)得到Observer对象。
② 创建一个渲染effect,里面包装了真正的渲染方法componentEffect,添加一些effect初始化属性。
③ 然后立即执行effect,然后将当前渲染effect赋值给activeEffect

数据响应
vue3新的响应式书写方式(老的也兼容)

setup() {
     const state = {
         count: 0,
         double: computed(() => state.count * 2)
     }
     function increment() {
         state.count++
     }
     
    onMounted(() => {
        console.log(state.count)
    })
    
    watch(() => {
        document.title = `count ${state.count}`
   })
    return {
        state,
      increment
   }
}

感觉setup这块就有点像 react hooks 理解成一个带有数据的逻辑复用模块,不再以vue组件为单位的代码复用了
和React钩子不同,setup()函数仅被调用一次。
所以新的响应书数据两种声明方式:

  1. Ref
    前提:声明一个类型 Ref
    ref()函数源码:
function ref(raw: unknown) {
   if (isRef(raw)) {
     return raw
   }
   // convert 内容:判断 raw是不是对象,是的话 调用reactive把raw响应化
   raw = convert(raw)
   const r = {
     _isRef: true,
     get value() {
      // track 理解为依赖收集
      track(r, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      // trigger 理解为触发监听,就是触发页面更新好了
      trigger(r, OperationTypes.SET, '')
    }
  }
  return r as Ref
}

上面的convert函数内容为

const convert = val => isObject(val) ? reactive(val) : val

可以看得出 ref类型 只会包装最外面一层,内部的对象最终还是调用reactive,生成Proxy对象进行响应式代理。
疑问:可能有人想问,为什么不都用proxy, 内部对象都用proxy,最外层还要搞个 Ref类型,多此一举吗?
理由可能比较简单,那就是proxy代理的都是对象,对于基本数据类型,函数传递或对象结构是,会丢失原始数据的引用。
官方解释:

However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:

  1. Reactive
    源码如下:
    注:target一定是一个对象,不然会报警告
function reactive(target) {
   // 如果target是一个只读响应式数据
   if (readonlyToRaw.has(target)) {
     return target
   }
   // 如果是被用户标记的只读数据,那通过readonly函数去封装
   if (readonlyValues.has(target)) {
     return readonly(target)
   }
  // go ----> step2
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers, // 注意传递
    mutableCollectionHandlers
  )
}

createReactiveObject函数如下:

function createReactiveObject(
   target: unknown,
   toProxy: WeakMap<any, any>,
   toRaw: WeakMap<any, any>,
   baseHandlers: ProxyHandler<any>,
   collectionHandlers: ProxyHandler<any>
 ) {
     // 判断target不是对象就 警告 并退出
   if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 通过原始数据 -> 响应数据的映射,获取响应数据
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // 如果原始数据本身就是个响应数据了,直接返回自身
  if (toRaw.has(target)) {
    return target
  }
  // 如果是不可观察的对象,则直接返回原对象
  if (!canObserve(target)) {
    return target
  }
  // 集合数据与(对象/数组) 两种数据的代理处理方式不同
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 声明一个代理对象 ----> step3
  observed = new Proxy(target, handlers)
  // 两个weakMap 存target observed
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

通过上面源码创建proxy对象的大致流程是这样的:

①首先判断目标对象有没有被proxy响应式代理过,如果是那么直接返回对象。

②然后通过判断目标对象是否是[ Set, Map, WeakMap, WeakSet ]数据类型来选择是用collectionHandlers , 还是baseHandlers->就是reactive传进来的mutableHandlers作为proxy的hander对象。

③最后通过真正使用new proxy来创建一个observed ,然后通过rawToReactive reactiveToRaw 保存 target和observed键值对。

拦截器
在baseHandles基类处理对象中可以看到对set/get的拦截处理
(我们以对象类型为例,集合类型的handlers稍复杂点)
handlers如下,new Proxy(target, handles)的 handles就是下面这个对象

export const mutableHandlers = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

依赖收集
打开createGetter(false)看实现思路如下:
思路:当我们代理get获取到res时,判断res 是否是对象,如果是那么 继续reactive(res),可以说是一个递归

reactive(target) ->
createReactiveObject(target,handlers) ->
new Proxy(target, handlers) ->
createGetter(readonly) ->
get() -> res ->
isObject(res) ? reactive(res) : res

function createGetter(isReadonly: boolean) {
   // isReadonly 用来区分是否是只读响应式数据
   // receiver即是被创建出来的代理对象
   return function get(target: object, key: string | symbol, receiver: object) {
     // 获取原始数据的响应值
     const res = Reflect.get(target, key, receiver)
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
     }
    if (isRef(res)) {
      return res.value
    }
    // 收集依赖
    track(target, OperationTypes.GET, key)
    // 这里判断上面获取的res 是否是对象,如果是对象 则调用reactive并且传递的是获取到的res,
    // 则形成了递归
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

在vue2.0的时候。响应式是在初始化的时候就深层次递归处理了

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

这样做好处是:

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

先来看看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)
  }
}

里面主要引入了两个概念 targetMap和 depsMap
track作用大致是,首先根据 proxy对象,获取存放deps的depsMap,然后通过访问的属性名key获取对应的dep,然后将当前激活的effect存入当前dep收集依赖。

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

为了方便理解,targetMap 和 depsMap的关系,下面我们用一个例子来说明:

例子:

<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')

渲染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的一个主要作用去触发监听,使试图更新,需要注意的是控制什么时候才是视图需要真的更新

function set(
   target: object,
   key: string | symbol,
   value: unknown,
   receiver: object
 ): boolean {
   // 拿到新值的原始数据
   value = toRaw(value)
   // 获取旧值
  const oldValue = (target as any)[key]
  // 如果旧值是Ref类型,新值不是,那么直接更新值,并返回
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // 如果是原始数据原型链上的数据操作,不做任何触发监听函数的行为。
  if (target === toRaw(receiver)) {
    // 更新的两种条件 
    // 1. 不存在key,即当前操作是在新增属性
    // 2. 旧值和新值不等
    if (!hadKey) {
      trigger(target, OperationTypes.ADD, key)
    } else if (hasChanged(value, oldValue)) {
      trigger(target, OperationTypes.SET, key)
    }
  }
  return result
}

疑问:对于数据的set操作会出发多次traps?
这里有个前提了解:就是我们日常修改数组,比如 let a = [1], a.push(2),
这个push操作,我们是实际上是对a做了2个属性的修改,1,set length 1; 2. set value 2
所以我们的set traps会出发多次
思路:通过属性值和value控制,比如当 set key是 length的时候,我们可以判断当前数组 已经有此属性,所以不需要出发更新,当新设置的值和老值一样是也不需要更新。

疑问:set的源码里面有 有一个 target === toRaw(receiver)条件下才继续操作 trigger更新视图?
这里就暴露出一个东西,即存在 target !== toRaw(receiver)
Receiver: 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)

其实源码有注释

// don’t trigger if target is something up in the prototype chain of original

即如果我们的操作是操作原始数据原型链上的数据操作,target 就不等于 toRaw(receiver)
什么情况下 target !== toRaw(receiver)
例如:

const child = new Proxy(
   {},
   { // 其他 traps 省略
     set(target, key, value, receiver) {
       Reflect.set(target, key, value, receiver)
       console.log('child', receiver)
       return true
     }
   }
)

const parent = new Proxy(
  { a: 10 },
  { // 其他 traps 省略
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      console.log('parent', receiver)
      return true
    }
  }
)

Object.setPrototypeOf(child, parent) // child.__proto__ === parent true

child.a = 4

// 结果
// parent Proxy {a: 4}
// Proxy {a: 4}

从结果可以看出,理论上 parent的set应该不会触发,但实际是触发了,此时

target: {a: 10}
receiver: Proxy {a: 4}
// 在vue3中
toRaw(receiver): {a: 4} 

set的流程大致是这样的
① 首先通过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事件调度