帮你读懂preact源码(二)


上篇文章已经介绍过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四个指针,在每次循环中先处理特殊情况,并通过缩小指针范围,获得性能上的提升。


发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>