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. 參考文檔