阅读 262

【Vue原理】Diff - 源码版 之 相关辅助函数

写文章不容易,点个赞呗兄弟

专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Diff - 源码版 之 相关辅助函数

在开始我们的 Diff 主要内容之前,我们先来了解下其中会用的的一些辅助函数

本来可以放到 Diff 那里写,但是全部合起来内容又太多

而且这些函数比较具有公用性,就是抽出来用

所以打算独立一篇文章,先预热一下,内容也不多,也挺简单,光看下也会对我们的思维有所帮助


节点操作函数

下面是 Diff 在比较节点时,更新DOM 会使用到的一些函数

本来会有更多,为了看得方便,我把一些合并了

下面会介绍三个函数

insert,createElm,createChildren

简单介绍

insert 作用是把节点插入到某个位置

createElm 作用是创建DOM 节点

createChildren 作用也是创建DOM 节点,但是处理的是一个数组,并且会创建 DOM 节点 和 文本节点

下面就来仔细说说这三个方法

1 insert

这个函数的作用就是 插入节点

但是插入也会分两种情况

1、没有参考兄弟节点,直接插入父节点的子节点末尾

2、有参考兄弟节点,则插在兄弟节点前面

可以说这个函数是 Diff 的基石哈哈

function insert(parent, elm, ref) {    

    if (parent) {        

        if (ref) {            

            if (ref.parentNode === parent) {

                parent.insertBefore(elm, ref);
            }
        } else {
            parent.appendChild(elm);
        }
    }
}
复制代码

2 createElm

这个函数的作用跟它的名字一样,就是创建节点的意思

创建完节点之后,会调用 insert 去插入节点

你可以看一下,不难

function createElm(vnode, parentElm, refElm) {  



    var children = vnode.children;    

    var tag = vnode.tag;    

    if (tag) {

        vnode.elm = document.createElement(tag)        



        // 先把 子节点插入 vnode.elm,然后再把 vnode.elm 插入parent

        createChildren(vnode, children);

       

        //  插入DOM 节点

        insert(parentElm, vnode.elm, refElm);
    }    



    else {

        vnode.elm = document.createTextNode(vnode.text);
        
        insert(parentElm, vnode.elm, refElm);
    }
}
复制代码

createElm 需要根据 Vnode 来判断需要创建什么节点

1、文本节点

2、普通节点

1 文本节点

当 vnode.tag 为 undefined 的时候,创建文本节点,看下 真实文本vnode

公众号

并且,文本节点是没有子节点的

2 普通节点

vnode.tag 有值,那就创建相应的 DOM

但是 该 DOM 可能存在子节点,所以子节点甚至子孙节点,也都是要创建的

所以会调用一个 createChildren 去完成所有子孙节点的创建

3 createChildren

这个方法处理子节点,必然是用遍历递归的方法逐个处理的

1如果子节点是数组,则遍历执行 createElm 逐个处理

2如果子节点的 text 属性有数据,则表示这个 vnode 是个文本节点,直接创建文本节点,然后插入到父节点中

function createChildren(vnode, children) {    



    if (Array.isArray(children)) {      



        for (var i = 0; i < children.length; ++i) {

            createElm(children[i], vnode.elm, null);
        }



    }



    else if (        

        typeof vnode.text=== 'string' ||

        typeof vnode.text=== 'number' ||
        typeof vnode.text=== 'boolean'
    ) {
        vnode.elm.appendChild(

            document.createTextNode(vnode.text)

        )

    }
}
复制代码

服务Diff工具函数

下面的函数是 Vue 专门用来服务 Diff 的,介绍两个

createKeyToOldIdx,sameVnode

1createKeyToOldIdx

接收一个 children 数组,生成 key 与 index 索引对应的一个 map 表

function createKeyToOldIdx(

    children, beginIdx, endIdx

) {    



    var i, key;    

    var map = {};    



    for (i = beginIdx; i <= endIdx; ++i) {

        key = children[i].key;        

        if (key) {

            map[key] = i;
        }
    }    

    return map

}
复制代码

比如你的旧子节点数组是

[{    
    tag:"div",  key: "key_1"

},{  

    tag:"strong", key:"key_2"

},{  

    tag:"span",  key:"key_4"

}]
复制代码

经过 createKeyToOldIdx 生成一个 map 表 oldKeyToIdx,是下面这样

{
    "key_1":0,
    "key_2":1,
    "key_4":2
}
复制代码

把 vnode 的 key 作为属性名,而该 vnode 在children 的位置 作为 属性值

这个函数在 Diff 中的作用是

判断某个新 vnode 是否在 这个旧的 Vnode 数组中,并且拿到它的位置。就是拿到 新 Vnode 的 key,然后去这个 map 表中去匹配,是否有相应的节点,有的话,就返回这个节点的位置

比如

现在我有一个 新子节点数组,一个 旧子节点数组

我拿到 新子节点数组 中的某一个 newVnode,我要判断他是否 和 旧子节点数组 中某个vnode相同

要怎么判断???难道是双重遍历数组,逐个判断 newVnode.key==vnode.key ??

Vue 用了更聪明的办法,使用 旧 Vnode 数组生成一个 map 对象 obj

当 obj[ newVnode.key ] 存在的时候,说明 新旧子节点数组都存在这个节点

并且我能拿到该节点在 旧子节点数组 中的位置(属性值)

反之,则不存在

这个方法也给我们提供了在项目中相似场景的一个解决思路,以对象索引查找替代数组遍历

希望大家记住哦

2 sameVnode

这个函数在 Diff 中也起到非常大的作用,大家务必要记住啊

它的作用是判断两个节点是否相同

这里说的相同,并不是完全一毛一样,而是关键属性一样,可以先看下源码

function sameVnode(a, b) {    



    return (

        a.key === b.key &&
        a.tag === b.tag &&
        !!a.data === !!b.data &&
        sameInputType(a, b)
    )
}



function sameInputType(a, b) {    



    if (a.tag !== 'input') return true

    var i;    

    var types = [

        'text','number','password',

        'search','email','tel','url'

    ]    



    var typeA = (i = a.data) && (i = i.attrs) && i.type;    

    var typeB = (i = b.data) && (i = i.attrs) && i.type;    

    

    // input 的类型一样,或者都属于基本input类型

    return (
        typeA === typeB ||
        types.indexOf(typeA)>-1 &&

        types.indexOf(typeB)>-1

    )
}
复制代码

判断的依据主要是 三点,key,tag,是否存在 data

这里判断的节点是只是相对于 节点本身,并不包括 children 在内

也就是说,就算data不一样,children 不一样,两个节点还是可能一样

比如下面这两个

公众号

公众号

有一种特殊情况,就是 input 节点

input 需要额外判断, 两个节点的 type 是否相同

或者

两个节点的类型可以不同,但是必须属于那些 input 类型

sameVnode 的内容就到这里了,但是我不禁又开始思考一个问题

为什么 sameVnode 会这么判断??

下面纯属个人意淫想法,仅供参考

sameVnode 应用在 Diff ,作用是为了判断节点是否需要新建

当两个 新旧vnode 进行 sameVnode 得到 false 的时候,说明两个vnode 不一样,会新建DOM插入

也就是两个节点从根本上不一样时才会创建

其中会比较 唯一标识符 key 和 标签名 tag,从而得到 vnode 是否一样 ,这些是毫无疑问的了

但是这里不需要判断 data 是否一样,我开始不太明白

后面想到 data 是包含有一些 dom 上的属性的,所以 data 不一样没有关系

因为就算不一样,他们还是基于同一个 DOM

因为DOM属性的值是可能是动态绑定动态更新变化的,所以变化前后的 两个 vnode,相应的 data 肯定不一样,但是其实他们是同一个 Vnode,所以 data 不在判断范畴

但是 data 在新旧节点中,必须都定义,或者都不定义

不存在 一个定义,而一个没定义, 但是会相同的 Vnode

比如,下面这个就会存在data

公众号

这个就不会存在data

公众号

他们在模板中,肯定是不属于同一个节点


总结

涉及的函数主要分为两类

一类是专门负责操作 DOM 的,insert,createElm,createChildren

这类函数比较通用,就算在我们自己的项目中也可以用得上

一类是专门特殊服务 Diff 的,createKeyToOldIdx,sameVnode

其中会包含一些项目的解决思路

大家务必先记住一下这几个函数,在下篇内容的源码中会频繁出现

到时不会仔细介绍


最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢

公众号

关注下面的标签,发现更多相似文章
评论