大前端【3-1-3笔记】Virtual DOM 的实现原理

Virtual DOM 的实现原理

  • 了解什么是虚拟DOM以及虚拟DOM的作用
  • Snabbdom的基本使用
  • Snabbdom的源码解析

一、虚拟DOM

1、什么是虚拟DOM

Virtual DOM (虚拟DOM),是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫虚拟DOM

2、为什么使用虚拟DOM
  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,
    但是随着项目的复杂 DOM 操作复杂提升
  • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是
    Virtual DOM 出现了
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述
    DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
  • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
  • 通过比较前后两次状态的差异更新真实 DOM
3、虚拟DOM的作用
  1. 维护视图和状态的关系
  2. 复杂视图情况下提升渲染性能
  3. 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序
    (mpvue/uni-app)等
4、虚拟DOM库Snabbdom
  1. Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
  2. 大约 200 SLOC(single line of code)
  3. 通过模块可扩展
  4. 源码使用 TypeScript 开发
  5. 最快的 Virtual DOM 之一
5、导入Snabbom
import {h,thunk,init} from 'snabbdom'
  • Snabbdom 的核心仅提供最基本的功能,只导出了三个函数 init()、h()、thunk()
  • init() 是一个高阶函数,返回 patch();
  • h() 返回虚拟节点 VNode;
  • thunk() 是一种优化策略,可以在处理不可变数据时使用
6、代码
//导入snabbdom
// import snabbdom from 'snabbdom'
// console.log(snabbdom)
import {h,init} from 'snabbdom'

//参数:数组,模块
//返回值:patch函数,作用:对比两个vnode的差异,并更新到真实的DOM
let patch = init([])
//第一个参数:标签+选择器,
//第二个参数:如果是字符串,就是标签中的内容
let vnode = h("div#app","helloworld")

let app = document.querySelector("#app")
//第一个参数:可以使DOM元素,内部会把DOM元素转化为VNode
//第二个参数:VNode
//返回值:VNode
let oldValue = patch(app,vnode)

//假设的时刻
vnode = h("div","hello Snabbdom")
patch(oldValue,vnode)
7、模块
import { init, h } from 'snabbdom'

//导入模块
import style from 'snabbdom/modules/style'
import eventListeners from 'snabbdom/modules/eventlisteners'

//注册模块
let patch = init([
    style,
    eventListeners
])
//使用h函数的第二个参数传入模块所需要的数据
let vnode = h('div', {
    style: {
        backgroundColor: 'red'
    },
    on: {
        click: eventHandler
    }
}, [
    h("h1", "我是h1标签"),
    h("p", "我是p标签")
]
)

function eventHandler(){
    console.log("點擊")
}

let app = document.querySelector("#app")

patch(app,vnode)
8、h函数
  • h()函数介绍

    • 在使用Vue的时候h函数的使用

      new Vue({
         router,
         store,
         render:h=>h(App)
      }).$mount("#app");
      
    • h()函数最早见于hyperscript,使用JavaScript创建超文本

    • Snabbdom中的h()函数不是用来创建超文本,而是创建VNode

  • 函数重载,等同于Java中的重载

    • 概念
      • 参数个数或类型不同的函数
      • JS中没有重载的概念
      • TypeScript中有重载,不过重载的实现还是通过代码调整参数
9、patch的整体过程
  • patch(oldValue,newValue)
  • 找补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧VNode是否是相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
  • 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
  • diff过程只进行同层级比较
10、updateChildren
  1. 功能:diff算法的核心,对比新旧节点的children,更新DOM

  2. 执行过程:

    • 在dom操作的时候我们很少会吧一个父节点移动或更新到某一个子节点,因此只需要找同级别的子节点一次比较,然后再找下一级别的节点进行比较。

    • 在进行同级别比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引

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

      oldStartVnode/newStartVnode(旧开始节点/新开始节点)

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

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

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

    • 开始节点和结束节点的比较,情况类似

      • oldStartVnode/newStartVnode(旧开始节点/新开始节点)
      • oldEndVnode/newEndVnode(旧开始节点/新结束节点)
    • 如果oldStartVnode和newStartVnode是sameVnode(key和sel相同)

      • 调用patchVnode()对比和更新节点
      • 把旧开始和新开始索引向后移动 oldStartIdx++/oldEndIdx++
    • 如果oldStartVnode/newEndVnode(旧开始节点/新结束节点)相同

      • 调用patchVnode()对比和更新节点
      • 把oldStartVnode对应的DOM元素,移动到右边
      • 更新索引
    • oldEndVnode/newStartVnode(旧结束节点/新开始节点)相同

      • 调用patchVnode()对比和更新节点
      • 把oldEndVnode对应的DOM元素,移动到左边
      • 更新索引
    • 如果不是以上四种情况

      • 遍历新节点,使用newStartNode的key在老节点数组中找相同节点

      • 如果没有找到,说明newStartNode是新节点

        • 创建新节点对应的DOM元素,插入到DOM树中
      • 如果找到了

        • 判断新节点与找到的老街店的sel选择器是否相同

        • 如果不相同,说明节点被修改了

          重新创建对应的DOM元素,插入到DOM树中。

        • 如果相同,吧elmToMove对应的DOM元素,移动到左边

        • 循环结束

          • 当老街店的所有子节点先遍历完,循环结束
          • 新节点的所有子节点先遍历完,循环结束
        • 如果老街店的数组先遍历完,说明新节点有剩余,把剩余节点批量插入到右边

        • 如果新节点的数组先遍历完,说明老节点有剩余,把剩余节点批量删除

你可能感兴趣的:(webpack,vue,拉钩教育)