震惊,react 不用 fiber 链表也能实现可中断渲染?

930 阅读1分钟

在 fiber 架构之前,react 是使用树形的 vdom 来描述应用的,树形结构可以很简单的描述一个 ui 界面,比如

{
  type: 'div',
  props: {
    class: 'div'
  },
  children: [
    {
      type: 'p',
      children: ['hello']
    },
    {
      type: 'p',
      children: ['world']
    }
  ]
}

我们可以写一个渲染函数将 vdom 渲染出来

function renderVDOM(vdom, $container) {
  // 文本节点
  if (typeof vdom === 'string') {
    const $dom = document.createTextNode(vdom)
    $container.appendChild($dom)
    return
  }

  const { type, props = {}, children = [] } = vdom
  const $dom = document.createElement(type)
  Object.keys(props).forEach(key => $dom.setAttribute(key, props[key]))
  children.forEach(childVdom => renderVDOM(childVdom, $dom))
  $container.appendChild($dom)
}

树形结构的渲染我们很容易就想到递归(stack reconciler),但是这种递归的渲染方式是不可中断的,一旦执行渲染函数就会占用 js 线程直到渲染结束,当页面的 ui 极其复杂,例如存在大量表单时这样的渲染方式很容易造成页面卡顿,因此 react 希望渲染是可以分片的,每次只执行一小段时间就让出 js 线程留给页面去做交互,然后再去接着执行渲染。那传统的树形结构是不是就不能实现可中断的渲染了呢,我们不妨试一下

function renderVDOM_async(rootVdom, $container) {
  const INTERVAL = 5 // 每次只执行5ms
  const DOMKEY = Symbol('DOMKEY') // 将真实dom暂存在vdom上面
  let vdomStack = [[[rootVdom], 0]] // 执行栈

  function pop() {
    while (vdomStack.length) {
      const [pendVdomList, curIdx] = vdomStack[vdomStack.length - 1]
      if (curIdx === pendVdomList.length - 1) {
        vdomStack.pop()
      } else {
        vdomStack[vdomStack.length - 1][1] = vdomStack[vdomStack.length - 1][1] + 1
        break
      }
    }
  }

  function mount(vdom) {
    (vdom.children || []).forEach(childVdom => {
      if (typeof childVdom === 'string') {
        vdom[DOMKEY].appendChild(document.createTextNode(childVdom))
      } else {
        vdom[DOMKEY].appendChild(childVdom[DOMKEY])
        mount(childVdom)
      }
    })
  }

  function render() {
    const start = performance.now()
    // 只执行5ms
    while (performance.now() - start < INTERVAL) {
      // 所有的vdom已经遍历完毕生成了真实dom 统一挂载
      // 挂载是不可中断的
      if (!vdomStack.length) {
        mount(rootVdom)
        $container.appendChild(rootVdom[DOMKEY])
        return
      }

      // 当前执行的是哪一层vdom 以及此层级的第几个
      const [pendVdomList, curIdx] = vdomStack[vdomStack.length - 1]
      const vdom = pendVdomList[curIdx]

      // 文本节点
      let $dom
      if (typeof vdom === 'string') {
        pop()
      } else {
        const { type, props = {}, children = [] } = vDom
        $dom = document.createElement(type)
        Object.keys(props).forEach(key => $dom.setAttribute(key, props[key]))

        if (children.length) {
          vdomStack.push([children, 0])
        } else {
          // 说明此层vdom已经遍历完毕 可以出栈了
          if (curIdx === pendVdomList.length - 1) {
            pop()
          // 此层级没有执行完毕 移至下一位
          } else {
            vdomStack[vdomStack.length - 1][1] = curIdx + 1
          }
        }
      }
      vdom[DOMKEY] = $dom
    }

    // 让出主线程
    setTimeout(() => {
      render()
    }, 0)
  }

  render()
}

上面尝试实现了一个可中断的树形结构渲染,做法是将递归的写法转为迭代。可以看到,由于树形的 vdom 结构没有子节点到父节点的指向,以及子节点到同级子节点的指向,要想实现中断必须手动的去维护一个执行栈,用进出栈来模拟父子节点指向,同时还需要维护同级节点的渲染位置变量,用来实现同级子节点渲染的移位。

总而言之是比较繁琐,由于数据结构的缺陷需要做许多额外的工作,react 最终选择使用链表是这个原因吗,如果你知道的话欢迎讨论。