深入浅出--虚拟DOM

虚拟DOM

Vue.js2.0引入了虚拟dom,比1.0的初始渲染速度提升了2-4倍,并大大降低了内存消耗。
dom操作越来越频繁,代码中多数都在操作dom,状态难以管理 => 代码应该集中于状态管理,dom操作交给框架去实现 => 状态改变,重新渲染,只更新与这个状态相关的dom节点,angular脏检查-react虚拟dom-vue1.0细粒度绑定。

虚拟dom的解决方式是通过状态生成一个虚拟节点树, 渲染前会用新的虚拟dom树和上一次的进行比对,只渲染不同的部分。

vue1.0通过细粒度绑定来更新,每一个绑定都有一个对应的watcher来观察状态的变化,产生内存开销和依赖追踪的开销,对大型项目来说,这个开销是非常大的。
vue2.0开始选择中等粒度的解决方案,引入虚拟dom,组件级别是一个watcher实例。即便一个组件中有10个节点使用了某个状态,但其实也只有一个watcher,状态发生变化时,通知到组件,然后组件内部再通过虚拟dom去进行对比和渲染。** vue能够随意调整绑定的粒度,本质上还要归功于变化侦测。**

模板 -> 渲染函数 -> vnode -> patch -> 视图
patch为新旧节点树比对,判断出哪些节点发生了变化,从而只对变化的节点进行更新操作。

VNode

VNode类可以实例化不同类型的vnode实例,表示不同类型的dom元素。使用vnode创建真实dom,再插入到页面中渲染视图。

    export default class VNode {
        constructor (tag, data, children, text, elm, context, componentOptions, asynFactory) {
            this.tag = tag
            this.data = data
            this.children = children
            this.text = text
            this.elm = elm
            this.ns = undefined
            this.context = context
             ...
        }
    }

作用在于,可以将上一次渲染视图时的vnode缓存起来,之后每当需要重新渲染视图时,将新创建的vnode和上一次缓存的vnode进行比对。

注释节点

    export const createEmptyVNode = text => {
        const node = new VNode()
        node.text = text
        node.isComment = true
        // 其余属性全是默认的undefined或者false
    }

文本节点

    export function createTextVNode (val) {
        return new VNode(undefined, undefined, undefined, String(val))
    }

克隆节点

将现有节点的属性复制到新节点中,优化静态节点和插槽节点。
eg: 组件因状态变化需要重新渲染时,静态节点不会改变,所以除了首次渲染需要执行渲染函数获取vnode之外,后续都创建克隆节点进行渲染,不用重新执行渲染函数生成新的vnode。
克隆节点的isCloned属性值为true,被克隆的原始节点isCloned为false。

    export function cloneVNode (vnode, deep) {
        const cloned = new VNode(
            vnode.tag,
            vnode.data,
            vnode.children,
            vnode.text,
            vnode.elm,
            vnode.context,
            vnode.componentOptions,
            vnode.asyncFactory  
        )
        cloned.ns = vnode.ns
        cloned.isStatic = vnode.isStatic
        cloned.key = vnode.key
        cloned.isComment = vnode.isComment
        cloned.isCloned = true
        if (deep && vnode.children) {
            cloned.children = cloneVNodes(vnode.children)
        }
        return cloned
    }

元素节点

    {
        // 通常存在4种有效属性
        children: [VNode, VNode],
        context: {...},
        data: {...},
        tag: "p",
        ...
    }

组件节点

    {
        componentInstance: {...},
        componentOptions: {...},
        context: {...},
        data: {...},
        tag: "vue-component-1-child"
    }

函数式组件

    {
        functionalContext: {...},
        functionalOptions: {...},
        context: {...},
        data: {...},
        tag: "div"
    }

patch

虚拟dom最核心的部分是patch,可以将vnode渲染成真实的dom。渲染时,不是直接暴力覆盖原有dom,而是通过比对新旧虚拟节点树,找出需要更新的节点操作dom更新。用js运算成本来替换dom操作的成本。

patch的目的是修改dom节点渲染视图,对比新旧vnode差异只是手段。

oldNode是否存在? -> 不存在,使用vnode创建节点并插入视图
-> 存在 -> oldNode和vnode是否是同一个节点
-> 是,使用patchVnode进行详细的对比和更新操作
否,使用vnode创建真实节点并插入到视图中旧节点旁边,删除视图中旧节点

创建节点

对于每一个节点,先判断节点类型,如果有tag属性,则是元素节点,调用当前环境下的createElement方法创建;如果isComment为true,则是注释节点,调用createComment方法创建;如果为false,则为文本节点,调用createTextNode创建。如果其有子节点,则需要递归创建子节点,等到所有子节点都创建完,再将其用appendChild方法插入到其父节点中。

删除节点

    // 删除一组指定节点
    function removeVnodes (vnodes, startIdx, endIdx) {
        for (; startIdx <= endIdx; ++startIdx) {
            const ch = vnodes[startIdx]
            if (isDef(ch)) {
                removeNode(ch.elm)
            }
        }
    }
    // 删除单个节点
    function removeNode(el) {
        const parent = nodeOps.parentNode(el)
        if (isDef(parent)) {
            nodeOps.removeChild(parent, el)    
        }
    }
    // 把removeChild封在nodeOps中,是为了把框架更新dom节点的操作封装,实现**跨平台渲染**,不用平台下调用节点操作。
    const nodeOps = {
          removeChild(node, child) {
              node.removeChild(child)
          }
    }

更新节点

以vnode为准来更新视图

静态节点

如果新旧节点都为静态节点,无论状态怎么变化都不会该改变,则不需要重新渲染,跳过更新节点的过程。

vnode有文本属性

如果oldVnode也有文本属性,且与vnode一致,则不要改动。其余情况,无论oldVNode怎样,都调用setTextContent方法,更新视图中dom内容为text属性值。

vnode无文本属性

  1. 有children
    如果oldVNode有children,则需要进一步详细比较其区别;
    如果oldVNode没有children,那不是空节点就是文本节点,如果是文本节点,就清空文本,然后遍历vnode的children创建真实子dom,并插入到视图中。
  2. 无children
    说明vnode是一个空节点,所以oldVNode中有什么就删什么,使真实dom变成一个空节点。

更新子节点

当vnode和oldVNode的子节点都存在且不同时,需要更新子节点。大概分为4种操作: 更新节点,新增节点,删除节点和移动节点。对比两个子节点列表,需要遍历newChildren,对于每一个child,遍历oldChildren查找,如果找不到,说明是新增节点,如果找到了,就更新节点,如果找到的oldChild和newChild位置不同,则需要移动节点。

更新策略

1.新增节点:创建新节点,并插入到oldChildren中所有未处理节点的前面。
为啥是所有未处理节点的前面?
因为我们是用新旧节点树进行对比,而不是真实dom树。如果,新旧节点树都已经处理了两个节点,新树的第三个节点,旧树中没有,此时需要在真实dom树中新增这个节点,而旧树中只有两个已处理和两个未处理。当比对第四个节点也需要新增时,如果添加在所有已处理的后面,将添加在第3位,位置错误。
2.更新子节点:当一个节点同时存在于newChildren和oldChildren中,且位置相同,只需要更新children和文本即可。
3.移动子节点:当一个节点同时存在于newChildren和oldChildren中,但位置不同,需要在视图中将此节点移动到所有未处理节点的最前面。节点更新并且完成移动后,开始进行下一轮循环,处理下一个未处理的节点。
4.删除子节点:当遍历完newChildren中所有节点时,oldChildren中剩余的未处理节点就是被废弃需要删除的节点。

优化策略

利用四种快捷查找方式,如果找到则直接更新和移动位置,如果没有找到,再进行遍历查找。
新前: newChildren中所有未处理的第一个节点
新后: newChildren中所有未处理的最后一个节点
旧前: oldChildren中所有未处理的第一个节点
旧后: oldChildren中所有未处理的最后一个节点

  1. 新前与旧前
    比对新前与旧前两个节点,如果是同一个节点,则直接更新节点并更新视图;如果不是,则尝试比对新后与旧后。
  2. 新后与旧后
    比对新后与旧后两个节点,如果是同一个节点,则直接更新节点并更新视图;如果不是,则尝试比对新后与旧前。
  3. 新后与旧前
    比对新后与旧前两个节点,如果是同一个节点,则更新节点,然后将此节点移动到oldChildren中所有未处理节点的最后面;如果不是,则尝试比对新前与旧后。
  4. 新前与旧后
    比对新前与旧后两个节点,如果是同一个节点,则更新节点,然后将此节点移动到oldChildren中所有未处理节点的最前面;如果不是,则遍历oldChildren查找。

未处理节点

被遍历过的都是处理过的节点,还没遍历到的就是未处理节点。但是由于优化策略,节点可能会有从后面比对的,所以不再只是处理所有未处理节点的第一个也可能是最后一个,所以遍历应该从两头向中间。
oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。start被处理过,就后移一位;end被处理过,就前移一位;old和new一起移动节点。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 4种快速查找
    // 遍历查找
}
while (oldStartIdx <= oldEndIdx) {
    // oldChildren中还有剩余节点,是需要被废弃的,可以遍历删除
}
while (newStartIdx <= newEndIdx) {
    // newChildren中还有剩余节点,是新增节点,需要遍历添加
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342