02-响应系统的作用与实现

27 阅读13分钟

1、响应式数据的基本实现

响应式要符合两个基本特征,在方法执行时里面的字段时需要触发读操作,修改字段时要触发写操作。用两幅图来表达一下(es2015之前只能使用Object.defineProperty实现 Vue2,es2015之后可以可以使用Proxy Vue3)

2、实现一个微型的响应式系统

<body></body>
<script>

  // 创建一个副作用桶
  const tong = new Set()

  // 原始数据
  const data = { text: 'hello' }

  // 对原始数据代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 放到桶里
      tong.add(effect)

      // 返回属性值
      return target[key]
    },
    // 拦截写入操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue

      // 副作用取出来并执行
      tong.forEach(fn => fn())

      // 返回true表示成功
      return true
    }
  })
  // 测试代码
  // 副作用函数
  function effect() {
    document.body.innerText = obj.text
  }
  // 执行副作用函数,触发读取
  effect()
  // 1 秒后修改响应式数据
  setTimeout(() => {
    obj.text = 'hello yyx'
  }, 1000)

</script>

完善响应式系统,添加activeEffect,使响应式系统不受方法名限制。

<body></body>
<script>

  // 创建一个副作用桶
  const tong = new Set()

  // 原始数据
  const data = { text: 'hello' }

  // 对原始数据代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 放到桶里(但是换人了 activeEffect 哟~)
      if (activeEffect) {
        tong.add(activeEffect)
      }
      

      // 返回属性值
      return target[key]
    },
    // 拦截写入操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue

      // 副作用取出来并执行
      tong.forEach(fn => fn())

      // 返回true表示成功
      return true
    }
  })

  // 用一个全局变量存储被注册的副作用函数
  let activeEffect

  // effect 函数用于注册副作用函数
  function effect(fn) {
    activeEffect = fn
    // 执行fn
    fn()
  }
  // 匿名函数方式,触发读取
  effect(
    // 一个匿名的副作用函数
    () => {
      console.log('effect run')
      document.body.innerText = obj.text
    }
  )
  // 1 秒后修改响应式数据
  setTimeout(() => {
    obj.text = 'hello yyx'
  }, 1000)



</script>

再次优化,定时器尝中试修改obj中,不存在的字段,作用桶应该只调用一次,但是调用了两次,不存在的属性是不可以再次调用到了桶里的方法,所以需要进一步优化。

<body></body>
<script>

  // 桶改为WeakMap map有key,可以区分桶中的东西
  const tong = new WeakMap()

  // 原始数据
  const data = { text: 'hello' }

  // 对原始数据代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 没activeEffect就 return
      if (!activeEffect) {
        return target[key]
      }

      // 桶中取得depsMap, 是map 类型 :key --> effects
      let depsMap = tong.get(target)
      // 如果不存在 depsMap,则新建一个Map 并与target 关联
      if (!depsMap) {
        tong.set(target, (depsMap = new Map()))
      }

      //再根据key从depsMap中取得deps, deps是Set类型
      // 里面存储着所有与当前 key 相关联的作用函数:effects
      let deps = depsMap.get(key)
      // 如果deps 不存在,新建一个 set 与key关联
      if (!deps) {
        depsMap.set(key, (deps = new Set()))
      }

      // 最后激活的副作用方法塞到同理
      deps.add(activeEffect)

      // 返回属性值
      return target[key]
    },
    // 拦截写入操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue

      // 根据 target 从桶中取得 depsMap
      const depsMap = tong.get(target)

      if (!depsMap) return

      // 根据key取得所有副作用函数
      const effects = depsMap.get(key)

      // 执行副作用
      effects && effects.forEach(fn => fn())

      
    }
  })

  // 用一个全局变量存储被注册的副作用函数
  let activeEffect

  // effect 函数用于注册副作用函数
  function effect(fn) {
    activeEffect = fn
    // 执行fn
    fn()
  }
  // 匿名函数方式,触发读取
  effect(
    // 一个匿名的副作用函数
    () => {
      console.log('effect run')
      document.body.innerText = obj.text
    }
  )
  // 1 秒后修改响应式数据
  setTimeout(() => {
    obj.notExist = 'hello yyx'
  }, 1000)



</script>

这回在没有对应字段时,不会调用两次作用方法了

下图为WeakMap结构图

拓展:WeakMap与Map的区别

WeakMap不会长期持有对象,对key是弱引用,不影响垃圾回收机制。Map则不管target是否有用仍然长期持有。

3、封装触发(trigger)方法和追踪(track)方法

封装set方法和get方法内部的操作,使其更加灵活。

<body></body>
<script>

  // 桶改为WeakMap map有key,可以区分桶中的东西
  const tong = new WeakMap()

  // 原始数据
  const data = { text: 'hello' }

  // 对原始数据代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      track(target, key)

      // 返回属性值
      return target[key]
    },
    // 拦截写入操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue
      
      // 副作用方法桶里取出并执行
      trigger(target, key)
    }
  })

  // 封装方法 get部分调用track方法追踪
  function track(target, key){
    // 没activeEffect就 return
    if (!activeEffect) {
      return target[key]
    }

    // 桶中取得depsMap, 是map 类型 :key --> effects
    let depsMap = tong.get(target)
    // 如果不存在 depsMap,则新建一个Map 并与target 关联
    if (!depsMap) {
      tong.set(target, (depsMap = new Map()))
    }

    //再根据key从depsMap中取得deps, deps是Set类型
    // 里面存储着所有与当前 key 相关联的作用函数:effects
    let deps = depsMap.get(key)
    // 如果deps 不存在,新建一个 set 与key关联
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }

    // 最后激活的副作用方法塞到同理
    deps.add(activeEffect)
  }

  //触发方法 set调用
  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap
    const depsMap = tong.get(target)

    if (!depsMap) return

    // 根据key取得所有副作用函数
    const effects = depsMap.get(key)

    // 执行副作用
    effects && effects.forEach(fn => fn())
  }

  // 用一个全局变量存储被注册的副作用函数
  let activeEffect

  // effect 函数用于注册副作用函数
  function effect(fn) {
    activeEffect = fn
    // 执行fn
    fn()
  }
  // 匿名函数方式,触发读取
  effect(
    // 一个匿名的作用函数
    () => {
      console.log('effect run')
      document.body.innerText = obj.text
    }
  )
  // 1 秒后修改响应式数据
  setTimeout(() => {
    obj.notExist = 'hello yyx'
  }, 1000)



</script>

4、分支切换和clean up

分支切换:举个简单的例子effectFn函数中存在三元表达式,在分支切换时会遗留作用。简单修改一下代码,在之前的obj中添加ok字段,通过ok进行三元表达式的判断。

effect(function effectFn() {
  console.log('effect run')
  document.body.innerText = obj.ok ? obj.text : 'not'
})

setTimeout(() => {
  obj.ok = false
  obj.text = 'hello vue3'
}, 1000)

修改了ok为false,触发更新,页面显示not,修改text字符串页面不会产生变化,但是仍然触发了更新,只是由于ok始终是false所以页面没有产生变化。

产生了不必要的更新,ok没有变化,由ok决定的变化不需要更新,所以响应式系统仍然需要继续改善。

响应式系统中副作用函数应该能明确知道包含哪些依赖集合,在每次执行副作用函数前先进行依赖集合的移除,解决了遗留副作用函数的问题,改造的关系图和代码如下

  // 桶改为WeakMap map有key,可以区分桶中的东西
  const tong = new WeakMap()

  // 原始数据
  const data = {ok: true, text: 'hello world' }

  // 对原始数据代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      track(target, key)

      // 返回属性值
      return target[key]
    },
    // 拦截写入操作
    set(target, key, newValue) {
      // 设置属性值
      target[key] = newValue
      
      // 副作用方法桶里取出并执行
      trigger(target, key)
    }
  })

  // 封装方法 get部分调用track方法追踪
  function track(target, key){
    // 没activeEffect就 return
    if (!activeEffect) return
    

    // 桶中取得depsMap, 是map 类型 :key --> effects
    let depsMap = tong.get(target)

    // 如果不存在 depsMap,则新建一个Map 并与target 关联
    if (!depsMap) {
      tong.set(target, (depsMap = new Map()))
    }

    //再根据key从depsMap中取得deps, deps是Set类型
    // 里面存储着所有与当前 key 相关联的副作用函数:effects
    let deps = depsMap.get(key)
    // 如果deps 不存在,新建一个 set 与key关联
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }

    // 最后激活的副作用方法塞到同理
    deps.add(activeEffect)

    //deps就是一个与当前作用函数存在联系的依赖
    //添加到activeEffect.deps数组中
    activeEffect.deps.push(deps)
  }

  //触发方法 set调用
  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap
    const depsMap = tong.get(target)

    if (!depsMap) return

    // 根据key取得所有副作用函数
    const effects = depsMap.get(key)

    // 执行作用
    effects && effects.forEach(fn => fn())
  }

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {

  const effectFn = () => {
    //清除
    cleanup(effectFn)
    //当 effectFn 执行时,将其设置为激活当前的副作用函数
    activeEffect = effectFn
    fn()

    
  }
  
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖聚合
  effectFn.deps = []
  // 执行作用函数
  effectFn()
}


//cleanup函数
function cleanup(effectFn) {
  // 遍历 effectFn.deps 数组
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps是依赖集合
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集合中移除
    deps.delete(effectFn)
  }
  // 最后重置effectFn.deps数组
  effectFn.deps.length = 0
}

上面代码仍然存在问题,运行时会产生无限循环的问题。调用副作用函数时会调用cleanup,清除操作就是effect副作用函数剔除,执行副作用函数又会加回来。解决的方法就是新增一个Set

//触发方法 set调用
  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap
    const depsMap = tong.get(target)

    if (!depsMap) return

    // 根据key取得所有作用函数
    const effects = depsMap.get(key)

    const effectsToRun = new Set(effects)

    effectsToRun.forEach(effectFn => effectFn())

  }

添加了新的Set就可以避免遍历没结束就清空,导致无限循环的问题

5、嵌套effect和effect栈

当前的响应式系统仍然存在着问题,这个问题就是,不能嵌套,如果当前的响应式系统执行嵌套操作就会出现如下效果

补充如下代码进行测试

const data = { foo: true, bar: true }

let temp1, temp2

effect(function effectFn1() {
  console.log('effectFn1 执行')

  effect(function effectFn2() {
    console.log('effectFn2 执行')
    temp2 = obj.bar
  })

  temp1 = obj.foo
})

obj.foo = false

添加了全局的变量,和嵌套操作,前面可以顺利执行,但当修改foo值的时候出现了问题,foo在嵌套的外层,应该再次执行的是effectFn1,但是实际执行的却是effectFn2

问题出现在这里,effectFn直接进行了覆盖,那么执行的肯定永远是最里层的方法。

//当 effectFn 执行时,将其设置为激活当前的副作用函数
    activeEffect = effectFn
    fn()

解决的方法很简单就是加入栈的结构。

/ 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect栈
const effectStack = []

function effect(fn) {

  const effectFn = () => {
    //清除
    cleanup(effectFn)
    //当 effectFn 执行时,将其设置为激活当前的作用函数
    activeEffect = effectFn
    //调用之前作用函数压入栈
    effectStack.push(effectFn)
    fn()

    //执行完毕之后出栈
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }

  // activeEffect.deps 用来存储所有与该作用函数相关联的依赖聚合
  effectFn.deps = []
  // 执行作用函数
  effectFn()
}

嵌套最里层在栈顶,每执行完成一个方法之后弹出一个栈中元素。

6、避免无限递归

响应式系统的设计需要注意许多细节,当前状态的响应式系统存在着无限递归的问题,举个栗子

 const data = { foo: 1}
effect(() => obj.foo++)

修改数据,进行自增操作,会产生报错,超出了栈的大小。

出现这个问题的原因如下:

obj.foo++实际是两步操作,是obj.foo = obj.foo + 1

设置值会调用trigger方法,读取会触发track方法,此时在设置读取值的时候又设置了值,会在“桶”中调用同一个副作用函数,上一个操作没有执行完毕,又执行另一个操作,反复使用这个副作用函数,造成无限递归,导致栈的溢出。

解决方案:添加判断,在当前副作用函数没有执行完毕时,不可以执行相同的副作用函数

修改后的具体代码如下:

//触发方法 set调用
  function trigger(target, key) {
    // 根据 target 从桶中取得 depsMap
    const depsMap = tong.get(target)

    if (!depsMap) return

    // 根据key取得所有副作用函数
    const effects = depsMap.get(key)

    const effectsToRun = new Set()

    effect && effects.forEach(effectFn => {
      //如果trigger出发执行的作用函数,与正在执行的相同,则不触发执行
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })

    effectsToRun.forEach(effectFn => effectFn())

  }

7、lazy与计算属性

lazy:就是不需要立即执行副作用函数,其具体实现过程大致如下

vue中的lazy通过使用options.lazy设置懒加载。需要在effect函数中补充options属性,在非lazy的时候才执行effect,手动执行作用函数拿到其返回值。

//修改后的trigger包含调度器
//触发方法 set调用
  function trigger(target, key) {
    const depsMap = tong.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
       effectsToRun.add(effectFn)
      }
    })
    effectsToRun.forEach(effectFn => {
      if (effectFn.options.scheduler) {
       effectFn.options.scheduler(effectFn)
      } else {
        effectFn()
      }
    })

  }

  //修改的effect函数

  function effect(fn, options = {}) {

  const effectFn = () => {
    //清除
    cleanup(effectFn)
    //当 effectFn 执行时,将其设置为激活当前的作用函数
    activeEffect = effectFn
    //调用之前作用函数压入栈
    effectStack.push(effectFn)
    //将fn的执行结果存储到res中
    const res = fn()

    //执行完毕之后出栈
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    return res
  }

  effectFn.options = options
  effectFn.deps = []

    //只有非lazy的时候才执行
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
  
}
const effectFn = effect(() => {
  obj.foo = obj + 7
  console.log(obj.foo)
},
{
  lazy: true
}
)

调用返回结果,lazy成功

计算属性

定义一个computed函数,getter函数作为参数,把getter作为副作用函数创建一个lazy的effect,设置dirty和value,dirty是一个标识判断是否重新计算,value用来缓存上一次计算的值

//计算属性部分
function computed(getter) {
  // 缓存上一次计算的值
  let value 

  // dirty标志,判断是否需要重新计算值,true为“脏”,需要重新计算
  let dirty = true

  // 把getter作为作用函数创建一个lazy的effect
  const effectFn = effect(getter, {
    lazy:true,
    //添加调度器,在调度器中将dirty重置为true
    scheduler() {
      if (!dirty) {
        dirty = true
        trigger(obj, 'value')
      }
      
    }
  })
  const obj = {
    // 当读取value时才执行 effectFn
    get value() {
      if (dirty) {
        value = effectFn()
        //将dirty设置为false,下一次访问直接使用缓存到value中的值
        dirty = false
      }
      track(obj, 'value')
      return value
    }
  }
  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)

effect(function effectFn() {
  console.log(sumRes.value)
})

8、watch实现原理

watch的本质是利用effect和option.scheduler调度器选项

//watch 接受两个参数,source是响应式数据,callback是回调函数
function watch(source, callback) {
  effect(
    // 触发读取操作,建立联系
    // 调用traverse 递归读取
    () => traverse(source),
    {
      scheduler() {
        // 当数据变化时,调用回调函数
        callback()
      }
    }
  )
}

function traverse(value, seen = new Set()) {
  // 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
  if (typeof value !== 'object' || value == null || seen.has(value)) {
    return
  }
  // 将数据添加到seen中,代表遍历的读取过,避免循环引用引起死循环
  seen.add(value)
  //暂时不考虑其他结构
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

watch内部的effect调用traverse函数,traverse函数内部遍历,可以读取对象上的任意属性。

vue中的watch不仅可以观测响应式数据,也可以接收getter函数,还可以获取新值(newValue)和旧值(oldValue)。

getter改良

首先要判断source的类型,判断是否是函数,如果是函数则说明用户直接传递了getter函数,直接使用用户的getter函数,否则仍然执行traverse函数进行遍历操作,具体代码如下:

//getter改良版本
// watch 接受两个参数,source是响应式数据,callback是回调函数
function watch(source, callback) {
  // 定义getter
  let getter 
  // 如果是函数则说明传的是getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  effect(
    // 触发读取操作,建立联系
    // 调用traverse 递归读取
    () => getter(),
    {
      scheduler() {
        // 当数据变化时,调用回调函数
        callback()
      }
    }
  )
}

旧值oldValue与新值newValue改良

通过lazy懒执行effect,手动调用effectFn函数的返回值就是oldValue,当值发生变化触发scheduler调用effectFn函数得到newValue,将oldValue和newValue当做参数传给回调函数,最后,把新值赋值给旧值newValue = oldValue。具体代码如下:

//旧值oldValue与新值newValue改良
// watch 接受两个参数,source是响应式数据,callback是回调函数
function watch(source, callback) {
  // 定义getter
  let getter 
  // 如果是函数则说明传的是getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue
  // 使用effect注册作用函数时开启lazy选项,并把返回值存储到effect中以便手动调用

  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler() {
        // scheduler执行的作用函数,拿到的是newValue
        newValue = effectFn()
        // 传给回调函数
        callback(newValue, oldValue)
        //更新旧值,否则下一次会得到错误的旧值
        oldValue = newValue
      }
    }
  )

  // 手动调用作用函数,拿到的就是旧值
  oldValue = effectFn()
}

9、watch的立即执行和回调的执行时机

immediate实现原理就是把scheduler抽出来,在初始化执行一次,在值产生变化后再执行。

//immediate
// watch 接受两个参数,source是响应式数据,callback是回调函数
function watch(source, callback) {
  // 定义getter
  let getter 
  // 如果是函数则说明传的是getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue
  // 使用effect注册作用函数时开启lazy选项,并把返回值存储到effect中以便手动调用

  // 提取 scheduler 调度函数为一个独立的 job 函数
  const job = () => {
    newValue = effectFn()
    callback(newValue, oldValue)
    oldValue = newValue
  }

  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler: job
    }
  )

  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job,从而触发回调执行
    job()
  } else {
    // 手动调用作用函数,拿到的就是旧值
    oldValue = effectFn()
  }

}

Vue3中watch可以通过flush指定调用时机

flush本质上是在指定调度函数scheduler的调用时机,

//flush
// watch 接受两个参数,source是响应式数据,callback是回调函数
function watch(source, callback, options = {}) {
  // 定义getter
  let getter 
  // 如果是函数则说明传的是getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue
  // 使用effect注册作用函数时开启lazy选项,并把返回值存储到effect中以便手动调用

  // 提取 scheduler 调度函数为一个独立的 job 函数
  const job = () => {
    newValue = effectFn()
    callback(newValue, oldValue)
    oldValue = newValue
  }

  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        // 判断flush是否为;'post',如果是放到微任务队列中执行
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )

  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job,从而触发回调执行
    job()
  } else {
    // 手动调用作用函数,拿到的就是旧值
    oldValue = effectFn()
  }

}

10、过期的副作用

竟态问题经常出现在多线程或者多进程中,前端中遇到的场景可能是这样的:

修改obj 发送请求A ,再次修改obj 发送请求B ,请求A成功返回结果,赋值给finalData , 请求B成功返回结果,赋值给finalData。这是正常的流程,但当前的响应式系统执行的顺序,并不一定是这个流程,很有可能会出现B请求成功结束率先返回,之后A请求返回,最后finalData可能不是我们需要的B而是A。一幅图来解释:

Vue中解决这个问题通过的是watch函数回调的第三个参数onInvalidate,onInvalidate的原理是:在watch内部每次监测到改变后,在副作用函数重新执行之前,会先调用onInvalidate注册的过期回调。代码如下:

//处理过期副作用
// watch 接受两个参数,source是响应式数据,callback是回调函数
function watch(source, callback, options = {}) {
  // 定义getter
  let getter 
  // 如果是函数则说明传的是getter,直接把source赋值给getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue
  // cleanup用来存储用户过期的回调
  let cleanup

  // 定义onInvalidate函数
  function onInvalidate(fn) {
    // 将过期回调存储到 cleanup 中
    cleanup = fn
  }

  // 提取 scheduler 调度函数为一个独立的 job 函数
  const job = () => {
    newValue = effectFn()

    // 在调用回调函数callback之前,先调用过期回调
    if (cleanup) {
      cleanup()
    }
    // 将 onInvalidate 作为回调函数的第三个参数,方便用户使用
    callback(newValue, oldValue, onInvalidate)
    oldValue = newValue
  }


  // 使用effect注册作用函数时开启lazy选项,并把返回值存储到effect中以便手动调用
  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        // 判断flush是否为;'post',如果是放到微任务队列中执行
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )

  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job,从而触发回调执行
    job()
  } else {
    // 手动调用作用函数,拿到的就是旧值
    oldValue = effectFn()
  }

}

总结

本章学习了:

1、副作用函数和响应式数据的概念

响应式数据最本质就是对读取和设置的拦截,在读取时把副作用函数放入“桶”中,在设置时从“桶”中取出副作用函数并执行

2、调整“桶”的结构,Map改为WeakMap

3、分支切换和冗余副作用,避免不必要的更新,在重新执行副作用函数之前,清除之前建立的联系

4、嵌套副作用函数,避免嵌套副作用函数出现问题,通过effect栈解决

5、响应式的可调度性,trigger再次执行副作用函数之前,有能力决定副作用函数的执行时机

6、lazy和计算属性,lazy通过scheduler实现,计算属性通过懒执行副作用函数实现

7、watch的实现原理,通过调度器scheduler,处理watch中注册的回调函数

8、过期的副作用函数,会引起竟态问题,onInvalidate注册过期的回调,在watch执行回调之前先执行注册过期的回调。