Preact源码阅读(三)- 更新机制

1,237 阅读4分钟

在Preact源码阅读(二),我们看了初始化阶段的diff算法,preact采用了深度diff的算法,由根节点开始,深度遍历子节点,完成比较及DOM的创建、插入。本章准备分析setState的基本函数功能,并基于此,分析preact更新阶段的流程。

1. setState功能

setState是我们使用最频繁的函数,在React框架里,我们通过setState更新数据,从而触发UI的渲染。setState的api如下:updater可以为object或类似(state, props)的函数。

setState(updater, [callback]);

Preact里setState的实现如下:

  • 设置_nextState。_nextState不存在时,初始化_nextState=assign({}, this.state),nextState为浅拷贝this.state。
  • update为函数时,调用update(s, this.props),注此处state传递的为_nextState。
  • update存在时,浅拷贝到_nextState中;update不存在,过滤此次无效更新。
  • 当前VNOde存在时,将callback放到_renderCallbacks中,将此次更新放到enqueueRender队列中。
Component.prototype.setState = function(update, callback) {
  // _nextState不存在时,设置s=this._nextSat
  let s;
  if (this._nextState != null && this._nextState !== this.state) {
    s = this._nextState;
  } else {
    s = this._nextState = assign({}, this.state);
  }
  if (typeof update == 'function') {
    update = update(s, this.props);
  }
  if (update) {
    assign(s, update);
  }
  // Skip update if updater function returned null
  if (update == null) return;
  if (this._vnode) {
    if (callback) this._renderCallbacks.push(callback);
    enqueueRender(this);
  }
};

我们可以看到setState的功能主要是将更新的state放到_nextState里,并将更新放到渲染队列中。enqueueRender的功能主要是什么那,其主要负责渲染的队列管理,例如setState的同步合并、多级组件的渲染顺序,我们看一下enqueueRender的具体功能。

2. 更新机制

在我们写React代码时,我们经常会这样写,在这段代码里,我们用setState更新了多个state,Preact是如何在一次更新完成渲染的,这就是enqueueRender的功能了,我们首先看下Preact的enqueueRender的功能执行顺序。

this.setState({
    count: count + 1,
});
...
this.setState({
    pre: pre + 1,
});

Preact的函数调用如下图:

  • setState调用,将更新放到enqueueRender队列。
  • defer(process)的调用,nextTick执行渲染队列。
  • 调用renderComponent完成UI的更新。

2.1 enqueueRender

enqueueRender函数功能主要是更新队列的状态,只有当组件未更新(_diry=false)且待更新组件数目为0时,才触发组件的更新。Preact用_dirty标记组件的更新状态, _dirty=true比较当前组件处于更新中,这主要是将组件的多个更新合并成一次。使用process_rerenderCount标记当前待更新的组件数目,大于1时就不触发渲染。 Preact支持自定义更新方式,可以通过options.deounceRendering自定义更新的方式,例如我们可以定义options.debounceRendering = window.requestAnimationFrame的形式,定义队列的更新方式。

export function enqueueRender(c) {
  // 组件未更新(_dirty=false)且待更新组件为0
  if (
    (!c._dirty &&
      (c._dirty = true) &&
      rerenderQueue.push(c) &&
      !process._rerenderCount++) ||
    // 自定义更新方式,触发UI更新
    prevDebounce !== options.debounceRendering
  ) {
    // prevDebounce为自定义更新方式
    prevDebounce = options.debounceRendering;
    (prevDebounce || defer)(process);
  }
}

Preact提供的更新方式为defer,defer的定义如下, Promise存在时,defer为Promise().then()、不存在时为setTimeout。defer(process)意味着将在下一个Eventloop周期,执行process,触发diff、组件的更新。

const defer =
  typeof Promise == 'function'
    ? Promise.prototype.then.bind(Promise.resolve())
    : setTimeout;

2.2 Process

process的函数定义如下,其具体的功能如下:

  • 设置process._rerenderCount = rerenderQueue.length,标记当前渲染的数目。
  • rerenderQueue基于_depth升序排序,更新从子组件开始更新,由下向上的更新。
  • 队列循环处理,调用renderComponent,完成组件的更新。
function process() {
  let queue;
  while ((process._rerenderCount = rerenderQueue.length)) {
    queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth);
    rerenderQueue = [];
    queue.some(c => {
      if (c._dirty) renderComponent(c);
    });
  }
}
process._rerenderCount = 0;

process主要是执行队列的更新及重置,Preact将按照由下向上的方式执行组件的更新,从而完成一次周期的更新。

2.3 renderComponent

renderComponent主要负责单个组件的更新,其主要功能是调用diff完成节点的更新。

  • 调用diff完成节点的diff及dom的更新。parentDom存在时,调用diff完成节点的更新、dom的生成。
  • 调用commitRoot, 完成_renderCallback的调用,包括生命周期、setState callback。
  • 父节点DOM的指向变更。当newDom != oldDom时,例如if/else导致的节点变更时,我们需要更新dom的指向。
function renderComponent(component) {
  let vnode = component._vnode,
    oldDom = vnode._dom,
    parentDom = component._parentDom;
  if (parentDom) {
    let commitQueue = [];
    const oldVNode = assign({}, vnode);
    oldVNode._original = oldVNode;
    // 调用diff完成节点的更新及dom的生成
    let newDom = diff(
      parentDom,
      vnode,
      oldVNode,
      component._globalContext,
      parentDom.ownerSVGElement !== undefined,
      null,
      commitQueue,
      oldDom == null ? getDomSibling(vnode) : oldDom
    );
    // _renderCallbcks的调用
    commitRoot(commitQueue, vnode);
    // parentDom的指向变更
    if (newDom != oldDom) {
      updateParentDomPointers(vnode);
    }
  }
}

updateParentDomPointers的功能如下,将从当前diff的节点开始,向上修改_dom/_component.base的指向。

function updateParentDomPointers(vnode) {
  // 父节点不为null其为组件时,修改base的指向。
  if ((vnode = vnode._parent) != null && vnode._component != null) {
    vnode._dom = vnode._component.base = null;
    for (let i = 0; i < vnode._children.length; i++) {
      let child = vnode._children[i];
      if (child != null && child._dom != null) {
        // 指向第一个不为null的child节点
        vnode._dom = vnode._component.base = child._dom;
        break;
      }
    }
    return updateParentDomPointers(vnode);
  }
}

3. 总结

本章主要分析了setState及Preact的更新机制,Preact setState将待更新的组件push到更新队列,在NextTick完成组件Vnode、UI的更新。Preact的异步更新机制,可以很好的减少渲染的次数,从而减少DOM的更新频率,从而提高UI的渲染效率。

4. 参考文档