preact原始碼分析(五)

利維亞的傑洛特發表於2019-03-30

image

image

前言

前兩篇文章, 講的是VNode和元件在初始化的情況下的渲染過程。因為沒有涉及和OldVNode的比較所以省略了很多原始碼中的細節。這次我們來說說, 當setState時元件是如何更新的, 這中間發生了什麼。我們本期會重新回顧之前幾期文章中, 介紹的函式比如diff, diffChildren, diffElementNode等。⛽️

寫的不一定對,都是我自己個人的理解,還請多多包含。

src/component.js

我們在第二期文章中介紹到。在呼叫setState的時候會將元件的例項傳入enqueueRender函式。enqueueRender函式會將元件的例項的_dirty屬性設定為true。並且將元件push到q佇列中。緊接著, 將process函式作為引數呼叫defer的函式, process函式中會清空q佇列, 並執行q佇列中每個元件的forceUpdate方法。而defer則會返回一個Promise.resolve()。

? 當然為了降低閱讀的複雜度, 元件不是很複雜。請仔細看我每一行標註的註釋哦


import { h, render, Component } from 'preact';

class Clock extends Component {
    constructor() {
      super();
      this.state = {
        time: Date.now();
      }
    }

    getTime = () => {
      this.setState({
        time: Date.now();
      })
    }

    render(props, state) {
      let time = new Date(state.time).toLocaleTimeString()
      return (
        <div>
          <button onClick="getTime">獲取時間</button>
          <h1>{ time }</h1>
        </div>
      )
    }
}

render(<Clock />, document.body);
複製程式碼

image

forceUpdate


Component.prototype.forceUpdate = function(callback) {
  // vnode為元件例項上掛載的元件VNode節點
  let vnode = this._vnode,
  // dom為元件VNode節點上掛載的,DOM例項由diff演算法生成的
      dom = this._vnode._dom,
  // parentDom為例項上掛載的,元件掛載的節點
      parentDom = this._parentDom;
	if (parentDom) {
    // force將會控制元件的shouldComponentUpdate的生命是否被呼叫
    // 當force為true時, shouldComponentUpdate不應該被呼叫
		const force = callback!==false;

    let mounts = [];
    // 返回更新後diff
    // 注意這裡傳入的newVNode和oldVNode都是vnode
    // 那麼他們的區別在那裡呢?我們如何區分這兩個VNode呢?
    // 我們可以看下setState方法, 我們在setState中將最新的setState掛載到了_nextState屬性中
		dom = diff(
      dom,
      parentDom,
      vnode,
      vnode,
      this._context,
      parentDom.ownerSVGElement!==undefined,
      null,
      mounts,
      this._ancestorComponent,
      force
    );
    // 如果掛載節點已經改變了,將更新後的dom, push到新的元件中
		if (dom!=null && dom.parentNode!==parentDom) {
			parentDom.appendChild(dom);
    }
  }
};
複製程式碼

src/diff/index.js

第一次呼叫diff的過程

image

除了上圖外,我們還可以得知,如果是一個複雜的VNode樹?結構,元件在更新的時候,會先從外向裡的順序執行getDerivedStateFromProps, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, getSnapshotBeforeUpdate的生命週期。再由內向外執行componentDidUpdate的生命週期。

初次掛載的時候也是同裡, 向外向內執行componentWillMount等生命週期,然後再由內向外的執行componentDidMount的生命週期。

我們通過diffElementNodes也可以看出來,Dom元素屬性的更新是由內到外的順序,進行更新的。


export function diff(
  dom,
  parentDom,
  newVNode,
  oldVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent,
  force
) {

	let c, p, isNew = false, oldProps, oldState, snapshot,
		newType = newVNode.type;

	try {
		outer: if (oldVNode.type===Fragment || newType===Fragment) {
			// ... 省略原始碼
		}
		else if (typeof newType==='function') {

      // _component屬性是在初始化渲染時, 掛載在VNode節點上的元件的例項
			if (oldVNode._component) {
				c = newVNode._component = oldVNode._component;
			}
			else {
				// ...初次渲染的情況,省略原始碼
			}

      // 掛載新的VNode節點, 供下一次setState的diff使用
			c._vnode = newVNode;

			// s為當前最新的元件的state狀態
      let s = c._nextState || c.state;
      
      // 呼叫getDerivedStateFromProps生命週期
			if (newType.getDerivedStateFromProps!=null) {
        // 更新前元件的state
				oldState = assign({}, c.state);
        if (s===c.state) {
          s = c._nextState = assign({}, s);
        }
        // 通過getDerivedStateFromProps更新元件的state
				assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
			}

			if (isNew) {
				// ...如果是新元件的初次渲染
			}
			else {
        // 執行componentWillReceiveProps生命週期, 並更新新state
				if (
          newType.getDerivedStateFromProps==null &&
          force==null &&
          c.componentWillReceiveProps!=null
        ) {
					c.componentWillReceiveProps(newVNode.props, cctx);
					s = c._nextState || c.state;
				}

        // 執行shouldComponentUpdate生命週期, 如果返回false停止渲染(不在執行diff函式)
        // ⚠️ 如果force引數是true則不會執行shouldComponentUpdate的生命週期
        // setState中時,forceUpdate函式,force始終傳入的是false, 所以會執行shouldComponentUpdate的函式
				if (
          !force &&
          c.shouldComponentUpdate!=null &&
          c.shouldComponentUpdate(newVNode.props, s, cctx) === false
        ) {
					c.props = newVNode.props;
          c.state = s;
          // _dirty設定為false, 停止更新
					c._dirty = false;
					break outer;
				}

        // 執行componentWillUpdate的生命週期
				if (c.componentWillUpdate!=null) {
					c.componentWillUpdate(newVNode.props, s, cctx);
				}
			}

      oldProps = c.props;
      
			if (!oldState) {
        oldState = c.state;
      }

      // 將元件的props和設定為最新的狀態(_nextState經過了一些生命週期函式的更新, 所以要重新賦予元件新的state)
			c.props = newVNode.props;
			c.state = s;

      // 之前的VNode節點
      let prev = c._prevVNode;
      // 返回最新的元件render後的VNode節點
			let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
			c._dirty = false;

      // 執行getSnapshotBeforeUpdate生命週期
			if (!isNew && c.getSnapshotBeforeUpdate!=null) {
				snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

      // 對比新舊VNode節點, 在下一次的diff函式中我們將進入diffElementNodes的分支語句
			c.base = dom = diff(
        dom,
        parentDom,
        vnode,
        prev,
        context,
        isSvg,
        excessDomChildren,
        mounts,
        c,
        null
      );

      // 掛載_parentDom
			c._parentDom = parentDom;

		}
		else {
			// ...
		}

		newVNode._dom = dom;

		if (c!=null) {
      // 執行setState的回撥函式
			while (p=c._renderCallbacks.pop()) {
        p.call(c);
      }

      // 執行componentDidUpdate的生命週期
			if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
				c.componentDidUpdate(oldProps, oldState, snapshot);
			}
		}
	}
	catch (e) {
		catchErrorInComponent(e, ancestorComponent);
	}

	return dom;
}
複製程式碼

第二次呼叫diff的過程

在第一次呼叫diff的時候, 進入了typeof newType==='function'的分支, 我們呼叫了元件的render函式, 返回的是類似如下的VNode結構。我們在第二次diff的時候, 將比較新舊元件返回的VNode, 並對屬性進行修改。完成對DOM的更新操作。

image


<div>
  <button onClick="getTime">獲取時間</button>
  <h1>{ time }</h1>
</div>

// 新的VNode
{
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: function () {
            // ...
          },
          children: [
            {
              type: null,
              text: '獲取時間'
            }
          ]
        }
      },
      {
        type: 'h1',
        props: {
          children: {
            {
              type: null,
              text: 新時間
            }
          }
        }
      }
    ]
  }
}

// 舊VNode
{
  type: 'div',
  props: {
    children: [
      {
        type: 'button',
        props: {
          onClick: function () {
            // ...
          },
          children: [
            {
              type: null,
              text: '獲取時間'
            }
          ]
        }
      },
      {
        type: 'h1',
        props: {
          children: {
            {
              type: null,
              text: 舊時間
            }
          }
        }
      }
    ]
  }
}
複製程式碼

export function diff(
  dom,
  parentDom,
  newVNode,
  oldVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent,
  force
) {


	let c, p, isNew = false, oldProps, oldState, snapshot,
		newType = newVNode.type;


	try {
		outer: if (oldVNode.type===Fragment || newType===Fragment) {
			// ...省略部分原始碼
		}
		else if (typeof newType==='function') {
      // ...省略部分原始碼
		}
		else {
      // 將新舊VNode帶入到diffElementNodes中
			dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);
		}

		newVNode._dom = dom;
	}
	catch (e) {
		catchErrorInComponent(e, ancestorComponent);
	}

	return dom;
}
複製程式碼

function diffElementNodes(
  dom,
  newVNode,
  oldVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent
) {

  // 這裡d就是之前掛載初次渲染的dom
	let d = dom;

  // ...
	
	if (dom==null) {
    // ...初次渲染時dom不能複用,需要建立dom的節點,我們已經建立裡Dom所以可以複用
	}
	newVNode._dom = dom;

	if (newVNode.type===null) {
    // ...
	}
	else {
		if (newVNode!==oldVNode) {
      // 舊的props
      let oldProps = oldVNode.props;
      // 新的props
			let newProps = newVNode.props;
      // 遞迴的比較每一個VNode子節點,這裡比較VNode子節點,將會插入到目前的dom中,
      // 我們在這裡不深入到子VNode中,而是關注與root節點
      // 當diffChildren遞迴的執行完成後內部的Dom已經完成了更新的過程,我們暫時不去關心內部。
			diffChildren(
        dom,
        newVNode,
        oldVNode,
        context,
        newVNode.type==='foreignObject' ? false : isSvg,
        excessDomChildren,
        mounts,
        ancestorComponent
      );
      // 更新完成後,我們將更新root層的dom的屬性
			diffProps(
        dom,
        newProps,
        oldProps,
        isSvg
      );
		}
	}

	return dom;
}
複製程式碼

export function diffProps(dom, newProps, oldProps, isSvg) {
  // 對於新props的更新策略,如果key是value或者checked使用原生Dom節點和newProps比較
  // 如果不是這兩個key使用oldProps和newProps比較
  // 更新兩者屬性不相等的屬性
	for (let i in newProps) {
    if (
      i!=='children' && i!=='key' &&
      (
        !oldProps ||
        ((i==='value' || i==='checked') ? dom : oldProps)[i]!==newProps[i]
      )
    ) {
			setProperty(dom, i, newProps[i], oldProps[i], isSvg);
		}
  }
  // 多於舊的props的更新策略,如果在newProps中不存在的屬性,則會去刪除這個屬性
  // setProperty一些內部處理細節,這裡就不做展開
	for (let i in oldProps) {
		if (
      i!=='children' &&
      i!=='key' &&
      (!newProps || !(i in newProps))
    ) {
			setProperty(dom, i, null, oldProps[i], isSvg);
		}
	}
}
複製程式碼

結語

接下來我們可以參考(抄?)一些部落格和preact的原始碼,實現屬於自己的React

其他

preact原始碼分析(一)

preact原始碼分析(二)

preact原始碼分析(三)

preact原始碼分析(四)

preact原始碼分析(五)

相關文章