Preact原始碼閱讀(三)- 更新機制

語言: CN / TW / HK

在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_rerenderCountb標記當前待更新的元件數目,大於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. 參考文件