前言
上一期我們瞭解到了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的元件時渲染的大致流程如下圖。