上篇文章已经介绍过idff的处理逻辑主要分为三块,处理textNode,element及component,但具体怎么处理component还没有详细介绍,接下来讲一下preact是如何处理component的。
组件的diff
通过学习元素节点的diff操作,我们不妨大胆猜测一下,组件是做了如下diff操作:
- 组件不同类型或者不存在就创建,走相应的生命周期钩子
- 比较组件的属性
- 比较组件的孩子
事实上和我们的猜想很相似,在进行下一步之前,我们先了解下preact中的数据结构:
// 如下JSX
// 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四个指针,在每次循环中先处理特殊情况,并通过缩小指针范围,获得性能上的提升。