背景:

  • DOM操作非常耗耗费性能
  • 使用jQuery, 可以自行控制DOM操作的时机, 手动调整
  • Vue和React是数据驱动视图, 如何有效控制DOM操作?

解决:

  • 有一定复杂度减少计算次数(较难)。
  • 把计算转化为JS计算(JS执行速度快)。
  • vdom - 用JS模拟DOM结构, 计算出最小变更, 再操作DOM
    1. <div id="app" class="container">
    2. <p>title</p>
    3. <ul style="font-size: 20px">
    4. <li>apple</li>
    5. </ul>
    6. </div>
    7. {
    8. tag: 'div',
    9. props: {
    10. className: 'container',
    11. id: 'app'
    12. },
    13. children: [
    14. {
    15. tag: 'p',
    16. children:: 'title',
    17. },
    18. {
    19. tag: 'ul',
    20. props: { style: 'font-size: 20px' },
    21. children: [
    22. { tag: 'li', children: 'apple' }
    23. ]
    24. }
    25. ]
    26. }

实现步骤:

  • 用JS模拟DOM结构(vnode)。所有的XML语言可以用法JSON表示。class是es6中的关键词, 用className代替。
  • 新旧vnode对比,得出最小的更新范围, 最后更新BDOM
  • 数据驱动视图的模式下, 有效控制DOM操作

    vnode(snabbdom)

    考点: vnode实现html代码片段。

  • h 函数, h 函数主要根据传进来的参数,返回一个 vnode 对象

  • vnode 对象,用来表示相应的 dom 结构
  • patch (补丁)方法 (snabbdom 的核心)

    虚拟DOM-diff算法概述

    diff 算法,算法的基本步骤如下:

  • 用 js 对象来描述 dom 树结构,然后用这个 js 对象来创建一棵真正的 dom 树,插入到文档中

  • 当状态更新时,将新的 js 对象和旧的 js 对象进行比较,得到两个对象之间的差异
  • 将差异应用到真正的 dom 上

树diff的时间复杂度O(n^3)。遍历tree1、遍历tree2、排序,分别是n。1000个节点, 计算1亿次, 算法不可用。
优化时间复杂到O(n):

  • 只比较同一层级, 不跨级比较
  • tag不相同, 则直接删除重建, 不再深度比较
  • tag和key, 两者相同, 则认为是相同节点, 不再深度比较

    snabbdom源码分析

    官网的🌰, 先init, 使用h创建vnode,patch(container, vnode) ; 使用h创建newVnode,patch(vnode, newVnode)

    生成vnode**
    1. export interface VNode {
    2. sel: string | undefined;
    3. data: VNodeData | undefined;
    4. children: Array<VNode | string> | undefined;
    5. elm: Node | undefined;
    6. text: string | undefined;
    7. key: Key | undefined;
    8. }
    9. export function h(sel: any, b?: any, c?: any): VNode
    首先会对参数进行格式化,然后对 children 属性做处理,将可能不是 vnode 的项转成 vnode,如果是 svg 元素,会做一个特殊处理,最后返回一个 vnode 对象。

    patch函数**
    1. patch(oldVnode: VNode | Element, vnode: VNode): VNode
  1. 如果不是vnode是element, 则转为空vnode
  2. 对比两个vnode
  • 如果sameVnode 相同时 (sel 和 key相同) 调用 patchVnode
  • 否则, 更新视图。依据变化key分别进行创建、删除


patchVnode函数**
对传入的两个 vnodediff,如果存在更新,将其反馈到 dom

  1. patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue)

首先调用 vnode 上的 prepatch hook,如果当前的两个 vnode 完全相同,直接返回。接着调用 modulevnode 上的 update hook。然后会分为以下几种情况做处理:

  • 均存在 children 且不相同,调用 updateChildren
  • vnode 存在 children,旧 vnode 不存在 children,如果旧 vnode 存在 text 先清空,然后调用 addVnodes
  • vnode 不存在 children,旧 vnode 存在 children,调用 removeVnodes 移除 children
  • 均不存在 children,新 vnode 不存在 text,移除旧 vnodetext
  • 均存在 text,更新 text

最后调用 postpatch hook

updateCildren函数**

  1. updateChildren(parentElm: Node, oldCh: Array<VNode>,
  2. newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue)

整个过程简单来说,对两个数组进行对比,找到相同的部分进行复用,并更新。