preact原始碼分析(三)

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

image

image

前言

在本節內容, 我將帶領大家瀏覽一邊Preact的render過程。當然為了降低閱讀原始碼的難度, 我們這一次不會考慮render元件的情況, 也不會考慮setState更新的情況。大家在閱讀文章的時候, 請仔細閱讀我新增在程式碼中的註釋

src/render.js

render

render方法在Preact文件中用法如下。可以看出render方法第一個引數是jsx, 第二個引數是需要掛載的DOM節點。


import { h, render } from 'preact';

render((
    <div id="foo">
        <span>Hello, world!</span>
        <button onClick={ e => alert("hi!") }>Click Me</button>
    </div>
), document.body);
複製程式碼

第一個引數雖然不是VNode,但是可以通過Babel的外掛transform-react-jsx, 轉換為如下的形式。其中h函式就是createElement函式。


h(
  'div',
  {'id': 'foo},
  h('span', { }, 'Hello, world!'),
  h('button', { onClick: e => alert("hi!") }, 'Click Me')
)
複製程式碼

export function render(vnode, parentDom) {
  // 第一次render時, parentDom上還沒有掛載_prevVNode屬性, 故oldVNode為null
  let oldVNode = parentDom. ;

  // 使用Fragment包裹vNode, 這時VNode的內容, 如下
  // {
  //   type: 'Fragment',
  //   props: {
  //     children: {
  //       type: 'content',
  //       props: {
  //         children: [
  //           {
  //             type: 'h1',
  //             props: {
  //               children: ['HelloWorld']
  //             },
  //           }
  //         ]
  //       }
  //     }
  //   }
  // }
	vnode = createElement(Fragment, null, [vnode]);

  let mounts = [];
  
  // 使用diffChildren方法比較新舊Vnode, 具體分析我們跳到下一節。注意這裡同時掛載了_prevVNode的屬性
	diffChildren(
    parentDom,
    parentDom._prevVNode = vnode,
    oldVNode,
    EMPTY_OBJ,
    parentDom.ownerSVGElement!==undefined,
    oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes),
    mounts,
    vnode
  );

  // 執行已掛載元件的componentDidMount生命週期, 本期文章不涉及元件的render故不展開
	commitRoot(mounts, vnode);
}
複製程式碼

src/diff/children.js

diff演算法是我們在學習Preact原始碼裡的重頭戲, 這裡也是原始碼中最複雜的一部分。

因為整個VNode是一個樹形的結構, 我們將從root節點開始,一步步分析它做了什麼, 在diff的過程中會有很多遞迴的操作, 所以我們需要留意每一次遞迴的時函式引數的不同。

我們假設需要渲染的Dom如下所示,我也會忽略一些邊界情況比如type為SVG標籤的情況。儘可能的簡單,方便理解。


render((
  <div id="content">
    <h1>HelloWorld</h1>
  </div>
), document.getElementById('app'));
複製程式碼

diffChildren

/**
 * parentDom為掛載的Dom節點(document.body)
 * newParentVNode新的Vnode節點
 * oldParentVNode之前的Vnode節點(第一次渲染時這裡oldParentVNode為null, setState更新時這裡將不為null)
 * context(第一次渲染時這裡為空物件)
 * isSvg(判斷是否為SVG)
 * excessDomChildren在第一次render時這裡的值應當為parentDom的所有的子節點, 這裡為空陣列
 * mounts為空陣列, mounts中為已掛載的元件的列表
 * ancestorComponent 直接父元件
 */
export function diffChildren(
  parentDom,
  newParentVNode,
  oldParentVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent
) {

	let childVNode, i, j, p, index, oldVNode, newDom,
		nextDom, sibDom, focus,
		childDom;

  // 在這裡進行操作是將newParentVNode, oldParentVNode扁平化。並將扁平化的子VNode陣列掛載到VNode節點的_children屬性上
  // vnode._children = [ { type: 'div', props: {...} } ]
  let newChildren = newParentVNode._children ||
    toChildArray(
      newParentVNode.props.children,
      newParentVNode._children=[],
      coerceToVNode
    );

  let oldChildren = []
  
  childDom = null

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

  // 對子VNode集合進行迴圈
	for (i=0; i<newChildren.length; i++) {

    // childVNode為newChildren中的每一個VNode
    childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
    oldVNode = index = null;
    
    // ... 省略一部分原始碼

    // 使用diff演算法依次的對比每一個新舊VNode節點, 因為是第一次render所以這裡oldVNode始終為null
    // diff返回的是一個對比後的dom節點
    // 我們接下來跳轉到下一節去看diff方法的具體實現
		newDom = diff(
      oldVNode==null ? null : oldVNode._dom,
      parentDom,
      childVNode,
      oldVNode,
      context,
      isSvg,
      excessDomChildren,
      mounts,
      ancestorComponent,
      null
    );

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

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

				outer: if (childDom==null || childDom.parentNode!==parentDom) {
          // 將diff比較後更新Dom掛載到parentDom中, 完成渲染
					parentDom.appendChild(newDom);
				} else {
				}
      }
		}
	}

	 // ... 省略一部分原始碼
}
複製程式碼

toChildArray

toChildArray函式會遍歷的VNode的Children, 將Children以陣列的形式掛載到VNode的_children屬性上。


export function toChildArray(children, flattened, map) {
	if (flattened == null) {
    flattened = []
  }
	if (children==null || typeof children === 'boolean') {

  } else if (Array.isArray(children)) {
    // 如果children為陣列, 進行遞迴操作
		for (let i=0; i < children.length; i++) {
			toChildArray(children[i], flattened);
		}
	} else {
		flattened.push(map ? map(children) : children);
	}

	return flattened;
}
複製程式碼

src/diff/index.js

diff

/**
 * 舊的Vnode節點上的_dom屬性, 原有的dom節點。第一次render時為null,不能複用原有的dom節點
 * parentDom需要掛載的父節點
 * newVNode新的Vnode節點
 * oldVNode舊的Vnode節點, 第一次render這裡為null
 * context此時為空物件
 * isSvg
 * excessDomChildren為空陣列
 * mounts為空陣列
 * ancestorComponent直接父元件, render時的VNode節點
 * force為null
 */
export function diff(
  dom,
  parentDom,
  newVNode,
  oldVNode,
  context,
  isSvg,
  excessDomChildren,
  mounts,
  ancestorComponent,
  force
) {

  // 如果oldVNode,newVNode型別不同, dom節點不能複用。
	if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type) {
		// ... 省略一部分原始碼
		dom = null;
		oldVNode = {};
	}

  let c, p, isNew = false, oldProps, oldState, oldContext

  // newType為newVNode節點的型別
  let newType = newVNode.type;
  
	let clearProcessingException;

	try {
		outer: if (oldVNode.type===Fragment || newType===Fragment) {
      // 當type為Fragment時,這裡先略過
      // ... 省略一部分原始碼
		} else if (typeof newType==='function') {
      // 當type為元件時
      // ... 省略一部分原始碼, 這裡先略過
		} else {
      // 我們這裡先關注type等於string的情況

      // 我們接下來跳轉到下一節去看diffElementNodes方法的具體實現
      // diffElementNodes將會返回對比後dom
			dom = diffElementNodes(
        dom,
        newVNode,
        oldVNode,
        context,
        isSvg,
        excessDomChildren,
        mounts,
        ancestorComponent
      )

			if (newVNode.ref && (oldVNode.ref !== newVNode.ref)) {
				applyRef(newVNode.ref, dom, ancestorComponent);
			}
		}

    // 掛載dom節點到_dom的屬性上
		newVNode._dom = dom;

		// ... 省略一部分原始碼
	}
	catch (e) {
		catchErrorInComponent(e, ancestorComponent);
	}

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

diffElementNodes

diffElementNodes顧名思義就是對新舊的非元件的ElementNodes節點比較


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

	let d = dom;

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

	if (dom==null) {
    // 由於是第一次渲染所以不能複用原有的dom, 需要建立dom節點
		dom = newVNode.type===null ? document.createTextNode(newVNode.text) : document.createEleme(newVNode.type)

		// 建立了一個新的父節點,因此以前的子節點都不能重用, excessDomChildren置為null
		excessDomChildren = null;
  }
  
  // 將建立好的dom節點掛載到_dom屬性上
	newVNode._dom = dom;

	if (newVNode.type===null) {
		// ... 省略一部分原始碼
	}
	else {
		if (excessDomChildren!=null && dom.childNodes!=null) {
      // ... 省略一部分原始碼
    }
    
		if (newVNode!==oldVNode) {
			let oldProps = oldVNode.props;
			if (oldProps==null) {
				oldProps = {};
				if (excessDomChildren!=null) {
					// ... 省略一部分原始碼
				}
      }
      
      // ... 省略一部分原始碼, 這裡是對dangerouslySetInnerHTML的處理

      // 子節點依然可能包含子節點所以將當前的節點做為父節點,使用diffChildren遍歷子節點diff
      // 注意這時parentDom這個引數的值,是剛剛建立的dom
			diffChildren(
        dom,
        newVNode,
        oldVNode,
        context,
        newVNode.type==='foreignObject' ? false : isSvg,
        excessDomChildren,
        mounts,
        ancestorComponent
      )

      // 對比新舊的props, 掛載到dom上,diffProps函式本身並複雜
			diffProps(
        dom,
        newVNode.props,
        oldProps,
        isSvg
      );
		}
	}

  // 返回更新後的dom節點
	return dom;
}
複製程式碼

src/diff/props.js

diffProps


export function diffProps(dom, newProps, oldProps, isSvg) {
	for (let i in newProps) {
    // 對新的props處理
		if (i!=='children' && i!=='key' && (!oldProps || oldProps[i]!=newProps[i])) {
			setProperty(dom, i, newProps[i], oldProps[i], isSvg);
		}
	}
	for (let i in oldProps) {
    // 對新的props中不存在的屬性處理
		if (i!=='children' && i!=='key' && (!newProps || !(i in newProps))) {
			setProperty(dom, i, null, oldProps[i], isSvg);
		}
	}
}
複製程式碼

setProperty


function setProperty(dom, name, value, oldValue, isSvg) {
  let v;
  // 對class屬性處理
  // 如果是svg屬性使用className
	if (name==='class' || name==='className') name = isSvg ? 'class' : 'className';

  // 對style屬性進行處理
	if (name==='style') {

		let s = dom.style;

		if (typeof value==='string') {
			s.cssText = value;
		} else {
			if (typeof oldValue==='string') {
        s.cssText = '';
      }
			for (let i in oldValue) {
        // 對駝峰的樣式處理
				if (value==null || !(i in value)) {
          s.setProperty(i.replace(CAMEL_REG, '-'), '');
        }
			}
			for (let i in value) {
        v = value[i];
        // 對駝峰的樣式和數字進行處理
				if (oldValue==null || v!==oldValue[i]) {
					s.setProperty(
            i.replace(CAMEL_REG, '-'),
            typeof v==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (v + 'px') : v
          );
				}
			}
		}
  }	else if (name==='dangerouslySetInnerHTML') {
		return;
	} else if (name[0]==='o' && name[1]==='n') {
    // 對事件進行處理
		let useCapture = name !== (name=name.replace(/Capture$/, ''));
		let nameLower = name.toLowerCase();
		name = (nameLower in dom ? nameLower : name).substring(2);

		if (value) {
			if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
		}
		else {
			dom.removeEventListener(name, eventProxy, useCapture);
		}
		(dom._listeners || (dom._listeners = {}))[name] = value;
	} else if (name!=='list' && !isSvg && (name in dom)) {
		dom[name] = value==null ? '' : value;
	} else if (value==null || value===false) {
    // 刪除屬性
		dom.removeAttribute(name);
	} else if (typeof value!=='function') {
    // 設定屬性
		dom.setAttribute(name, value);
	}
}
複製程式碼

結語

我們通過上面?精簡後的原始碼可知,如果render中是VNode的type為普通ElementNodes節點的渲染的大致流程如下。

image

如果渲染流程外,我們也知道VNode節點上,幾個私有屬性的含義

image

下面兩期將會介紹, 渲染元件的流程以及setState非首次渲染的流程,加油~!

其他

preact原始碼分析(一)

preact原始碼分析(二)

preact原始碼分析(三)

preact原始碼分析(四)

preact原始碼分析(五)

相關文章