前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3响应系统设计-下

Vue3响应系统设计-下

作者头像
韦东锏
发布2023-08-26 14:56:13
1920
发布2023-08-26 14:56:13
举报
文章被收录于专栏:Android码农

继续分析响应式的设计,一步步深入,会比上一篇难理解些

无限递归的坑

一个完善的响应式系统,需要考虑诸多细节,比如下面这个无限递归的例子

代码语言:javascript
复制
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })
effect(() => obj.foo++)

上面的这个代码会导致栈溢出 Uncaught RangeError: Maximum call stack size exceeded

实际上,把 obj.foo++ 这个自增操作分开来看,它相当于

代码语言:javascript
复制
effect(() => {
    // 语句
    obj.foo = obj.foo + 1
  })

问题出现场景是这样的:首先读取 obj.foo 的值,触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。由于该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。导致无限递归地调用自己,于是就产生了栈溢出。

这个问题要如何解决呢?

关键是副作用函数执行的时候,要避免trigger再次触发执行;这里可以发现,track跟trigger触发的都是同个activeEffect,那就可以基于此增加判断条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行

代码语言:javascript
复制
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
    if (effectFn !== activeEffect) {  // 新增
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => effectFn())
  // effects && effects.forEach(effectFn => effectFn())
}

这样就可以修复问题了

effect函数的可调度性

可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。先来看下下面的代码

代码语言:javascript
复制
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => {
  console.log(obj.foo)
})
obj.foo++
console.log('结束了')

这段代码的输出结果如下

代码语言:javascript
复制
1
2
'结束了'

现在假设需求有变,输出顺序需要调整为:

代码语言:javascript
复制
1
'结束了'
2

可以把语句 obj.foo++ 和语句 console.log('结束了') 位置互换。有什么办法在不调整代码的情况下实现需求呢? 这时就需要响应系统支持调度

可以为 effect 函数设计一个选项参数 options,允许用户指定调度器

代码语言:javascript
复制
effect(
  () => {
    console.log(obj.foo)
  },
  // options
  {
    // 调度器 scheduler 是一个函数
    scheduler(fn) {
      // ...
    }
  }
)

effect函数新增一个options参数,同时把options挂载在副作用函数上

代码语言:javascript
复制
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 将 options 挂载到 effectFn 上
  effectFn.options = options  // 新增
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

有了调度函数,在trigger函数中触发副作用函数执行时,可以直接调用调度器函数,从而把控制权外放

代码语言:javascript
复制
function trigger(target, key) {
  const depsMap = bucket.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()  // 新增
    }
  })
}

在trigger触发执行的时候,优先判断有没有调度器,如果存在,直接调用调度器函数,没有的话,直接执行副作用函数;

有了调度器,就可以实现前面的需求了

代码语言:javascript
复制
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(
  () => {
    console.log(obj.foo)
  },
  // options
  {
    // 调度器 scheduler 是一个函数
    scheduler(fn) {
      // 将副作用函数放到宏任务队列中执行
      setTimeout(fn)
    }
  }
)
obj.foo++
console.log('结束了')

用setTimeout 开启一个宏任务来执行副作用函数 fn,这样就能实现期望的打印顺序了

另外调度器还可以控制它的执行次数,这个很重要,考虑下面例子

代码语言:javascript
复制
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => {
  console.log(obj.foo)
})
obj.foo++
obj.foo++

它的输出如下

代码语言:javascript
复制
1
2
3

如果只关系最终结果,不关心过程,期望的打印结果是

代码语言:javascript
复制
1
3

也可以基于调度器来实现

代码语言:javascript
复制
// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务
const p = Promise.resolve()
// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return
  // 设置为 true,代表正在刷新
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    // 结束后重置 isFlushing
    isFlushing = false
  })
}
effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    // 每次调度时,将副作用函数添加到 jobQueue 队列中
    jobQueue.add(fn)
    // 调用 flushJob 刷新队列
    flushJob()
  }
})
obj.foo++
obj.foo++

任务队列jobQueue是一个Set数组,可以自动去重,在执行的时候,isFlushing来判断一个周期只执行一次,最终的执行是用p.then将函数添加到微任务队列,在队列内完成遍历执行

vue在连续多次修改响应数据,当只会触发一次更新,思路跟这个是相同的

computed 与 lazy

现在设计实现的effect函数,都会立即执行传递给它的副作用函数,例如

代码语言:javascript
复制
effect(
  // 这个函数会立即执行
  () => {
    console.log(obj.foo)
  }
)

有些场景,我们不希望立即执行,而是在需要的时候才执行,可以在options中添加lazy属性来达到目的

代码语言:javascript
复制
effect(
  // 指定了 lazy 选项,这个函数不会立即执行
  () => {
    console.log(obj.foo)
  },
  // options
  {
    lazy: true
  }
)

相应的effect函数也要调整,当options.lazy为true时,不立即执行副作用函数,并且把副作用函数返回,由外部手动执行

代码语言:javascript
复制
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.options = options
  effectFn.deps = []
  // 只有非 lazy 的时候,才执行
  if (!options.lazy) {  // 新增
    // 执行副作用函数
    effectFn()
  }
  // 将副作用函数作为返回值返回
  return effectFn  // 新增
}

然后就可以手动执行副作用函数了

代码语言:javascript
复制
const effectFn = effect(() => {
  console.log(obj.foo)
}, { lazy: true })
// 手动执行副作用函数
effectFn()

单纯手动执行其实意义不大,不过如果把副作用函数看做一个getter,例如

代码语言:javascript
复制
 const effectFn = effect(
   // getter 返回 obj.foo 与 obj.bar 的和
   () => obj.foo + obj.bar,
   { lazy: true }
 )

在手动执行副作用函数时,可以拿到返回值

代码语言:javascript
复制
const effectFn = effect(
  // getter 返回 obj.foo 与 obj.bar 的和
  () => obj.foo + obj.bar,
  { lazy: true }
)
// value 是 getter 的返回值
const value = effectFn()

由于新增了返回值,需要再对effect函数做一些修改

代码语言:javascript
复制
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储到 res 中
    const res = fn()  // 新增
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res  // 新增
  }
  effectFn.options = options
  effectFn.deps = []
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

effectFn把真正的副作用函数 fn 的执行结果,保存到 res 变量中,然后将其作为 effectFn 函数的返回值,这样接下来就可以实现计算属性computed了

代码语言:javascript
复制
function computed(getter) {
  // 把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    // 当读取 value 时才执行 effectFn
    get value() {
      return effectFn()
    }
  }
  return obj
}

computed 函数返回一个对象,该对象的 value 属性是一个访问器属性,只有当读取 value 的值时,才会执行 effectFn 并将其结果作为返回值返回

我们可以使用 computed 函数来创建一个计算属性

代码语言:javascript
复制
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)  // 3

现在computed函数已经可以实现懒计算了,只有当真正读取sumRes.value 的值时,它才会进行计算并得到值,但是多次访问sumRes.value 的值,会导致effectFn的多次计算,所以computed方法还要继续优化,增加缓存功能

代码语言:javascript
复制
function computed(getter) {
  // value 用来缓存上一次计算的值
  let value
  // dirty 标志,用来标识是否需要重新计算值,为 true 则意味着“脏”,需要计算
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true
  })
  const obj = {
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为 false,下一次访问直接使用缓存到 value 中的值
        dirty = false
      }
      return value
    }
  }
  return obj
}

用value来缓存上一次计算的值,用dirty来表示是否需要重新计算,这样每次访问sumRes.value,拿到的都是缓存的值了

不过有个明显的漏洞,当修改obj.foo 或 obj.bar 的值的时候, sumRes.value 返回的值不会变,所以当obj.foo的值改变的时候,需要把dirty改成true,这个要如何实现呢?

可以利用之前结算的调度器来实现,代码如下

代码语言:javascript
复制
function computed(getter) {
  let value
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true,
    // 添加调度器,在调度器中将 dirty 重置为 true
    scheduler() {
      dirty = true
    }
  })
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      return value
    }
  }
  return obj
}

由于effect添加了scheduler调度器,在每次getter说依赖的响应式数据变化的时候,可以把dirty置为true,这样就可以得到预期的结果了

上面的设计趋于完美了,不过还有一个缺陷,当在另外一个effect读取计算属性,当修改obj.foo的值,不会触发副作用函数的重新执行

代码语言:javascript
复制
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
  // 在该副作用函数中读取 sumRes.value
  console.log(sumRes.value)
})
// 修改 obj.foo 的值
obj.foo++

对于计算属性的 getter 函数来说,它里面访问的响应式数据只会把computed 内部的 effect 收集为依赖。外层的 effect 不会被内层 effect 中的响应式数据收集,这个怎么办?

其实方法不难,就是在读取计算属性的值时,手动调用 track 函数进行追踪;当响应式数据发生变化时,再手动调用 trigger 函数触发响应

代码语言:javascript
复制
function computed(getter) {
  let value
  let dirty = true
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      if (!dirty) {
        dirty = true
        // 当计算属性依赖的响应式数据变化时,手动调用 trigger 函数触发响应
        trigger(obj, 'value')
      }
    }
  })
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      // 当读取 value 时,手动调用 track 函数进行追踪
      track(obj, 'value')
      return value
    }
  }
  return obj
}

我们把计算属性返回的对象obj作为track函数的第一个参数,这样也就跟外层的effect函数建立关系了,在数据发生变化后,再手动调用trigger触发响应

watch的实现原理

watch其实就是一个响应式数据,数据一旦变化,有相应的回调

代码语言:javascript
复制
watch(obj, () => {
  console.log('数据变了')
})
// 修改响应数据的值,会导致回调函数执行
obj.foo++

上面的watch方法,可以利用effect方法配合scheduler选项来实现

代码语言:javascript
复制
effect(() => {
  console.log(obj.foo)
}, {
  scheduler() {
    // 当 obj.foo 的值变化时,会执行 scheduler 调度函数
  }
})

在副作用函数中,访问响应式数据,就把函数跟响应式数据建立联系,再加上scheduler的回调,可以有最简单的watch函数的实现方式

代码语言:javascript
复制
// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
function watch(source, cb) {
  effect(
    // 触发读取操作,从而建立联系
    () => source.foo,
    {
      scheduler() {
        // 当数据变化时,调用回调函数 cb
        cb()
      }
    }
  )
}

上面函数可以正常工作,不过有个坑,硬编码了对source.foo的读取,如果非foo属性是无法触发watch的回调了,这里要怎么办?

其实很简单,遍历响应式数据的所有变量,让它跟副作用函数简历关联

代码语言:javascript
复制
function watch(source, cb) {
  effect(
    // 调用 traverse 递归地读取
    () => traverse(source),
    {
      scheduler() {
        // 当数据变化时,调用回调函数 cb
        cb()
      }
    }
  )
}
function traverse(value, seen = new Set()) {
  // 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  // 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
  seen.add(value)
  // 暂时不考虑数组等其他结构
  // 假设 value 就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

traverse会递归的读取所有的变量,从而实现任意属性都可以触发回调执行了,另外,扩大下watch的功能,既可以监听响应式对象,也可以是一个getter函数

代码语言:javascript
复制
watch(
  // getter 函数
  () => obj.foo,
  // 回调函数
  () => {
    console.log('obj.foo 的值变了')
  }
)

那watch方法要如何处理getter的场景呢?其实很简单,把getter当做特殊的traverse函数即可

代码语言:javascript
复制
function watch(source, cb) {
  // 定义 getter
  let getter
  // 如果 source 是函数,说明用户传递的是 getter,所以直接把 source 赋值给 getter
  if (typeof source === 'function') {
    getter = source
  } else {
    // 否则按照原来的实现调用 traverse 递归地读取
    getter = () => traverse(source)
  }
  effect(
    // 执行 getter
    () => getter(),
    {
      scheduler() {
        cb()
      }
    }
  )
}

通过判断source的类型,如果是getter类型,就在effect中直接执行,建立响应式联系;

另外,watch方法,还缺少旧值与新值的回调,这样要怎么处理?

利用effect函数的lazy选项,可以快速的实现

代码语言:javascript
复制
function watch(source, cb) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  // 定义旧值与新值
  let oldValue, newValue
  // 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到 effectFn 中以便后续手动调用
  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler() {
        // 在 scheduler 中重新执行副作用函数,得到的是新值
        newValue = effectFn()
        // 将旧值和新值作为回调函数的参数
        cb(newValue, oldValue)
        // 更新旧值,不然下一次会得到错误的旧值
        oldValue = newValue
      }
    }
  )
  // 手动调用副作用函数,拿到的值就是旧值
  oldValue = effectFn()
}

由于是lazy加载,所以先主动调用下effectFn,拿到旧值,然后在每次的scheduler回调中,再去更新新值

watch的立即执行和执行时机

watch正常是数据变成了,才会触发回调执行,不过有个immediate参数,来指定回调立即执行,这个要怎么实现?

代码语言:javascript
复制
watch(obj, () => {
  console.log('变化了')
}, {
  // 回调函数会在 watch 创建时立即执行一次
  immediate: true
})

回调的执行是在scheduler函数中的,把scheduler函数封装下,再手动执行一次就可以了

代码语言:javascript
复制
function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  // 提取 scheduler 调度函数为一个独立的 job 函数
  const job = () => {
    newValue = effectFn()
    cb(newValue, oldValue)
    oldValue = newValue
  }
  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      // 使用 job 函数作为调度器函数
      scheduler: job
    }
  )
  if (options.immediate) {
    // 当 immediate 为 true 时立即执行 job,从而触发回调执行
    job()
  } else {
    oldValue = effectFn()
  }
}

另外,watch还有一个flush参数,可以指定回调在在一个微队列中,等待DOM更新结束后执行

代码语言:javascript
复制
watch(obj, () => {
  console.log('变化了')
}, {
  flush: 'post' // 还可以指定为 'pre' | 'sync'
})

其实现如下

代码语言:javascript
复制
function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  const job = () => {
    newValue = effectFn()
    cb(newValue, oldValue)
    oldValue = newValue
  }
  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        // 在调度函数中判断 flush 是否为 'post',如果是,将其放到微
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

指定为post的时候,job会被放在微队列中执行,实现了异步延迟执行

过期的副作用

我们考虑一个竞态问题的场景

代码语言:javascript
复制
let finalData
watch(obj, async () => {
  // 发送并等待网络请求
  const res = await fetch('/path/to/request')
  // 将请求结果赋值给 data
  finalData = res
})

上面这段代码,可能会存在问题;

我们第一次修改 obj 对象的某个字段值,这会导致回调函数执行,同时发送了第一次请求 A。随着时间的推移,在请求 A 的结果返回之前,我们对 obj 对象的某个字段值进行了第二次修改,这会导致发送第二次请求 B。此时请求 A 和请求 B 都在进行中,如果请求B 先于请求 A 返回结果,就会导致最终 finalData 中存储的是 A 请求的结果,这样是不符合常规场景的

但由于请求 B 是后发送的,因此我们认为请求 B 返回的数据才是“最新”的,而请求 A 则应该被视为“过期”的,所以我们希望变量 finalData 存储的值应该是由请求B 返回的结果,而非请求 A 返回的结果

这个问题要如何处理呢?其实可以在callback增加第三个参数onInvalidate,它是一个函数,利用这个回调可以处理过期的副作用函数

代码语言:javascript
复制
watch(obj, async (newValue, oldValue, onInvalidate) => {
  // 定义一个标志,代表当前副作用函数是否过期,默认为 false,代表没有过期
  let expired = false
  // 调用 onInvalidate() 函数注册一个过期回调
  onInvalidate(() => {
    // 当过期时,将 expired 设置为 true
    expired = true
  })
  // 发送网络请求
  const res = await fetch('/path/to/request')
  // 只有当该副作用函数的执行没有过期时,才会执行后续操作。
  if (!expired) {
    finalData = res
  }
})

那么onInvalidate是如何实现的呢?

其实很简单,在 watch 内部每次检测到变更后,在副作用函数重新执行之前,会先调用我们通过 onInvalidate 函数注册的过期回调,仅此而已

代码语言:javascript
复制
function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  // cleanup 用来存储用户注册的过期回调
  let cleanup
  // 定义 onInvalidate 函数
  function onInvalidate(fn) {
    // 将过期回调存储到 cleanup 中
    cleanup = fn
  }
  const job = () => {
    newValue = effectFn()
    // 在调用回调函数 cb 之前,先调用过期回调
    if (cleanup) {
      cleanup()
    }
    // 将 onInvalidate 作为回调函数的第三个参数,以便用户使用
    cb(newValue, oldValue, onInvalidate)
    oldValue = newValue
  }
  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

定义了 cleanup 变量,用来存储用户通过onInvalidate 函数注册的过期回调。在 job 函数内,每次执行回调函数 cb 之前,先检查是否存在过期回调,如果存在,则执行过期回调函数cleanup

代码语言:javascript
复制
watch(obj, async (newValue, oldValue, onInvalidate) => {
  let expired = false
  onInvalidate(() => {
    expired = true
  })
  const res = await fetch('/path/to/request')
  if (!expired) {
    finalData = res
  }
})
// 第一次修改
obj.foo++
setTimeout(() => {
  // 200ms 后做第二次修改
  obj.foo++
}, 200)

在 200ms 时第二次修改了 obj.foo 的值,会导致 watch 的回调函数再次执行。 watch 的回调函数第二次执行之前,会优先执行之前注册的过期回调,这会使得第一次执行的副作用函数内闭包的变量 expired 的值变为 true,即副作用函数的执行过期了。于是等请求 A 的结果返回时,其结果会被抛弃,从而避免了过期的副作用函数带来的影响

结束语

这个系列,主要是参考Vue.js设计与实现这个本书。响应式数据是Vue的核心概念,整体了解下来,确实有一定的难度,部分逻辑也是看了多遍才理解透,需要考虑很多的实际场景,看完后由衷的佩服框架的设计者,这是多么天才型的人才才可以如何优雅的处理好这一个个场景问题

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-06-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android码农 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 无限递归的坑
  • effect函数的可调度性
  • computed 与 lazy
  • watch的实现原理
  • watch的立即执行和执行时机
  • 过期的副作用
  • 结束语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档