1 什么是虚拟 DOM

Virtual Dom(虚拟DOM)是由普通的 JS 对象来描述 DOM 对象,创建虚拟 DOM的开销比真实DOM 的开销小很多

2 为什么要使用虚拟 DOM

  • 前端开发旧的时代
  • MVVM 框架解决视图和状态同步的问题
  • 模板引擎可以简化视图操作,没办法跟踪状态
  • 虚拟DOM 跟踪状态变化,用 diff 算法来有效更新 DOM
  • 参考 github 上 virtual-dom 的动机描述

    • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次的 DOM 差异更新真实 DOM

      3 虚拟 DOM 的作用和虚拟 DOM 库

      虚拟 DOM 的作用
  • 维护视图和状态的关系

  • 复杂视图情况下提升渲染性能
  • 跨平台
    • 浏览器平台渲染 DOM
    • 服务端渲染 SSR (nust.js/next.js)
    • 原生应用 (weex/React Native)
    • 小程序(mpvue/uni-app)等

虚拟 DOM 库

  • Snabbdom
    • Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
    • 大约 200 SLOC(single line of code)
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual Dom 之一
  • virtual-dom

    4 Snabbdom 的基本使用

  1. 安装 parcel

初始化package.json yarn init —yes
安装parcel yarn add parcel-bundler -D

  1. 配置 scripts

package.json 的scripts 中添加两条命令

  1. ...
  2. "scripts": {
  3. "dev": "parcel index.html --open",
  4. "build": "parcel build index.html"
  5. },
  6. ...
  1. 目录结构

根目录新建 index.html,src 目录,src下新建js

  1. 安装 snabbdom

yarn add snabbdom —save

  1. js 中引入 snabbdom ```javascript // nodejs 12以上的版本才支持 package.json 中的export webpack 5支持 // import { init } from ‘snabbdom/init’ // import { init } from ‘snabbdom/h’ // 改为实际路径 import { init } from ‘snabbdom/build/package/init’ import { h } from ‘snabbdom/build/package/h’;

const path = init([])

  1. <a name="zf56X"></a>
  2. #### 5 snabbdom 中 init/h/path 的使用
  3. ```javascript
  4. import { init } from 'snabbdom/build/package/init'
  5. import { h } from 'snabbdom/build/package/h';
  6. const patch = init([])
  7. // h 函数
  8. // 第一个参数:标签加选择器
  9. // 第二个参数:如果是字符串就是标签中的文本内容
  10. let vnode = h('div#container.cls','hello world')
  11. // 获取挂载点
  12. let app = document.querySelector('#app')
  13. // 对比两个vnode,比较两个vnode 的差异,更新真实dom
  14. // 第一个参数:旧VNODE,可以是 dom 元素
  15. // 第二个参数:新的VNODE,
  16. // 返回新的 VNODE
  17. let oldVNode = patch(app, vnode)
  18. vnode = h('div#container.container','hello there')
  19. patch(oldVNode, vnode)

6 snabbdom 中嵌套节点、异步更新视图及清除视图

  1. import { init } from 'snabbdom/build/package/init'
  2. import { h } from 'snabbdom/build/package/h';
  3. const patch = init([])
  4. // 通过数组中h函数调用嵌套视图
  5. let vnode = h('div#container.cls',[
  6. h('h1','hello world'),
  7. h('p', '这是一个p')
  8. ])
  9. let app = document.querySelector('#app')
  10. let oldVNode = patch(app, vnode)
  11. setTimeout(()=>{
  12. // vnode = h('div#container.container',[
  13. // h('h1','hello there'),
  14. // h('p', 'hello p')
  15. // ])
  16. // patch(oldVNode, vnode)
  17. // 清除div 中的内容
  18. patch(oldVNode, h('!'))
  19. },2000)

7 模块的使用

  • 模块的作用
    • Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现
    • Snabbdom 中的模块可以用来扩展 Snbbdom 的功能
    • Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
  • 官方提供的模块
    • attributes 设置 DOM 的属性,通过attributes 来设置顺序,判断 Boolean 类型的属性
    • props 设置DOM 的属性,通过对象名-属性的方式设置属性的,不会判断 Boolean 类型的属性
    • dataset 处理h5 中 data-attr 类属性
    • class 切换类样式
    • style 设置行内样式
    • eventlisteners 处理事件的
  • 模块的使用步骤
    • 导入需要的模块
    • init() 中注册模块
    • h() 函数的第二个参数处,处理模块,通过第二个参数设置为对象来设置 ```javascript import { init } from ‘snabbdom/build/package/init’ import { h } from ‘snabbdom/build/package/h’;

// 1.导入模块 import { styleModule } from ‘snabbdom/build/package/modules/style’; import { eventListenersModule } from ‘snabbdom/build/package/modules/eventlisteners’; // 2.注册模块 const patch = init([ styleModule, eventListenersModule ]) // 3.使用 h 函数的第二个参数传入模块中使用的数据(对象) let vnode = h(‘div’, [ h(‘h1’, { style: { backgroundColor: ‘red’ } }, ‘hello world’), h(‘p’, { on: { click: eventHandler } }, ‘hello P’) ])

function eventHandler() { console.log(‘点我穴?’) }

let app = document.querySelector(‘#app’)

patch(app, vnode)

  1. <a name="gk55P"></a>
  2. #### 8 阅读 Snabbdom 源码
  3. 如何阅读源码
  4. - 宏观了解
  5. - 带着目标看源码
  6. - 看源码的过程要不求甚解
  7. - 调试
  8. - 参考资料
  9. Snabbdom 的核心
  10. - init() 设置模块,创建 patch() 函数
  11. - 使用 h() 函数创建 JavaScript 对象(vnode)描述真实dom
  12. - patch() 比较新旧两个vnode
  13. - 把变化的内容更新到真实的 DOM 树上
  14. <a name="u1ZAT"></a>
  15. #### 9 h 函数
  16. - 作用: 创建 vnode 对象
  17. - vue 中的h 函数
  18. - h 函数最早见于 hyperscript,使用 JavaScript 创建文本
  19. 函数重载
  20. - 参数个数或者参数类型不同的函数
  21. - JavaScript 中没有重载的概念
  22. - TypeScript 中有重载,不过重载的实现是通过代码调整参数
  23. ```javascript
  24. ...
  25. function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  26. ...
  27. }
  28. // h 函数的重载
  29. export function h (sel: string): VNode
  30. export function h (sel: string, data: VNodeData | null): VNode
  31. export function h (sel: string, children: VNodeChildren): VNode
  32. export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
  33. export function h (sel: any, b?: any, c?: any): VNode {
  34. var data: VNodeData = {}
  35. var children: any
  36. var text: any
  37. var i: number
  38. // 处理参数实现重载机制
  39. if (c !== undefined) {
  40. // 三个参数的情况
  41. // sel data children/text
  42. if (b !== null) {
  43. data = b
  44. }
  45. // 如果c 是 数组
  46. if (is.array(c)) {
  47. children = c
  48. // 如果c 是字符串或者数字
  49. } else if (is.primitive(c)) {
  50. text = c
  51. // 如果c 是 vnode
  52. } else if (c && c.sel) {
  53. children = [c]
  54. }
  55. } else if (b !== undefined && b !== null) {
  56. // 两个参数的情况
  57. if (is.array(b)) {
  58. children = b
  59. } else if (is.primitive(b)) {
  60. text = b
  61. } else if (b && b.sel) {
  62. children = [b]
  63. } else { data = b }
  64. }
  65. if (children !== undefined) {
  66. // 判断 children 是否有值
  67. for (i = 0; i < children.length; ++i) {
  68. // 如果是字符串或者数字,创建文本节点
  69. if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
  70. }
  71. }
  72. if (
  73. sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
  74. (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  75. ) {
  76. // 如果是 SVG 添加命名空间
  77. addNS(data, children, sel)
  78. }
  79. // 返回 vnode
  80. return vnode(sel, data, children, text, undefined)
  81. };

10 vnode

vnode 中,引入了一些模块,定义了 VNode 接口和 VNodeData 接口,然后导出了一个 vnode 函数,函数中通过data.key 获取key,然后导出了一个vnode 对象

11 patch 整体过程分析

  • patch(oldNode, newNode)
  • 把新节点中变化的内容渲染到真实的DOM,最后返回新节点作为下次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的key和sel)相同
  • 如果不是相同节点,删除之前的内容重新渲染
  • 如果是相同节点,判断新的 VNode 是否有 text ,如果有并且和 oldNode 的 text 不同,直接更新文本内容
  • 如果新的 VNode 有children,判断子节点是否变化

    12 init 函数

  • init 函数接受两个参数,模块和api,api的作用是把vnode 转换成其他平台的元素,不传的时候默认浏览器环境下的html DOM 操作 api

  • cbs 对象,对象的键是方法上面hooks 数组的成员,然后遍历模块数组,把每个模块中的 hooks 成员存到 cbs 对象的对应数组中
  • 之后定义了一些内部方法,最后返回一个patch(oldNode,newNode) 函数,此处用到高阶函数,通过init 函数初始化 modules和domapi,然后再后续调用patch 的时候内存中就会有了这个两个成员,直接传oldNode 和 newNode 就可以了

    1. export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
    2. let i: number
    3. let j: number
    4. const cbs: ModuleHooks = {
    5. create: [],
    6. update: [],
    7. remove: [],
    8. destroy: [],
    9. pre: [],
    10. post: []
    11. }
    12. const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
    13. for (i = 0; i < hooks.length; ++i) {
    14. cbs[hooks[i]] = []
    15. for (j = 0; j < modules.length; ++j) {
    16. const hook = modules[j][hooks[i]]
    17. if (hook !== undefined) {
    18. (cbs[hooks[i]] as any[]).push(hook)
    19. }
    20. }
    21. }
    22. function emptyNodeAt ...
    23. function createRmCb ...
    24. function createElm ...
    25. function addVnodes ...
    26. function invokeDestroyHook ...
    27. function removeVnodes ...
    28. function updateChildren ...
    29. function patchVnode ...
    30. return function patch (oldVnode: VNode | Element, vnode: VNode)...
    31. }

    13 patch 函数

  • patch 函数接收两个参数,oldNode|elemet 和 newNode

  • 函数内部定义了几个内部成员和新插入节点的队列数组,队列的目的是为了后续触发节点上用户定义的钩子函数
  • 预处理,遍历 init 函数中定义的 cbs 对象中 pre 属性对应的钩子数组,并且执行
  • 判断oldnode 是否是 vnode 对象,不是的话将dom对象转换为vnode 对象
  • 判断新旧node 是否相同(key和sel)相同,如果是相同的则对比内容是否有变化,然后将变化的内容更新到dom;如果不是相同的,则获取父节点,并且调用createElm 方法生成新的dom挂载到新的节点elm上面,然后把新节点插入到父节点下的旧节点的兄弟元素位置,移除旧的节点
  • 然后遍历插入节点的数组,调用 节点的data下的hook下的insert方法
  • 最后遍历cbs 的post 属性数组,调用post 对应数组的钩子

    1. ...
    2. return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    3. let i: number, elm: Node, parent: Node
    4. const insertedVnodeQueue: VNodeQueue = []
    5. // 执行pre 中的钩子函数
    6. for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    7. // 判断是否vnode节点
    8. if (!isVnode(oldVnode)) {
    9. // dom 转换为vnode 节点
    10. oldVnode = emptyNodeAt(oldVnode)
    11. }
    12. //判断是否相同的vnode key/sel 相等
    13. if (sameVnode(oldVnode, vnode)) {
    14. // 更新变化的内容
    15. patchVnode(oldVnode, vnode, insertedVnodeQueue)
    16. } else {
    17. elm = oldVnode.elm!
    18. parent = api.parentNode(elm) as Node // 父节点
    19. // 创建真实dom
    20. createElm(vnode, insertedVnodeQueue)
    21. // 新节点插入dom 树并且移除旧节点
    22. if (parent !== null) {
    23. // 将新节点的dom 插入到父节点中、旧节点后
    24. api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
    25. // 移除旧节点
    26. removeVnodes(parent, [oldVnode], 0, 0)
    27. }
    28. }
    29. // 遍历新节点队列,调用data中hook中的insert方法
    30. for (i = 0; i < insertedVnodeQueue.length; ++i) {
    31. insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    32. }
    33. // 调用 post 数组中的钩子函数
    34. for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    35. // 返回新节点作为下次更新的旧节点
    36. return vnode
    37. }

    14 createElm 函数

  • 执行用户设置的init钩子函数

  • 把vnode 转换成真实的的 dom 对象
  • 返回创建的dom

    1. function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    2. // 执行用户设置的init钩子函数
    3. let i: any
    4. let data = vnode.data
    5. if (data !== undefined) {
    6. const init = data.hook?.init
    7. if (isDef(init)) {
    8. init(vnode)
    9. data = vnode.data
    10. }
    11. }
    12. const children = vnode.children
    13. // 把vnode 转换成真实的 dom 对象
    14. const sel = vnode.sel
    15. if (sel === '!') {
    16. // 注释节点
    17. if (isUndef(vnode.text)) {
    18. vnode.text = ''
    19. }
    20. // 挂载空节点到vnode的elm上
    21. vnode.elm = api.createComment(vnode.text!)
    22. } else if (sel !== undefined) {
    23. // 解析选择器
    24. // Parse selector
    25. const hashIdx = sel.indexOf('#')
    26. const dotIdx = sel.indexOf('.', hashIdx)
    27. const hash = hashIdx > 0 ? hashIdx : sel.length
    28. const dot = dotIdx > 0 ? dotIdx : sel.length
    29. const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
    30. // 判断是否有命名空间
    31. const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
    32. ? api.createElementNS(i, tag)
    33. : api.createElement(tag)
    34. // 添加id和class
    35. if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
    36. if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
    37. for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
    38. // 如果vnode 中有子节点,创建vnode 对应的 DOM 元素并追加到 DOM 树上
    39. if (is.array(children)) {
    40. for (i = 0; i < children.length; ++i) {
    41. const ch = children[i]
    42. if (ch != null) {
    43. // 递归调用
    44. api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
    45. }
    46. }
    47. } else if (is.primitive(vnode.text)) {
    48. // text 类型是字符串或则数字则直接插入到文本节点
    49. api.appendChild(elm, api.createTextNode(vnode.text))
    50. }
    51. const hook = vnode.data!.hook
    52. if (isDef(hook)) {
    53. hook.create?.(emptyNode, vnode)
    54. if (hook.insert) {
    55. insertedVnodeQueue.push(vnode)
    56. }
    57. }
    58. } else {
    59. // 选择器为空则创造文本节点
    60. vnode.elm = api.createTextNode(vnode.text!)
    61. }
    62. // 返回新创建的dom
    63. return vnode.elm
    64. }

    15 removeVnodes 和 addVnodes

    removeVnodes

  • 接受4个参数:父节点、要删除的节点数组、删除开始的下标、结束的下标

  • 从开始下标到结束下标遍历传入的节点数组,判断节点是否为null,为null 时为文本节点,直接删除,不为null 则判断sel 是否定义
  • 如果有 sel ,执行节点 data 属性的hook 中的destroy 方法,然后遍历 cbs 数组中 destroy 属性的数组,并执行其中的钩子函数,有children 则递归执行
  • 通过 createRmCb 高阶函数,返回真实删除dom 的函数,并在函数内部记录 cbs 中的remove 钩子函数数量,然后赋值给rm 函数
  • 遍历 cbs 中remove 数组,执行钩子函数
  • 判断data.hook.remove 是否定义,如果定义了则把节点和rm函数传入并调用,否则直接调用rm 函数

    1. function removeVnodes (parentElm: Node,
    2. vnodes: VNode[],
    3. startIdx: number,
    4. endIdx: number): void {
    5. for (; startIdx <= endIdx; ++startIdx) {
    6. let listeners: number
    7. let rm: () => void
    8. const ch = vnodes[startIdx]
    9. if (ch != null) {
    10. if (isDef(ch.sel)) {
    11. invokeDestroyHook(ch)
    12. listeners = cbs.remove.length + 1
    13. rm = createRmCb(ch.elm!, listeners)
    14. for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
    15. const removeHook = ch?.data?.hook?.remove
    16. if (isDef(removeHook)) {
    17. removeHook(ch, rm)
    18. } else {
    19. rm()
    20. }
    21. } else { // Text node
    22. api.removeChild(parentElm, ch.elm!)
    23. }
    24. }
    25. }
    26. }

    addVnodes

  • 接收6个参数:父节点、插入位置的节点、插入的节点数组、开始下标、结束下标、插入节点队列

  • 从开始下标到结束下标循环,获取到插入的节点数组中的对应节点,不为空则直接调用插入的方法

    1. function addVnodes (
    2. parentElm: Node,
    3. before: Node | null,
    4. vnodes: VNode[],
    5. startIdx: number,
    6. endIdx: number,
    7. insertedVnodeQueue: VNodeQueue
    8. ) {
    9. for (; startIdx <= endIdx; ++startIdx) {
    10. const ch = vnodes[startIdx]
    11. if (ch != null) {
    12. api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
    13. }
    14. }
    15. }

    16 patchVnode

  • 触发prepatch、update 钩子函数

  • 新节点有text 属性且不等于旧节点的 text 属性时,如果老节点有children,移除老节点的children对应的 DOM 元素,设置新节点对应 DOM 元素的 textContent
  • 新老节点都有children 且不相等时,调用 updateChildren 方法,对比子节点并且更新子节点的差异
  • 只有新节点有 children时,如果老节点有 text属性,清空对应 DOM 元素的 textContent,添加所有的子节点
  • 只有老节点有 children 时,移除所有的老节点
  • 只有老节点有 text 属性时,清空对应 DOM 元素的 textcontent
  • 触发 postpatch 钩子函数

    1. function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    2. // 第一个过程 触发 prepatch 和 update 钩子函数
    3. const hook = vnode.data?.hook
    4. hook?.prepatch?.(oldVnode, vnode)
    5. const elm = vnode.elm = oldVnode.elm!
    6. const oldCh = oldVnode.children as VNode[]
    7. const ch = vnode.children as VNode[]
    8. if (oldVnode === vnode) return
    9. if (vnode.data !== undefined) {
    10. for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    11. // 模块和用户都可以设置 update 钩子函数,用户设置的后执行是模块可能会先操作dom,之后用户再操作去覆盖
    12. vnode.data.hook?.update?.(oldVnode, vnode)
    13. }
    14. // 第二个过程:真正对比新旧 Vnode
    15. if (isUndef(vnode.text)) {
    16. // 新节点的 text 不存在
    17. if (isDef(oldCh) && isDef(ch)) {
    18. // 新旧节点都存在children
    19. // 对比新旧节点的所有子节点并更新 DOM
    20. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) // 核心代码
    21. } else if (isDef(ch)) {
    22. // 新节点的 children 存在
    23. // 老节点有 text
    24. if (isDef(oldVnode.text)) api.setTextContent(elm, '')
    25. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    26. } else if (isDef(oldCh)) {
    27. // 旧节点的 children 存在
    28. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    29. } else if (isDef(oldVnode.text)) {
    30. // 旧节点的 text 存在
    31. api.setTextContent(elm, '')
    32. }
    33. } else if (oldVnode.text !== vnode.text) {
    34. // 新旧节点的 text 不相等
    35. if (isDef(oldCh)) {
    36. // 旧节点是否有子节点
    37. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    38. }
    39. // 插入新节点
    40. api.setTextContent(elm, vnode.text!)
    41. }
    42. // 第三过程:触发 postpatch 钩子函数
    43. hook?.postpatch?.(oldVnode, vnode)
    44. }

    17 updateChildren 函数

    diff 算法

  • 虚拟 DOM 中的 Diff 算法

    • 查找两棵树中的每个节点的差异
  • Snabbdom 根据 Dom 的特点对传统的 diff 算法做了优化
    • Dom 操作时候很少会跨级别操作节点
    • 只比较同级别的节点

执行过程

  • 在对开始和结束节点比较的时候,总共有四种情况
    • oldStartVnode / newStartVnode
    • oldEndVnode / newEndVnode
      • 如果新旧开始节点是 samenode(key/sel相同)
        • 调用 patchVnode() 对比和更新节点
        • 把旧开始和新开始索引往后移动 oldStart++ / newStart++
      • 如果新旧结束节点是 samenode
        • 调用 patchVnode() 对比和更新节点
        • 把旧开始和新开始索引往前移动 oldStart— / newStart—
    • oldStartVnode / newEndVnode
      • 调用 patchVnode() 对比和更新节点
      • 把 oldStartVnode 对应的 DOM 元素,移到右边,更新索引
    • oldEndVnode / newStartVnode
      • 调用 patchVnode() 对比和更新节点
      • 把 oldEndVnode 对应的 DOM 元素,移动到左边,更新索引
  • 非上述四种情况说明开始和结束节点都不同
    • 拿新节点的开始去跟旧节点数组中的每个节点对比,判断是否samevnode
    • 如果不是则创建新的dom插入到旧节点的第一个位置
    • 如果是samevnode则递归调用,找到更新的元素更新
  • 循环结束
    • 当老节点的所有子节点先遍历完(oldStartIndex > oldEndIndex),循环结束
      • 如果老节点的数组先遍历完(oldStartIndex>oldEndIndex)
        • 说明新节点有剩余,把剩余节点批量插入到右边
    • 新节点的所有子节点先遍历完(newStartIndex > newEndIndex),循环结束
      • 如果新节点的数组先遍历完
        • 说明老节点有剩余,把剩余节点批量删除
          1. function updateChildren (parentElm: Node,
          2. oldCh: VNode[],
          3. newCh: VNode[],
          4. insertedVnodeQueue: VNodeQueue) {
          5. let oldStartIdx = 0 // 旧开始节点索引
          6. let newStartIdx = 0 // 新开始节点索引
          7. let oldEndIdx = oldCh.length - 1 // 旧节点结束索引
          8. let oldStartVnode = oldCh[0] // 旧节点开始节点
          9. let oldEndVnode = oldCh[oldEndIdx] // 旧节点结束节点
          10. let newEndIdx = newCh.length - 1 // 新节点结束索引
          11. let newStartVnode = newCh[0] // 新节点开始节点
          12. let newEndVnode = newCh[newEndIdx] // 新节点结束节点
          13. let oldKeyToIdx: KeyToIndexMap | undefined
          14. let idxInOld: number
          15. let elmToMove: VNode
          16. let before: any
          17. // 同级别节点比较
          18. // 新旧节点都没有遍历完 则开始循环
          19. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          20. // 先判断 null
          21. if (oldStartVnode == null) {
          22. oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
          23. } else if (oldEndVnode == null) {
          24. oldEndVnode = oldCh[--oldEndIdx]
          25. } else if (newStartVnode == null) {
          26. newStartVnode = newCh[++newStartIdx]
          27. } else if (newEndVnode == null) {
          28. newEndVnode = newCh[--newEndIdx]
          29. // 比较开始和结束的四种情况
          30. } else if (sameVnode(oldStartVnode, newStartVnode)) {
          31. // 旧开始等于新开始
          32. // 比较差异并更新dom
          33. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          34. // 索引指向下个节点
          35. oldStartVnode = oldCh[++oldStartIdx]
          36. newStartVnode = newCh[++newStartIdx]
          37. } else if (sameVnode(oldEndVnode, newEndVnode)) {
          38. // 旧结束等于新结束
          39. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
          40. oldEndVnode = oldCh[--oldEndIdx]
          41. newEndVnode = newCh[--newEndIdx]
          42. } else if (sameVnode(oldStartVnode, newEndVnode)) {
          43. // 旧开始等于新结束
          44. // Vnode moved right
          45. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
          46. api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
          47. oldStartVnode = oldCh[++oldStartIdx]
          48. newEndVnode = newCh[--newEndIdx]
          49. } else if (sameVnode(oldEndVnode, newStartVnode)) {
          50. // 旧结束等于新开始
          51. // Vnode moved left
          52. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
          53. api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
          54. oldEndVnode = oldCh[--oldEndIdx]
          55. newStartVnode = newCh[++newStartIdx]
          56. } else {
          57. // 开始和结尾比较结束
          58. if (oldKeyToIdx === undefined) {
          59. oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
          60. }
          61. idxInOld = oldKeyToIdx[newStartVnode.key as string]
          62. if (isUndef(idxInOld)) {
          63. // New element
          64. api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          65. } else {
          66. elmToMove = oldCh[idxInOld]
          67. if (elmToMove.sel !== newStartVnode.sel) {
          68. api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          69. } else {
          70. patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          71. oldCh[idxInOld] = undefined as any
          72. api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          73. }
          74. }
          75. newStartVnode = newCh[++newStartIdx]
          76. }
          77. }
          78. // 循环结束前的收尾工作
          79. if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
          80. if (oldStartIdx > oldEndIdx) { // 老节点数组先遍历完成,新节点数组有剩余
          81. before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm // 插入参考数组
          82. addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
          83. } else { // 新节点数组先遍历完成,老节点数组有剩余
          84. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
          85. }
          86. }
          87. }

          18 key的意义

          不添加key的时候,对于 samevnode 的节点,会最大程度的重用,此时子节点如果有某种被改变了的状态也会保留进新的节点中,导致整体的状态与预期不一致。