上篇文章已引見過idff的處置懲罰邏輯重要分為三塊,處置懲罰textNode,element及component,但細緻怎樣處置懲罰component還沒有細緻引見,接下來說一下preact是怎樣處置懲罰component的。
組件的diff
經由過程進修元素節點的diff操縱,我們無妨斗膽勇敢猜測一下,組件是做了以下diff操縱:
- 組件差別範例或許不存在就建立,走響應的生命周期鈎子
- 比較組件的屬性
- 比較組件的孩子
事實上和我們的猜測很相似,在舉行下一步之前,我們先了解下preact中的數據結構:
// 以下JSX
<App>
<Child></Child>
</App>
// App組件的實例,會有以下屬性
{
base, // 對應組件襯着的dom
_component, // 指向Child組件
}
// Child組件有以下屬性
{
base, // 與App組件實例指向同一個dom
_parentComponent, // 指向App組件
}
// 對應的dom節點,即前文中的base對象
{
_component // 指向App組件,而不是Child組件
}
然後我們看一下buildComponentFromVNode邏輯:
- 假如組件範例雷同挪用setComponentProps
假如組件範例差別:
- 接納老的組件
- 建立新的組件實例
- 挪用setComponentProps
- 接納老的dom
- 返回dom
function buildComponentFromVNode(dom, vnode, context, mountAll) {
let c = dom && dom._component,
originalComponent = c,
oldDom = dom,
isDirectOwner = c && dom._componentConstructor === vnode.nodeName, // 組件範例是不是變了
isOwner = isDirectOwner,
props = getNodeProps(vnode);
while (c && !isOwner && (c = c._parentComponent)) { // 假如組件範例變了,一向向上遍歷;看範例是不是雷同
isOwner = c.constructor === vnode.nodeName;
}
// 此時isOwner就代表組件範例是不是雷同
// 假如組件範例雷同,只設置屬性;然後將dom指向c.base
if (c && isOwner && (!mountAll || c._component)) {
setComponentProps(c, props, 3, context, mountAll);
dom = c.base;
} else {
if (originalComponent && !isDirectOwner) { // 組件範例差別就先卸載組件
unmountComponent(originalComponent);
dom = oldDom = null;
}
// 建立組件的重要邏輯就是return new vnode.nodeName()
c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
c.nextBase = dom;
// passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
oldDom = null;
}
setComponentProps(c, props, 1, context, mountAll);
dom = c.base;
if (oldDom && dom !== oldDom) {
oldDom._component = null;
recollectNodeTree(oldDom, false);
}
}
return dom;
}
能夠看到組件進一步diff的中心邏輯在setComponentProps要領中,setComponentProps大抵做了兩件事:
- 挪用襯着前的生命周期鈎子: componentWillMount 與 componentWillReceiveProps
- 挪用renderComponent
renderComponent重要邏輯為:
- 挪用shouldComponentUpdate 或 componentWillUpdate生命周期鈎子
挪用組件的render要領
- 假如render的結果是一個組件,做相似與buildComponentFromVNode的操縱
- 假如render的結果是dom節點,挪用diff操縱
- 替換新的節點,卸載老的節點或組件
- 為組件的base增加組件援用_component
- 挪用組件的生命周期鈎子componentDidUpdate,componentDidMount。
至此,我們已大抵了解了preact的大抵全流程,接下來我們看一下它的diffChildren的算法:
- 將原始dom的子節點分為兩部分,有key的放在keyed map內里,沒有key的放在children數組內里。
- 遍歷vchildren,經由過程key找到keyed中的child,假如child不存在,從children中掏出雷同範例的子節點
- 對child與vchild舉行diff,此時獲得的dom節點就是新的dom節點
- 然後與老的dom節點對應的節點比較,操縱dom樹。
- 末了刪除新的dom樹中不存在的節點。
function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
let originalChildren = dom.childNodes,
children = [],
keyed = {},
keyedLen = 0,
min = 0,
len = originalChildren.length,
childrenLen = 0,
vlen = vchildren ? vchildren.length : 0,
j,
c,
f,
vchild,
child;
if (len !== 0) {
for (var i = 0; i < len; i++) {
var _child = originalChildren[i],
props = _child.__preactattr_,
key = vlen && props ? _child._component ? _child._component.__key : props.key : null;
if (key != null) {
keyedLen++;
keyed[key] = _child;
} else if (props || (_child.splitText !== undefined ? isHydrating ? _child.nodeValue.trim() : true : isHydrating)) {
children[childrenLen++] = _child;
}
}
}
// 遍歷假造dom節點
// 取child(有key,證實它兩個是要對應比較的)
// 假如child和originchildren[i]比較
// originchild沒有,過剩,不然插進去到originchild前面
if (vlen !== 0) {
for (var i = 0; i < vlen; i++) {
vchild = vchildren[i];
child = null;
// attempt to find a node based on key matching
var key = vchild.key;
if (key != null) {
if (keyedLen && keyed[key] !== undefined) {
child = keyed[key];
keyed[key] = undefined;
keyedLen--;
}
}
// attempt to pluck a node of the same type from the existing children
else if (!child && min < childrenLen) {
for (j = min; j < childrenLen; j++) { //從min今後最先遍歷,假如是雷同範例的節點就拿出來,誰人位置設為undefined
if (children[j] !== undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
child = c;
children[j] = undefined;
if (j === childrenLen - 1) childrenLen--;
if (j === min) min++;
break;
}
}
}
// morph the matched/found/created DOM child to match vchild (deep)
child = idiff(child, vchild, context, mountAll);
f = originalChildren[i];
if (child && child !== dom && child !== f) {
if (f == null) {
dom.appendChild(child);
} else if (child === f.nextSibling) {
removeNode(f);
} else {
dom.insertBefore(child, f);
}
}
}
}
// remove unused keyed children:
// keyedLen標識老的鳩合中另有的元素,但沒在新的鳩合中運用
if (keyedLen) {
for (var i in keyed) {
if (keyed[i] !== undefined) recollectNodeTree(keyed[i], false);
}
}
// remove orphaned unkeyed children:
// min代表拿走的元素
while (min <= childrenLen) {
if ((child = children[childrenLen--]) !== undefined) recollectNodeTree(child, false);
}
}
從上面能夠看出,preact只處置懲罰了罕見的運用場景,沒有做迥殊的優化步伐,這也致使它在一些狀況下的性能比react低,如:從a b到b a。
而react中會紀錄lastIndex,對其做了響應的優化,節點的Index > lastIndex的狀況下,不做挪動操縱。
然則假如react中有length > 2,最前面的節點位置與末了面的節點位置交換的狀況下,因為index一向小於lastIndex,就會落空上述的優化結果。
這類狀況,在snabbdom中獲得了優化,snabbdom經由過程oldStartIdx,oldEndIdx,newStartIdx,newEndIdx四個指針,在每次循環中先處置懲罰特殊狀況,並經由過程減少指針局限,獲得性能上的提拔。