preact原始碼分析(四)

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

image

image

前言

上一期我們瞭解到了Preact渲染普通的節點(VNode的type不是Function型別)時的過程。本期我帶大家來了解下當渲染的是一個元件時preact中發生了什麼。當然為了降低閱讀原始碼的複雜度, 我們本次只討論初次渲染元件的情況。暫不考慮setState時元件更新的情況。

我們將從官方的Demo入手, 一步步瞭解渲染的過程


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

class Clock extends Component {
    render() {
        let time = new Date().toLocaleTimeString();
        return <span>{ time }</span>;
    }
}

// 將一個時鐘渲染到 <body > 標籤:
render(<Clock />, document.body);
複製程式碼

src/render.js

同渲染普通型別的VNode節點一樣, 將VNode包裹一層Fragment後, VNode進入了diffChildren方法中。唯一值得注意的時VNode的type為Clock, 而非普通的字串型別。我們將在diff方法看到兩者的具體的區別。


export function render(vnode, parentDom) {

  // render時_prevVNode還沒沒有掛載, 此時為null
  let oldVNode = parentDom._prevVNode;
  
  // 使用Fragment包裹VNode
  // {
  //   type: 'Fragment',
  //   props: {
  //     children: [
  //       {
  //         type: Clock, 這裡型別不是字串
  //         props: {
  //         }
  //       }
  //     ]
  //   }
  // }
	vnode = createElement(Fragment, null, [vnode]);

  let mounts = [];
  // 將當前的VNode掛載到parentDom的_prevVNode屬性  
  diffChildren(
    parentDom,
    parentDom._prevVNode = vnode,
    oldVNode,
    EMPTY_OBJ,
    parentDom.ownerSVGElement!==undefined, oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes), mounts,
    vnode
  );
  
  // 執行已掛載元件的componentDidMount生命週期
	commitRoot(mounts, vnode);
}
複製程式碼

src/diff/children.js

在渲染元件VNode時, 流程大致和渲染普通的VNode一致。都是將通過diff演算法返回Dom節點append到parentDom中。


export function diffChildren(
  parentDom,
  newParentVNode,
  oldParentVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent
) {
	let childVNode, i, j, p, index, oldVNode, newDom,
		nextDom, sibDom, focus,
		childDom;

  // 將扁平的的VNode掛載到_children屬性上
  // [ { type: Clock, props: { ... } } ]
  let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode);
  // oldChildren此時為[]
	let oldChildren = []

	let oldChildrenLength = oldChildren.length;

	// ... 省略一部分原始碼

  // 遍歷VNode節點
	for (i=0; i<newChildren.length; i++) {
		childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
    oldVNode = index = null;
    
    p = oldChildren[i];
    
    // ... 省略一部分原始碼,這裡主要是查詢可以複用的DOM節點,

    // 進入diff演算法比較新舊VNode節點
		newDom = diff(
      oldVNode==null ? null : oldVNode._dom,
      parentDom,
      childVNode,
      oldVNode,
      context,
      isSvg,
      excessDomChildren,
      mounts,
      ancestorComponent,
      null
    );

		if (childVNode!=null && newDom !=null) {

      
			else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) {

				if (childDom==null || childDom.parentNode!==parentDom) {
					parentDom.appendChild(newDom);
				}
			}
		}
	}
}
複製程式碼

src/diff/index.js

在diff方法中, 主要會比較三種型別的節點。第一種Fragment型別, 第二種Function型別, 和其他型別的節點。我們的示例會用到兩種判斷,請仔細閱讀我給原始碼新增的註釋。

我們在首次diff中, newVNode為Clock元件, 所以我們會進入VNode.type為Functiond的分支。我們接下來會呼叫元件例項的render函式, 返回VNode**( { tpye: 'span', props: { //... } } )**。接下來, 遞迴的使用diff演算法比較render返回的VNode。我們在遞迴的時, diff函式將進入其他型別的節點的分支,比較返回的VNode, 並且會呼叫diffElementNodes函式, 返回建立後dom節點。最後並新增到parentDom中。


// 這裡每一個引數的含義請參考, 上一期的文章
export function diff(
  dom,
  parentDom,
  newVNode,
  oldVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent,
  force
) {

  // 因為是初始化渲染, dom是不能複用, 我們刪除整個子樹, oldVNode重置為null
	if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type) {
		dom = null;
		oldVNode = EMPTY_OBJ;
	}

  let c, p, isNew = false, oldProps, oldState, oldContext
  
  // 元件的型別(元件的類)
	let newType = newVNode.type;

	try {
		outer: if (oldVNode.type===Fragment || newType===Fragment) {
			// ... 省略一部分原始碼, 這裡是對Fragment型別的處理
		}
		else if (typeof newType==='function') {


			if (oldVNode._component) {
        // ... 省略一部分原始碼,如果是,不是第一次渲染元件的處理
			}
			else {
        // 第一次渲染的元件處理
        isNew = true;
        
        // 如果元件的類擁有render函式, c和_component儲存的是元件的例項
				if (newType.prototype && newType.prototype.render) {
					newVNode._component = c = new newType(newVNode.props, cctx);
				} else {
          // 如果元件的類沒有render函式, 使用基礎的Component類構建元件的例項
          newVNode._component = c = new Component(newVNode.props, cctx);
          // 例項c的建構函式等於元件類的建構函式
          c.constructor = newType;
          // render函式直接返回建構函式返回的結果
					c.render = doRender;
        }
        // _ancestorComponent掛載的是自己父VNode, 目前指的是被Fragment包裹的那一層元件
        c._ancestorComponent = ancestorComponent;
      
        // 初始化c的props和state
				c.props = newVNode.props;
        if (!c.state) c.state = {};
        
        // _dirty屬性表明是否更新元件
        c._dirty = true;
        // 用於儲存setState回撥的陣列
				c._renderCallbacks = [];
			}

      // _vnode掛載元件例項的VNode節點
			c._vnode = newVNode;

      // s變數儲存了元件的state狀態
      let s = c._nextState || c.state;
      
      // 呼叫靜態的生命週期方法getDerivedStateFromProps
      // getDerivedStateFromProps生命週期返回的物件用於更新state
			if (newType.getDerivedStateFromProps!=null) {
				oldState = assign({}, c.state);
				if (s === c.state) {
          s = assign({}, s);
        }
        // 更新state, 如果返回null, 不更新
				assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
			}

			if (isNew) {
        // 如果是第一次渲染, 並且componentWillMount不為null, 執行componentWillMount的生命週期函式
				if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) {
          c.componentWillMount();
        }
        // 將元件的例項push到已經掛載的元件的列表中
				if (c.componentDidMount!=null) {
          mounts.push(c);
        }
			}
			else {
				// ... 省略一部分原始碼,不是第一次渲染元件的處理
			}

      // 渲染更新前的oldProps,oldState
      oldProps = c.props;
			if (!oldState) {
        oldState = c.state;
      }

      // 更新元件的props和state
			c.props = newVNode.props;
			c.state = s;

      // 初次渲染prev為null
      let prev = c._prevVNode;
      // 元件例項上掛載的_prevVNode的屬性為當前例項呼叫render函式返回的VNode節點(之前c儲存的是元件的例項)
      let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
      // 這個元件禁止更新
			c._dirty = false;

      // 呼叫getSnapshotBeforeUpdate的生命週期函式
      // getSnapshotBeforeUpdate()在最新的渲染輸出提交給DOM前將會立即呼叫
			if (!isNew && c.getSnapshotBeforeUpdate!=null) {
				oldContext = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

      // vnode現在儲存的是元件例項render返回的VNode
      // 將render返回VNode,遞迴的使用diff方法繼續比較,返回的dom儲存base屬性中
			c.base = dom = diff(
        dom,
        parentDom,
        vnode,
        prev,
        context,
        isSvg,
        excessDomChildren,
        mounts,
        c,
        null
      );

			c._parentDom = parentDom;

		}
		else {
      // 當上面的c元件render後, 將render返回VNode節點,使用diff進行遞迴的比較時
      // render返回VNode的type是string型別, 所以進入這個分支中, 進行dom比較, 具體內容可以參考上一篇文章
			dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);
		}

    // 將dom掛載到_dom中
		newVNode._dom = dom;
	}
	catch (e) {}

  // 返回dom
	return dom;
}
複製程式碼

結語

我們通過上面?精簡後的原始碼可知,如果render中是VNode的元件時渲染的大致流程如下圖。

image

相關文章