颗粒度更新

174 阅读2分钟

什么是颗粒度更新

如果你使用过vue和react就会发现一个很明显的区别,即react的许多hook使用需要我们显示的指定一个数组参数,例如useEffect或useMemo的第二个参数

useMemo(() => a + b, [a, b])

而Vue中类似computed就不需要,原因是Vue能"自动追踪响应式依赖"。我们把这种能自动追踪依赖的技术称为颗粒度更新

实现一个简单的颗粒度更新

首先我们要知道我们要实现的最终目的,如下例子:

// useState返回值[0]是一个getter 可以方便我们后续的依赖收集操作
function useState(value) {
  const getter = () => value
  const setter = (nextValue) => value = nextValue

  return [getter, setter]
}

const [count, setCount] = useState(0)
useEffect(() => {
  // 打印count()值
  console.log(count())
})

useEffect(() => {
  console.log('无关effect')
})
setCount(2)// 这里我们希望能再次打印count()值 即执行count()相关的useEffect回调

实现的关键在于useEffect执行参数函数时如果执行了对应state的getter,我们要想办法收集到该执行参数函数,然后在state的setter调用改变value时再次执行前面收集到的函数即可,下面是具体实现和测试

const effectStack = []

function cleanup(effect) {
  // 清除所有被收集的effect
  effect.deps.forEach(subs => {
    subs.delete(effect)
  })
  // 清除deps
  effect.deps.clear()
}

function useEffect(cb) {
  const execute = () => {
    // execute每次执行重置依赖 类似Vue中的分支处理 避免多余effect收集 下面有相关文章链接
    cleanup(effect)
    // 将effec推入栈顶
    effectStack.push(effect)
    try {
      cb()
    } finally {
      // 出栈
      effectStack.pop()
    }
  }
  const effect = {
    execute,
    deps: new Set()
  }
  // 立即执行一次
  execute()
}

function useState(value) {
  // 保存订阅该state的effects
  const subs = new Set()

  const getter = () => {
    // 收集当前effect
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
      // 建立订阅关系
      subscribe(effect, subs)
    }
    return value
  }

  const setter = (nextValue) => {
    value = nextValue
      // 通知所有订阅该state的effect执行
      // 注意此处不能使用subs原集合 因为执行execute会修改subs 导致进入死循环
      ;[...subs].forEach(effect => [
      effect.execute()
    ])
  }

  return [getter, setter]
}

function subscribe(effect, subs) {
  // 订阅关系建立
  subs.add(effect)
  // 依赖关系建立
  effect.deps.add(subs)
}

const [count, setCount] = useState(0)
const [nm, setNm] = useState('lisi')

useEffect(() => {
  console.log(count()) // 0
})

useEffect(() => {
  console.log(nm()) // lisi
})

useEffect(() => {
  console.log('11111')
})
setTimeout(() => {
  setCount(2)  // 2
  setNm('zhangsan') // zhangsan
}, 2000);

上述关于Vue中的分支处理 避免多余effect收集的内容可以参考:juejin.cn/post/709566…