实现React细粒度更新

2,057 阅读5分钟

前言

现代前端框架的实现原理几乎都可以利用 UI = f (state) 来进行概括,即框架内部运行机制根据当前状态渲染视图。

  • state 代表当前视图状态
  • f 代表框架内部运行机制
  • UI 代表宿主环境的视图

对 f 进一步解释 ,其实他的工作原理分为两步

  1. 根据 state 变化计算出 ui 变化
  2. 根据 ui 变化执行具体宿主环境 API 比如在浏览器环境中,ui的增删改查是通过 DOM API 来进行实现。

所以,不同框架的差异主要体现在根据 state 变化计算出 ui 变化的实现上。

react 每次更新的流程都是从应用的根节点开始,遍历整个应用,而对比其他框架,Vue3的更新流程开始于组建。Svelte的更新流程开始于元素。因为React不需要确定哪个变量发生了变化,任何变量的变化都会开启一次遍历应用的更新流程,因此 React 不需要 "细粒度更新"。

本文完。哈哈哈开个玩笑,下面我们就来一起看下,什么是细粒度更新。

介绍

能够自动追踪依赖的技术就被称为细粒度更新

比如我们在React中都需要显式的指明依赖的变量,而在Vue / Mobx 中并不需要显式指明参数。

// React
const y = useMemo(() => x + 1 , [x])

// Vue
const y = computed(() => x.value + 1);

// Mobx
const y = computed(() => x.data + 1);

下面我们用不到100行代码来实现一个简单的细粒度更新的demo ,上才艺

首先实现一个 useState, 来定义自变量

const useState = (value) => {
  const getter = () => value;
  const setter = (newValue) => value = newValue;
  return [getter, setter];  // 注意:我们这里 返回的是 getter 而不是value
}

下面来实现一个usEffect,我们期望的行为是

  1. useEffect 执行后,回调函数立即执行
  2. 依赖的自变量变化后,回调函数立即执行
  3. 不需要我们显示指明依赖

比如

const [count, setCount] = useState(0)

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

useEffect(() => {
  console.log('哈哈哈')
})

setCount(2)

期望打印顺序 先打印 0,然后打印 哈哈哈,然后count 改变,第一个effect内部依赖count, 然后打印 2

这里关键在于我们要建立起 useState 和 useEffect 的关系我们建立一个发布订阅关系

  1. 当 useEffect 回调中执行 useState 的 getter 的时候,就让这个effect 订阅 该state 的变化

  2. 当 useStare 的 setter 执行的时候,就向订阅了他的 effect 发布通知

在 state 内部创建一个集合 subs,用来保存 订阅他变化的 effect, 将 effect 设置一个数据结构

const effect = {
  execute, // 执行 useEffect 的回调函数
  deps: new Set() // 保存该 useEffect 依赖的 state.subs
}

这样的话, 就可以通过遍历 state 的 subs 来找到所有订阅该 state 变化的 effect, 然后通过 effect 的 deps 找到所有 该 effect 依赖的 state.subs

来画个图描述一下

完整的effect 如下

const useEffect = (callback) => {
  const execute = () => {
    cleanup(effect);  // 重置订阅发布依赖 
    effectStack.push(effect) // 将当前 effect 推入栈顶
    try {
      callback() // 执行回调
    } finally {
      effectStack.pop() // effect 出栈
    }
  }
	const effect = {
    execute,
    deps: new Set()
  }
  execute(); // 立即执行一次建立关系
}

在 callback 执行前调用 cleanup 来 清除所有 与该 effect 相关的订阅发布关系,具体原因例子我们在下文解释, callback执行时会重建订阅发布关系。这为 细粒度更新 带来 自动依赖追踪能力,

function cleanup(effect) {

  // 从该 effect 订阅的所有 state 对应 subs 中移除该effect
  for (const subs of effect.deps) {
    	subs.delete(effect)
  }
  // 将该effect 依赖所有 state 对应 subs 移除
  effect.deps.clear()
  
}

在调用 state 的 getter 时候,需要知道这个 state 当前是哪个effect上下文,主要是用来建立 effect 和 state 的联系。 所以callback 执行的时候将 effect 推入effectStack 栈顶,执行后出栈。在useState的getter 内部就可以通过获取栈顶元素得到当前所处的 effect 的上下文

然后 useEffect 执行后内部执行execute, 首次建立订阅发布关系。这是自动收集依赖的关键

然后我们需要改造 useStaet,完成完整的逻辑

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

  const getter = () => {
    // 获取当前上下文的effect
    const effect = effectStack.at(-1);
    if (effect) {
      // 如果他处在上下文中,则需要建立订阅发布关系
      subscribe(effect, subs)
    }
    return value
  }

  const setter = (nextValue) => {
    value = nextValue;
    // 执行订阅该state变化的effect执行
    for (const effect of [...subs]) {
      effect.execute()
    }
  }

  return [getter, setter]
  
}

实现subscribe方法

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

上面实现了useState, useEffect 后,我们就可以在这个基础上实现useMemo

function useMemo(callback) {
  const [value, setValue] = useState()
  useEffect(() => setValue(callback())) // 首次执行callback, 建立回调中state的订阅发布关系
  return value
}

现在我们来看下,为什么每次在effect 的 execute 执行 都需要重置订阅发布关系,我们来看下面的例子, 比如

const [name1, setName1] = useState('小金')
const [name2, setName2] = useState('小王')
const [show, setShow] = useState(true)

const whoSmile = useMemo(() => {
  if (!show()) {
    return name1()
  }
  return `${name1()} 和 ${name2()}`
})

useEffect(() => console.log('谁在那哈哈哈', whoSmile()))

setName1('小李')

setShow(false)

setName2('小杨')

打印如下:

  1. 谁在那哈哈哈 小金 和 小王
  2. 谁在那哈哈哈 小李 和 小王
  3. 谁在那哈哈哈 小李
  4. 不打印信息

我们可以看到,当 setShow 为 false 都时候,whoSmile 中的 name2 并没有执行,因此name2 和 whoSmile 并不存在了关系,只有 show() 为 true 的时候,whoSmile 才会重新依赖 name1 和 name2

到这里,我们就已经实现了 ”细粒度更新“,相比 React Hooks 有两个优点

  1. 不需要显示指明依赖

  2. 可以自动跟踪依赖,所以 不受 hooks 不能在条件语句中声明的限制

那为啥 react 不用哩。正如上面说的,react 属于应用级框架,不太需要,哎, 就是玩~

完结,撒花~~ ~