资讯专栏INFORMATION COLUMN

帮你读懂preact源码(二)

Warren / 2511人阅读

摘要:最后删除新的树中不存在的节点。而中会记录对其做了相应的优化,节点的的情况下,不做移动操作。这种情况,在中得到了优化,通过四个指针,在每次循环中先处理特殊情况,并通过缩小指针范围,获得性能上的提升。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95134.html

相关文章

  • 帮你读懂preact源码(三)

    摘要:对回收的处理在中,回收调用了两个方法,节点的回收一般会调用,组件的回收会调用。个人理解从以上源码阅读中我们可以看到,最大的性能问题在于递归的,中的与也是为了缓解这个问题。为不同类型的更新分配优先级。 对回收的处理 在preact中,回收调用了两个方法,dom节点的回收一般会调用recollectNodeTree,组件的回收会调用unmountComponent。 preact复用dom...

    yuanxin 评论0 收藏0
  • 帮你读懂preact源码(一)

    摘要:是一个最小的库,但由于其对尺寸的追求,它的很多代码可读性比较差,市面上也很少有全面且详细介绍的文章,本篇文章希望能帮助你学习的源码。建议与源码一起阅读本文。 作为一名前端,我们需要深入学习react的运行机制,但是react源码量已经相当庞大,从学习的角度,性价比不高,所以学习一个react mini库是一个深入学习react的一个不错的方法。 preact是一个最小的react mi...

    XboxYan 评论0 收藏0
  • 少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别

    摘要:的选择器允许单个线程监视多个输入通道。一旦执行的线程已经超过读取代码中的某个数据片段,该线程就不会在数据中向后移动通常不会。 1、引言 很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典IO(也就是常说的阻塞式IO)的API时,很快就会发现一个问题:我什么时候应该使用经典IO,什么时候应该使用NIO? 在本文中,将尝试用简明扼要的文字,阐明Java NIO和经典IO之...

    Meils 评论0 收藏0
  • 源码入手,一文带你读懂Spring AOP面向切面编程

    摘要:,,面向切面编程。,切点,切面匹配连接点的点,一般与切点表达式相关,就是切面如何切点。例子中,注解就是切点表达式,匹配对应的连接点,通知,指在切面的某个特定的连接点上执行的动作。,织入,将作用在的过程。因为源码都是英文写的。 之前《零基础带你看Spring源码——IOC控制反转》详细讲了Spring容器的初始化和加载的原理,后面《你真的完全了解Java动态代理吗?看这篇就够了》介绍了下...

    wawor4827 评论0 收藏0
  • 数据结构与算法():带你读懂冒泡排序(Bubble Sorting)

    摘要:经过一次冒泡排序,最终在无序表中找到一个最大值,第一次冒泡结束。也是我们后面要说的冒泡排序的优化。冒泡排序只执行第一层循环,而不会执行第二层循环。因此冒泡排序的时间复杂度是。 showImg(https://user-gold-cdn.xitu.io/2019/6/19/16b6f986df6880f9?w=640&h=142&f=gif&s=17175);showImg(https:...

    chuyao 评论0 收藏0

发表评论

0条评论

Warren

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<