前言
在本節內容, 我將帶領大家瀏覽一邊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節點的渲染的大致流程如下。
如果渲染流程外,我們也知道VNode節點上,幾個私有屬性的含義
下面兩期將會介紹, 渲染元件的流程以及setState非首次渲染的流程,加油~!