Snabbdom 是 Vue 使用的虚拟 DOM 库,专注提供简单、模块性的体验,以及强大的功能和性能。

虚拟 DOM

Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象。真实的 DOM 对象,成员非常多,创建这样的对象成本是非常高的。使用 Virtual DOM 来描述 DOM,虚拟 DOM 对象就是一个 JS 对象,相比之下,成员少了很多,创建这样一个对象的成本要小很多。
虚拟 DOM 可以维护程序的状态,跟踪上一次的状态,通过比较前后两次状态差异更新真实 DOM。维护状态,更新视图。

作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 跨平台
    • 浏览器平台渲染 DOM
    • 服务端渲染 SSR(Nuxt.js/Next.js)
    • 原生应用(Weex/React Nelative)
    • 小程序(mpvue/uni-app)

安装

  1. yarn add snabbdom

基本使用

snabbdom 库的三个核心函数:init,h,patch。init 函数就是 snabbdom 的初始化函数;patch 是 init 函数返回的一个函数,它把虚拟 DOM 转换成真实 DOM,并挂载到 DOM 树上;h 函数创建并返回一个 VNode 虚拟节点。

  1. import { init } from 'snabbdom/build/package/init'
  2. import { h } from 'snabbdom/build/package/h'
  3. const patch = init([])
  4. // 第一个参数:标签+选择器
  5. // 第二个参数:如果是字符串就是标签中的文本内容
  6. let vnode = h('div#container.cls', 'Hello World!')
  7. let app = document.querySelector('#app')
  8. // 第一个参数:旧的 VNode,可以是 DOM 元素
  9. // 第二个参数:新的 VNode
  10. // 返回新的 VNode
  11. let oldVNode = patch(app, vnode)
  12. vnode = h('div#container.xxx', 'Hello Snabbdom')
  13. patch(oldVnode, vnode)

h 函数可以给元素嵌套生成子节点。传入 !可以创建一个空的注释节点<!---->

  1. import { init } from 'snabbdom/build/package/init'
  2. import { h } from 'snabbdom/build/package/h'
  3. const patch = init([])
  4. let vnode = h('div#container', [
  5. h('h1', 'Hello Snabbdom'),
  6. h('p', '这是一个p')
  7. ])
  8. let app = document.querySelector('#app')
  9. let oldVnode = patch(app, vnode)
  10. setTimeout(() => {
  11. // vnode = h('div#container', [
  12. // h('h1', 'Hello World'),
  13. // h('p', 'Hello P')
  14. // ])
  15. // patch(oldVnode, vnode)
  16. // 清除div中的内容
  17. patch(oldVnode, h('!')) // ! 生成一个空的注释节点:<!---->
  18. }, 2000);

插件

  • Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现。
  • Snabbdom 中的模块可以用来扩展 Snabbdom 的功能。
  • Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的。

使用步骤

  1. 导入需要的模块
  2. init() 中注册模块
  3. h() 函数的第二个参数处使用模块
  1. import { init } from 'snabbdom/build/package/init'
  2. import { h } from 'snabbdom/build/package/h'
  3. // 使用 style 模块改变元素的样式,使用 eventListeners 为元素注册事件
  4. // 1. 导入模块
  5. import { styleModule } from 'snabbdom/build/package/modules/style'
  6. import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
  7. // 2. 注册模块
  8. const patch = init([
  9. styleModule,
  10. eventListenersModule
  11. ])
  12. // 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
  13. let vnode = h('div', [
  14. h('h1', { style: { backgroundColor: 'red' } }, 'Hello World'),
  15. h('p', { on: { click: eventHandler } }, 'Hello P')
  16. ])
  17. function eventHandler () {
  18. console.log('别点我,疼')
  19. }
  20. let app = document.querySelector('#app')
  21. patch(app, vnode)

源码分析

h 函数分析

  1. import { vnode, VNode, VNodeData } from './vnode'
  2. import * as is from './is'
  3. export type VNodes = VNode[]
  4. export type VNodeChildElement = VNode | string | number | undefined | null
  5. export type ArrayOrElement<T> = T | T[]
  6. export type VNodeChildren = ArrayOrElement<VNodeChildElement>
  7. function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  8. data.ns = 'http://www.w3.org/2000/svg'
  9. if (sel !== 'foreignObject' && children !== undefined) {
  10. for (let i = 0; i < children.length; ++i) {
  11. const childData = children[i].data
  12. if (childData !== undefined) {
  13. addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
  14. }
  15. }
  16. }
  17. }
  18. // h 函数的重载
  19. export function h (sel: string): VNode
  20. export function h (sel: string, data: VNodeData | null): VNode
  21. export function h (sel: string, children: VNodeChildren): VNode
  22. export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
  23. export function h (sel: any, b?: any, c?: any): VNode {
  24. var data: VNodeData = {}
  25. var children: any
  26. var text: any
  27. var i: number
  28. // 处理参数,实现重载的机制
  29. if (c !== undefined) {
  30. // 处理三个参数的情况
  31. // sel、data、children/text
  32. if (b !== null) {
  33. data = b
  34. }
  35. if (is.array(c)) {
  36. children = c
  37. // 如果 c 是字符串或数字
  38. } else if (is.primitive(c)) {
  39. text = c
  40. // 如果 c 是 VNode
  41. } else if (c && c.sel) {
  42. children = [c]
  43. }
  44. } else if (b !== undefined && b !== null) {
  45. // 处理两个参数的情况
  46. // 如果 b 是数组
  47. if (is.array(b)) {
  48. children = b
  49. // 如果 c 是字符串或数字
  50. } else if (is.primitive(b)) {
  51. text = b
  52. // 如果 c 是 VNode
  53. } else if (b && b.sel) {
  54. children = [b]
  55. } else { data = b }
  56. }
  57. if (children !== undefined) {
  58. // 处理 children 中的原始值(string/number)
  59. for (i = 0; i < children.length; ++i) {
  60. // 如果 child 是 string/number,创建文本节点
  61. if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
  62. }
  63. }
  64. if (
  65. sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
  66. (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  67. ) {
  68. // 如果是 svg,添加命名空间
  69. addNS(data, children, sel)
  70. }
  71. // 返回 VNode
  72. return vnode(sel, data, children, text, undefined)
  73. };

VNode

  1. import { Hooks } from './hooks'
  2. import { AttachData } from './helpers/attachto'
  3. import { VNodeStyle } from './modules/style'
  4. import { On } from './modules/eventlisteners'
  5. import { Attrs } from './modules/attributes'
  6. import { Classes } from './modules/class'
  7. import { Props } from './modules/props'
  8. import { Dataset } from './modules/dataset'
  9. import { Hero } from './modules/hero'
  10. export type Key = string | number
  11. export interface VNode {
  12. sel: string | undefined // 选择器
  13. data: VNodeData | undefined // 模块中所需的数据
  14. children: Array<VNode | string> | undefined // 与 text 互斥,描述子节点
  15. elm: Node | undefined //
  16. text: string | undefined // 与 children 互斥,记录文本节点的内容
  17. key: Key | undefined // VNode 对象的唯一标识
  18. }
  19. export interface VNodeData {
  20. props?: Props
  21. attrs?: Attrs
  22. class?: Classes
  23. style?: VNodeStyle
  24. dataset?: Dataset
  25. on?: On
  26. hero?: Hero
  27. attachData?: AttachData
  28. hook?: Hooks
  29. key?: Key
  30. ns?: string // for SVGs
  31. fn?: () => VNode // for thunks
  32. args?: any[] // for thunks
  33. [key: string]: any // for any other 3rd party module
  34. }
  35. export function vnode (sel: string | undefined,
  36. data: any | undefined,
  37. children: Array<VNode | string> | undefined,
  38. text: string | undefined,
  39. elm: Element | Text | undefined): VNode {
  40. const key = data === undefined ? undefined : data.key
  41. return { sel, data, children, text, elm, key }
  42. }

patch 分析

整体过程

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

创建

由 init() 函数返回。

  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. // 初始化 api
  13. const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
  14. // 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
  15. // 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ... ]
  16. for (i = 0; i < hooks.length; ++i) {
  17. // cbs['create'] = []
  18. cbs[hooks[i]] = []
  19. for (j = 0; j < modules.length; ++j) {
  20. // const hook = modules[0]['create']
  21. const hook = modules[j][hooks[i]]
  22. if (hook !== undefined) {
  23. (cbs[hooks[i]] as any[]).push(hook)
  24. }
  25. }
  26. }
  27. ……
  28. return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
  29. ……
  30. }
  31. }

源码

  1. return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
  2. let i: number, elm: Node, parent: Node
  3. // 保存新插入节点的队列,为了触发钩子函数
  4. const insertedVnodeQueue: VNodeQueue = []
  5. // 执行模块的 pre 钩子函数
  6. for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
  7. // 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
  8. if (!isVnode(oldVnode)) {
  9. // 把 DOM 元素转换成空的 VNode
  10. oldVnode = emptyNodeAt(oldVnode)
  11. }
  12. // 如果新旧节点是相同节点(key 和 sel 相同)
  13. if (sameVnode(oldVnode, vnode)) {
  14. // 找节点的差异并更新 DOM
  15. patchVnode(oldVnode, vnode, insertedVnodeQueue)
  16. } else {
  17. // 如果新旧节点不同,vnode 创建对应的 DOM
  18. // 获取当前的 DOM 元素
  19. elm = oldVnode.elm!
  20. parent = api.parentNode(elm) as Node
  21. // 触发 init/create 钩子函数,创建 DOM
  22. createElm(vnode, insertedVnodeQueue)
  23. if (parent !== null) {
  24. // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
  25. api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
  26. // 移除老节点
  27. removeVnodes(parent, [oldVnode], 0, 0)
  28. }
  29. }
  30. // 执行用户设置的 insert 钩子函数
  31. for (i = 0; i < insertedVnodeQueue.length; ++i) {
  32. insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
  33. }
  34. // 执行模块的 post 钩子函数
  35. for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
  36. return vnode
  37. }

createElm 函数

createElm 函数用来创建一个 DOM 对象。

  1. function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  2. let i: any
  3. let data = vnode.data
  4. if (data !== undefined) {
  5. // 执行用户设置的 init 钩子函数
  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. const sel = vnode.sel
  14. if (sel === '!') {
  15. // 如果选择器是!,创建注释节点
  16. if (isUndef(vnode.text)) {
  17. vnode.text = ''
  18. }
  19. vnode.elm = api.createComment(vnode.text!)
  20. } else if (sel !== undefined) {
  21. // 如果选择器不为空
  22. // 解析选择器
  23. // Parse selector
  24. const hashIdx = sel.indexOf('#')
  25. const dotIdx = sel.indexOf('.', hashIdx)
  26. const hash = hashIdx > 0 ? hashIdx : sel.length
  27. const dot = dotIdx > 0 ? dotIdx : sel.length
  28. const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
  29. const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
  30. ? api.createElementNS(i, tag)
  31. : api.createElement(tag)
  32. if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
  33. if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
  34. // 执行模块的 create 钩子函数
  35. for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
  36. // 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
  37. if (is.array(children)) {
  38. for (i = 0; i < children.length; ++i) {
  39. const ch = children[i]
  40. if (ch != null) {
  41. api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
  42. }
  43. }
  44. } else if (is.primitive(vnode.text)) {
  45. // 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
  46. api.appendChild(elm, api.createTextNode(vnode.text))
  47. }
  48. const hook = vnode.data!.hook
  49. if (isDef(hook)) {
  50. // 执行用户传入的钩子 create
  51. hook.create?.(emptyNode, vnode)
  52. if (hook.insert) {
  53. // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
  54. insertedVnodeQueue.push(vnode)
  55. }
  56. }
  57. } else {
  58. // 如果选择器为空,创建文本节点
  59. vnode.elm = api.createTextNode(vnode.text!)
  60. }
  61. // 返回新创建的 DOM
  62. return vnode.elm
  63. }

removeVnodes 函数和 addVnodes 函数

removeVnodes 函数用于删除 DOM 树上的元素。

  1. function removeVnodes (parentElm: Node, // 父节点
  2. vnodes: VNode[], // 要删除的元素对应的 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. // 如果节点存在
  10. if (ch != null) {
  11. if (isDef(ch.sel)) {
  12. // 元素节点
  13. // 递归触发元素和它的子元素的 destroy 钩子
  14. invokeDestroyHook(ch)
  15. listeners = cbs.remove.length + 1
  16. rm = createRmCb(ch.elm!, listeners) // 返回删除函数,当 remove 钩子全部执行后才执行这个函数
  17. for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
  18. const removeHook = ch?.data?.hook?.remove
  19. // 如果用户传入了 remove 钩子函数,执行钩子函数
  20. if (isDef(removeHook)) {
  21. removeHook(ch, rm)
  22. } else {
  23. // 如果没有传入 remove 钩子函数,直接执行 rm
  24. rm()
  25. }
  26. } else { // Text node
  27. // 文本节点
  28. api.removeChild(parentElm, ch.elm!)
  29. }
  30. }
  31. }
  32. }

addVnodes 函数用于创建 VNode 对象,并把它转成 DOM 对象,添加到 DOM 树上。

  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. }

patchVnode 函数

对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM。

过程

  • 首先执行用户设置的 prepatch 钩子函数
  • 执行 create 钩子函数
    • 首先执行模块create 钩子函数
    • 然后执行用户设置的 create 钩子函数
  • 如果 vnode.text 未定义
    • 如果 oldVnode.children 和 vnode.children 都有值
      • 调用 updateChildren()
      • 使用 diff 算法对比子节点,更新子节点
    • 如果 vnode.children 有值,oldVnode.children 无值
      • 清空 DOM 元素
      • 调用 addVnodes(),批量添加子节点
    • 如果 oldVnode.children 有值,vnode.children 无值
      • 调用 removeVnodes(),批量移除子节点
    • 如果 oldVnode.text 有值
      • 清空 DOM 元素的内容
  • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
    • 如果老节点有子节点,全部移除
    • 设置 DOM 元素的 textContent 为 vnode.text
  • 最后执行用户设置的 postpatch 钩子函数

源码

  1. function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
  2. const hook = vnode.data?.hook
  3. // 首先执行用户设置的 prepatch 钩子函数
  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. // 如果新老 vnode 相同返回
  9. if (oldVnode === vnode) return
  10. if (vnode.data !== undefined) {
  11. // 执行模块的 update 钩子函数
  12. for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  13. // 执行用户设置的 update 钩子函数
  14. vnode.data.hook?.update?.(oldVnode, vnode)
  15. }
  16. // 如果 vnode.text 未定义
  17. if (isUndef(vnode.text)) {
  18. // 如果新老节点都有 children
  19. if (isDef(oldCh) && isDef(ch)) {
  20. // 调用 updateChildren 对比子节点,更新子节点
  21. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
  22. } else if (isDef(ch)) {
  23. // 如果新节点有 children,老节点没有 children
  24. // 如果老节点有text,清空dom 元素的内容
  25. if (isDef(oldVnode.text)) api.setTextContent(elm, '')
  26. // 批量添加子节点
  27. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  28. } else if (isDef(oldCh)) {
  29. // 如果老节点有children,新节点没有children
  30. // 批量移除子节点
  31. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  32. } else if (isDef(oldVnode.text)) {
  33. // 如果老节点有 text,清空 DOM 元素
  34. api.setTextContent(elm, '')
  35. }
  36. } else if (oldVnode.text !== vnode.text) {
  37. // 如果没有设置 vnode.text
  38. if (isDef(oldCh)) {
  39. // 如果老节点有 children,移除
  40. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  41. }
  42. // 设置 DOM 元素的 textContent 为 vnode.text
  43. api.setTextContent(elm, vnode.text!)
  44. }
  45. // 最后执行用户设置的 postpatch 钩子函数
  46. hook?.postpatch?.(oldVnode, vnode)
  47. }

diff 算法

diff 算法用来查找两棵树每一个节点的差异。DOM 操作很少会跨级别操作节点,根据这个特点 Snabbdom 对传统的 diff 算法进行优化,只比较同级别的节点。
image-20200102103653779.png

在对开始节点和结束节点比较的时候,总共有四种情况:

  • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
  • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
  • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
  • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

新/旧开始节点、新旧结束节点

image-20200103121812840.png

如果 新旧开始节点是 sameNode (key 和 sel 相同)

  1. - 调用 patchNode() 对比和更新节点
  2. - 把旧开始和新开始的索引往后移动: `oldStartIdx++``newStartIdx++`

如果新旧开始节点不是 sameNode,从后面开始比较旧结束节点和新结束节点,如果新旧结束节点是 sameNode

  1. - 调用 patchNode() 对比和更新节点
  2. - 把旧结束和新开始的索引往前移动:`oldEndIdx--``newEndInx--`

旧开始节点/新结束节点

image-20200103125428541.png

如果 旧开始节点和新结束节点是 sameNode (key 和 sel 相同)

  1. - 调用 patchNode() 对比和更新节点
  2. - 把旧开始节点对应的 DOM 元素,移动到右边,更新索引:`oldStartIdx = oldEndIdx + 1``newEndIdx--`

旧结束节点/新开始节点

image-20200103125735048.png

如果 旧结束节点和新开始节点是 sameNode (key 和 sel 相同)

  1. - 调用 patchNode() 对比和更新节点
  2. - 把旧结束节点对应的 DOM 元素,移动到左边,更新索引:`oldEndIdx = 0;oldStartIdx++`

非上述四种情况

image-20200109184822439.png

  • 如果不是以上四种情况
    • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
    • 如果没有找到,说明 newStartNode 是新节点
      • 创建新节点对应的 DOM 元素,插入到 DOM 树中
    • 如果找到了
      • 判断新节点和找到的老节点的 sel 选择器是否相同
      • 如果不相同,说明节点被修改了
        • 重新创建对应的 DOM 元素,插入到 DOM 树中
      • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边

循环结束

  • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
  • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束

  • oldStartIdx > oldEndIdx

    • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)
      • 说明新节点有剩余,把剩余节点批量插入到右边
        image-20200103150918335.png
  • newStartIdx > newEndIdx
    • 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
      • 说明老节点有剩余,把剩余节点批量删除
        image-20200103151051928.png

updateChildren 函数

  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. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  18. // 索引变化后,可能会把节点设置为空
  19. if (oldStartVnode == null) {
  20. // 节点为空移动索引
  21. oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
  22. } else if (oldEndVnode == null) {
  23. oldEndVnode = oldCh[--oldEndIdx]
  24. } else if (newStartVnode == null) {
  25. newStartVnode = newCh[++newStartIdx]
  26. } else if (newEndVnode == null) {
  27. newEndVnode = newCh[--newEndIdx]
  28. // 比较开始和结束节点的四种情况
  29. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  30. // 1. 比较老开始节点和新的开始节点
  31. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  32. oldStartVnode = oldCh[++oldStartIdx]
  33. newStartVnode = newCh[++newStartIdx]
  34. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  35. // 2. 比较老结束节点和新的结束节点
  36. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  37. oldEndVnode = oldCh[--oldEndIdx]
  38. newEndVnode = newCh[--newEndIdx]
  39. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  40. // 3. 比较老开始节点和新的结束节点
  41. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  42. api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
  43. oldStartVnode = oldCh[++oldStartIdx]
  44. newEndVnode = newCh[--newEndIdx]
  45. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  46. // 4. 比较老结束节点和新的开始节点
  47. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  48. api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
  49. oldEndVnode = oldCh[--oldEndIdx]
  50. newStartVnode = newCh[++newStartIdx]
  51. } else {
  52. // 开始节点和结束节点都不相同
  53. // 使用 newStartNode 的 key 再老节点数组中找相同节点
  54. // 先设置记录 key 和 index 的对象
  55. if (oldKeyToIdx === undefined) {
  56. oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  57. }
  58. // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
  59. idxInOld = oldKeyToIdx[newStartVnode.key as string]
  60. // 如果是新的vnode
  61. if (isUndef(idxInOld)) { // New element
  62. // 如果没找到,newStartNode 是新节点
  63. // 创建元素插入 DOM 树
  64. api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
  65. } else {
  66. // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
  67. elmToMove = oldCh[idxInOld]
  68. if (elmToMove.sel !== newStartVnode.sel) {
  69. // 如果新旧节点的选择器不同
  70. // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
  71. api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
  72. } else {
  73. // 如果相同,patchVnode()
  74. // 把 elmToMove 对应的 DOM 元素,移动到左边
  75. patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
  76. oldCh[idxInOld] = undefined as any
  77. api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
  78. }
  79. }
  80. // 重新给 newStartVnode 赋值,指向下一个新节点
  81. newStartVnode = newCh[++newStartIdx]
  82. }
  83. }
  84. // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
  85. if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
  86. if (oldStartIdx > oldEndIdx) {
  87. // 如果老节点数组先遍历完成,说明有新的节点剩余
  88. // 把剩余的新节点都插入到右边
  89. before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
  90. addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  91. } else {
  92. // 如果新节点数组先遍历完成,说明老节点有剩余
  93. // 批量删除老节点
  94. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  95. }
  96. }
  97. }

key的意义

key 的作用就是在同一个父节点下,标识相同标签的唯一一个元素,避免可能出现的渲染错误。