什么是虚拟 DOM

  • Virtual DOM(虚拟DOM),是由普通的 JS 对象描述 DOM 对象,因为不是真实的 DOM对象,所以叫 Virtual DOM
  • 真实的 DOM成员 非常非常多,所以创建一个 DOM 对象的成本非常高
  • 可以通过 Virtual DOM 来描述真实 DOM
    1. {
    2. sel:'div',
    3. data: {},
    4. children: undefined,
    5. text: 'Hello Virtual DOM',
    6. elm: undefined,
    7. key: undefined
    8. }
    Virtual DOM 对象非常小的,我们创建一个 Virtual DOM 的成本比创建一个真实 DOM 的成本小很多

为什么使用虚拟DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery等简化 DOM 操作,但是随着项目的复杂 DOM操作复杂提升
  • 为了简化 DOM 的复杂操作于是出现来各种 MVVM框架,MVVM框架解决来视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模版引擎,但是模版引擎没有解决跟踪状态变化的问题,于是 Virtual DOM 出现了;Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述 DOM,Virtual DOM 内部将弄清楚如果有效(diff) 的更新DOM
  • 参考github上virtual-dom的描述

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

      虚拟DOM的作用

  • 维护视图和状态的关系

  • 复杂视图情况下提升渲染性能
  • 除了浏览器平台渲染DOM以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)

    虚拟DOM库

  • Snabbdom

    • Vue2.x内部使用的Virtual DOM 就是改造的 Snabbdom
    • 大约200SLOC(Single Line Of Code)
    • 通过模块可扩展
    • 源码使用TypeScript开发
    • 最快的Virtual DOM之一
  • virtual-dom

    Snabbdom 基本使用

    ```powershell

    创建项目目录

    mkdir snabbdom-demo

进入项目目录

cd snabbdom-demo

初始化 package.json

npm init -y

本地安装 parcel

npm install parcel-bundler -D

  1. - 配置 package.json script
  2. ```json
  3. "scripts": {
  4. "dev": "parcel index.html --open",
  5. "build": "parcel build index.html"
  6. }
  • 创建项目基本结构
    1. |-index.html
    2. |-package.json
    3. |-src
    4. |-01.basicusage.js

导入 Snabbdom

  • 安装
    1. npm install snabbdom@3.0.1
    ```javascript import { init, h } from ‘snabbdom’

const patch = init([])

  1. <a name="GGeeI"></a>
  2. ### 案例1-只包含文本内容
  3. ```javascript
  4. // 01-BasicUsage.js
  5. import { init, h } from 'snabbdom'
  6. // 1 通过 h 函数创建 VNode
  7. let vNode = h('div#box.container', '新内容')
  8. // 获取挂载元素
  9. const dom = document.querySelector('#app')
  10. // 2 通过 init 函数得到 patch 函数
  11. const patch = init([])
  12. // 3 通过 patch,将 vNode 渲染到 DOM
  13. let oldVNode = patch(dom, vNode)
  14. // 4 创建新的 VNode,更新给 oldVNode
  15. vNode = h('p#text.abc', '这是p标签的内容')
  16. patch(oldVNode, vNode)

案例2-包含子节点

  1. import { h, init} from 'snabbdom'
  2. const patch = init([])
  3. // 创建包含子节点的 VNode
  4. // - 参数2的数组为子节点列表,内部就应该传入 vNode
  5. let vNode = h('div#container', [
  6. h('h1', '标题文本'),
  7. h('p', '内容文本')
  8. ])
  9. // 获取挂载元素
  10. const dom = document.querySelector('#app')
  11. // 渲染 vNode
  12. const oldVNode = patch(dom, vNode)
  13. // 清空页面元素
  14. patch(oldVNode, h('!'))

Snabbdom 中的模块

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

官方提供的模块:

  • attributes
    • 设置了DOM 元素的属性,使用 setAttribute()
    • 处理布尔类型的属性
  • props
    • 和 attributes 模块相似,设置 DOM元素 的属性 element[attr] = value
    • 不处理布尔类型的属性
  • class
    • 切换类样式
    • 给元素设置类样式是通过 set 选择器
  • dataset
    • 设置 data-*的自定义属性
  • eventlisteners
    • 注册和移除事件
  • style

    • 设置行内样式,支持动画
    • delayed/remove/destroy

      模块的使用

  • 导入模块

  • init() 注册模块
  • h() 函数的第二个参数处使用模块 ```javascript // 1 导入模块(注意拼写,导入的名称不要拼错) import { init, h, styleModule, eventListenersModule } from ‘snabbdom’

// 2 注册模块(为 patch 函数添加模块对应的能力) const patch = init([ styleModule, eventListenersModule ])

// 3 使用模块 let vNode = h(‘div#box’, { style: { backgroundColor: ‘green’, height: ‘200px’, width: ‘200px’ } }, [ h(‘h1#title’, { style: { color: ‘#fff’ }, on: { click () { console.log(‘点击了 h1 标签’) } } }, ‘这是标题内容’), h(‘p’, ‘这是内容文本’) ])

const dom = document.getElementById(‘app’) patch(dom, vNode)

  1. <a name="601Kr"></a>
  2. ## Snabbdom 源码解析
  3. - 如何学习源码
  4. - 先宏观了解
  5. - 带着目标看源码
  6. - 看源码的过程要不求甚解
  7. - 调试
  8. - 参考资料
  9. - Snabbdom的核心
  10. - 使用 h() 函数创建 JavaScript 对象(VNode)描述真实DOM
  11. - init设置模块,创建patch()
  12. - patch()比较新旧两个 VNode
  13. - 把变化的内容更新到真实 DOM树上
  14. - Snabbdom源码<br />
  15. - [源码地址](https://github.com/snabbdom/snabbdom)<br />
  16. <a name="OTkHP"></a>
  17. ### h函数
  18. - 作用:创建 VNode 对象
  19. - Vue中的 h函数
  20. ```javascript
  21. new Vue ({
  22. render: h=> h(app)
  23. }).$mount('#app')
  • h函数 最早见于 hyperscript,使用 JavaScript 创建超文本
  • Snabbdom中的h函数不是用来创建超文本,而是创建VNode

函数的重载

  • 参数个数或参数类型不同的函数
  • JavaScript 中没有重载的概念
  • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
  • 参数个数不同的重载示例 ```javascript function add (a: number, b: number) { console.log(a + b) }

function add (a: number, b: number, c: number) { console.log(a + b + c) }

add(1, 2) add(1, 2, 3)

  1. - [源码位置](https://gitee.com/zhou__jian/lagou-basis/blob/master/part04-05/codes/02-virtual-DOM/snabbdom/src/h.ts)
  2. <a name="q3BFM"></a>
  3. ### VNode
  4. - [源码地址](https://gitee.com/zhou__jian/lagou-basis/blob/master/part04-05/codes/02-virtual-DOM/snabbdom/src/vnode.ts)
  5. <a name="yRysf"></a>
  6. ### patch 整体分析
  7. - patch(oldVnode, newVnode)
  8. - 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  9. - 对比新旧 VNode 是否相同节点(节点的 key sel 相同)
  10. - 如果不是相同节点,删除之前的内容,重新渲染
  11. - 如果是相同节点,再判断新的 VNode 是否是 text,如果有并且和 oldVnode text 不同,直接更新文本内容
  12. - 如果新的 VNode children,判断子节点是否有变化
  13. <a name="0tKAf"></a>
  14. ####
  15. <a name="sD5MS"></a>
  16. ### init函数
  17. [返回patch函数源码](https://gitee.com/zhou__jian/lagou-fed/blob/master/part03-01/codes/snabbdom-master/src/package/init.ts)
  18. <a name="RhuiV"></a>
  19. ####
  20. <a name="SWXuL"></a>
  21. ### patch
  22. ```javascript
  23. //./codes/snabbdom-master/src/package/init.ts
  24. return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
  25. let i: number, elm: Node, parent: Node
  26. //存储新插入节点的队列
  27. const insertedVnodeQueue: VNodeQueue = []
  28. //遍历 pre中的钩子函数并执行 正式处理 VNode 之前触发的第一个钩子函数
  29. for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
  30. if (!isVnode(oldVnode)) {
  31. //如果不是 VNode 对象,将转换成 VNode 对象
  32. oldVnode = emptyNodeAt(oldVnode)
  33. }
  34. //判断新旧 VNode 是否是相同节点
  35. if (sameVnode(oldVnode, vnode)) {
  36. //对比新旧 VNode 差异,更新 DOM
  37. patchVnode(oldVnode, vnode, insertedVnodeQueue)
  38. } else {
  39. elm = oldVnode.elm!
  40. // 获取父元素
  41. parent = api.parentNode(elm) as Node
  42. // 创建 VNode 对于的 DOM 元素,并将新插入的节点队列传入
  43. createElm(vnode, insertedVnodeQueue)
  44. if (parent !== null) {
  45. // 插入到父元素中
  46. api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
  47. //移除老节点
  48. removeVnodes(parent, [oldVnode], 0, 0)
  49. }
  50. }
  51. for (i = 0; i < insertedVnodeQueue.length; ++i) {
  52. insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
  53. }
  54. // 触发 post 钩子函数
  55. for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
  56. //返回 VNode
  57. return vnode
  58. }

createElm

  1. function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  2. //1. 执行用户设置的 init 钩子函数
  3. let i: any
  4. let data = vnode.data
  5. if (data !== undefined) {
  6. const init = data.hook?.init
  7. // 判断 init 是否有定义
  8. if (isDef(init)) {
  9. init(vnode)
  10. data = vnode.data
  11. }
  12. }
  13. // 2. 把 VNode 转换成真实 DOM 对象(没有渲染到页面)
  14. const children = vnode.children
  15. const sel = vnode.sel
  16. if (sel === '!') {
  17. //undefined
  18. if (isUndef(vnode.text)) {
  19. vnode.text = ''
  20. }
  21. // 如果选择器是 '!' 创建注释节点
  22. vnode.elm = api.createComment(vnode.text!)
  23. } else if (sel !== undefined) {
  24. // 如果选择器不为空
  25. //解析选择器
  26. // Parse selector
  27. const hashIdx = sel.indexOf('#')
  28. const dotIdx = sel.indexOf('.', hashIdx)
  29. const hash = hashIdx > 0 ? hashIdx : sel.length
  30. const dot = dotIdx > 0 ? dotIdx : sel.length
  31. const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
  32. const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
  33. ? api.createElementNS(i, tag)
  34. : api.createElement(tag)
  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. api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
  44. }
  45. }
  46. //是否是 原始值 string/number
  47. } else if (is.primitive(vnode.text)) {
  48. // 创建文本节点,追加到 DOM 树上
  49. api.appendChild(elm, api.createTextNode(vnode.text))
  50. }
  51. // 判断 hook 是否有定义
  52. const hook = vnode.data!.hook
  53. if (isDef(hook)) {
  54. // 触发 create 钩子函数
  55. hook.create?.(emptyNode, vnode)
  56. if (hook.insert) {
  57. // 将设置了 insert 钩子函数的 VNode 存入数组队列,在 DOM插入 DOM 树后执行
  58. insertedVnodeQueue.push(vnode)
  59. }
  60. }
  61. } else {
  62. // 如果选择器为空,创建文本节点
  63. vnode.elm = api.createTextNode(vnode.text!)
  64. }
  65. // 3. 返回新创建的 DOM
  66. return vnode.elm
  67. }

removeVnodes

  1. function removeVnodes (parentElm: Node,//父元素
  2. vnodes: VNode[],//需要删除的元素
  3. startIdx: number,//开始索引
  4. endIdx: number//结束索引
  5. ): void {
  6. for (; startIdx <= endIdx; ++startIdx) {
  7. let listeners: number
  8. let rm: () => void
  9. const ch = vnodes[startIdx]
  10. if (ch != null) {
  11. if (isDef(ch.sel)) {//元素节点
  12. invokeDestroyHook(ch)
  13. listeners = cbs.remove.length + 1 //防止重复删除 DOM 元素
  14. rm = createRmCb(ch.elm!, listeners) // 返回真正删除DOM元素 的函数
  15. for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
  16. const removeHook = ch?.data?.hook?.remove
  17. if (isDef(removeHook)) {
  18. //如果用户传入remove 钩子函数,需要手动调用 rm 删除 DOM 元素
  19. removeHook(ch, rm)
  20. } else {
  21. rm()
  22. }
  23. } else { // Text node 文本节点
  24. api.removeChild(parentElm, ch.elm!)
  25. }
  26. }
  27. }
  28. }

addVnodes

  1. function addVnodes (
  2. parentElm: Node,//父节点
  3. before: Node | null,//参考节点,插入 before 之前
  4. vnodes: VNode[],//被添加节点
  5. startIdx: number,//开始索引
  6. endIdx: number,//结束索引
  7. insertedVnodeQueue: VNodeQueue //存储 具有 insert 钩子函数的 VNode 节点
  8. ) {
  9. for (; startIdx <= endIdx; ++startIdx) {
  10. const ch = vnodes[startIdx]
  11. if (ch != null) {
  12. // 插入到 DOM 树中
  13. api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
  14. }
  15. }
  16. }

patchVnode

patchVnode.png

  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. vnode.data.hook?.update?.(oldVnode, vnode)
  12. }
  13. // 第二个过程:真正对比新旧 vnode 差异的地方
  14. if (isUndef(vnode.text)) {
  15. //新旧节点是否都有子节点
  16. if (isDef(oldCh) && isDef(ch)) {
  17. //对比新旧节点的子节点 更新DOM
  18. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
  19. } else if (isDef(ch)) {
  20. if (isDef(oldVnode.text)) api.setTextContent(elm, '') //清空老节点的 文本内容
  21. //将新节点 子节点插入 elm 中
  22. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  23. } else if (isDef(oldCh)) {
  24. //清空老节点 的子节点
  25. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  26. } else if (isDef(oldVnode.text)) {
  27. //清空老节点的 文本内容
  28. api.setTextContent(elm, '')
  29. }
  30. } else if (oldVnode.text !== vnode.text) {
  31. //新旧节点是文本节点,且不想等
  32. if (isDef(oldCh)) {
  33. //清空老的文本节点
  34. removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  35. }
  36. //更新文本节点
  37. api.setTextContent(elm, vnode.text!)
  38. }
  39. // 第三个过程:触发 postpatch
  40. // 这个钩子函数可以获取最新的数据
  41. hook?.postpatch?.(oldVnode, vnode)
  42. }

updateChildren

  1. function updateChildren (parentElm: Node,//父元素
  2. oldCh: VNode[],//旧Vnode 子节点
  3. newCh: VNode[],//新Vnode 子节点
  4. insertedVnodeQueue: VNodeQueue//有 insert 钩子函数的 Vnode 节点队列
  5. ) {
  6. let oldStartIdx = 0 //旧开始索引
  7. let newStartIdx = 0 //新开始索引
  8. let oldEndIdx = oldCh.length - 1 //旧结束索引
  9. let oldStartVnode = oldCh[0] // 旧的开始节点
  10. let oldEndVnode = oldCh[oldEndIdx] //旧结束节点
  11. let newEndIdx = newCh.length - 1 //新结束索引
  12. let newStartVnode = newCh[0] //新开始节点
  13. let newEndVnode = newCh[newEndIdx] //新结束节点
  14. let oldKeyToIdx: KeyToIndexMap | undefined
  15. let idxInOld: number
  16. let elmToMove: VNode
  17. let before: any
  18. // 同级别节点比较
  19. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  20. if (oldStartVnode == null) {
  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. // 比较开始和结束的 4 中情况
  29. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  30. //旧开始节点 和 新开始节点 是相同节点
  31. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  32. oldStartVnode = oldCh[++oldStartIdx]
  33. newStartVnode = newCh[++newStartIdx]
  34. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  35. // 旧结束节点 和 新结束节点 是相同节点
  36. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  37. oldEndVnode = oldCh[--oldEndIdx]
  38. newEndVnode = newCh[--newEndIdx]
  39. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  40. // 旧开始节点 和 新就是节点 是相同节点
  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. // 旧结束节点 和 新 开始节点 是相同节点
  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. if (oldKeyToIdx === undefined) {
  54. oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  55. }
  56. idxInOld = oldKeyToIdx[newStartVnode.key as string]
  57. if (isUndef(idxInOld)) { // New element
  58. api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
  59. } else {
  60. elmToMove = oldCh[idxInOld]
  61. if (elmToMove.sel !== newStartVnode.sel) {
  62. api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
  63. } else {
  64. patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
  65. oldCh[idxInOld] = undefined as any
  66. api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
  67. }
  68. }
  69. newStartVnode = newCh[++newStartIdx]
  70. }
  71. }
  72. //循环结束的收尾工作
  73. if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
  74. if (oldStartIdx > oldEndIdx) {
  75. // 老节点数组遍历完,新节点数组有剩余
  76. before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
  77. addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  78. } else {
  79. // 新节点数组遍历完,旧节点有剩余
  80. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  81. }
  82. }
  83. }