解析snabbdom源码,理解virtual dom 实现

谈论Virtual DOM 之前,首先说下什么是DOM, 为什么需要虚拟DOM, 他们之间存在着什么样的关系

什么是DOM

文档对象模型 (DOM) 是HTML和XML文档的编程接口,将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合, 简单的说,DOM就是解析文档,将web页面和脚本或程序语言连接起来。更多内容可以参考MDN中对DOM的定义

为什么需要虚拟DOM

DOM主要问题是没有为创建动态UI而优化

以前直接使用 DOM API 比较繁琐,然后有了 jQuery 等库来简化 DOM 操作;但这没有解决大量 DOM 操作的性能问题。大型页面/单页应用里动态创建/销毁 DOM 很频繁(尤其现在前端渲染的普遍),我们当然可以用各种 trick 来优化性能,但这太痛苦了。

而 Virtual DOM 就是解决问题的一种探索。

Virtual DOM 建立在 DOM 之上,是基于 DOM 的一层抽象,实际可理解为用更轻量的纯 JavaScript 对象(树)描述 DOM(树)。

虚拟dom的好处是当状态改变时,不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,虚拟DOM内部将弄清楚如何diff,更新dom

snabbdom 就是 Virtual DOM 的一个简洁实现,下面我们来看一下snabbdom是如何实现的

snabbdom 准备

  • 首先git上clone snabbdom,然后观察下项目的代码结构 git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
    解析snabbdom源码,理解virtual dom 实现_第1张图片

我们重点关注 examples(官方示例) 和 src(源码) 目录,pref是性能测试我们不做讨论。

snabbdom 核心

  • init() 设置模块,创建patch()函数
  • 使用h()函数创建JavaScript对象(VNode)描述真实DOM
  • patch()比较新旧两个VNode
  • 把变化的内容更新到真实DOM树

下面我们从这几个方面去看一下snabbdom的实现

h函数

  • 作用:创建VNode 对象
  • Vue中的h函数
new Vue({
     
  router,
  store,
  render: h => h(App)
}).$mount('#app')
  • h函数最早于hyperscript, 使用JavaScript创建超文本
  • 目标: 搞清楚h函数式如何创建VNode
    解析snabbdom源码,理解virtual dom 实现_第2张图片
    上图为h.ts文件中实现的h函数,通过ts的函数重载,根据不同的参数类型实现重载,然后判断参数的类型,去创建vnode对象

vnode函数

通过观察我们看到h函数通过调用vnode函数创建vnode对象,下面我们看下vnode函数创建的vnode对象长啥样

解析snabbdom源码,理解virtual dom 实现_第3张图片
vode.ts 中,首先定义并导出了三种类型,vnode实现函数接受五个参数,单独处理了key,发现key是通过data传递的,最后返回了VNode接口中定义的对象。

看完了vnode 的创建,我们看一下vnode渲染dom的过程,因为过程比较复杂,我们首先看下第一个核心函数patch

init

  • init函数式一个高阶函数,他接收两个参数后返回了patch函数
  • init函数接收了modules:出入函数中需要执行的钩子函数,domApi: 用什么api来操作dom,默认为htmlDomApi
  • 初始化了钩子函数后,并定义了一些工具函数,最后返回patch函数

patch

patch的整体过程分析

  • patch(oldVnode,newVnode)
  • 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下次处理的旧节点
  • 对比新旧VNode是否是相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
  • 如果新的VNode有children,判断子节点是否有变化
    patch函数流程图
    解析snabbdom源码,理解virtual dom 实现_第4张图片

patchVNode函数

是当新旧VNode为相同节点的时候,执行 pathNode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue),下面的流程图是对patchVode简单分析

patchVode 的函数执行过程主要分为三个部分

  1. 触发prepatch 和 update 钩子函数
  2. 对比新旧vnode差异
  3. 触发postpatch 钩子函数
    以下流程图我们仅对于对比vnode 差异部分进行讨论
    解析snabbdom源码,理解virtual dom 实现_第5张图片

updateChildren

当新旧节点都有子节点,并且子节点不相同时,会调用updateChildren函数,这个函数也是diff算法的核心

diff算法

首先我们需要理解在虚拟dom中为什么使用diff算法?

渲染真实dom的开销很大,dom操作会引发浏览器的重排和重绘,非常消耗性能。虚拟dom中diff的核心是当数据发生变化时不去直接操作dom,而是用对象描述真实dom, 当数据变化后,找到变化位置,最小化更新dom,从而提高性能

由此我们知道,diff算法也没什么神奇的,就是查找两颗树的差异,但是他有很多种查找方式

  • 第一种方式:第一颗数上的节点和第二颗树上的每个节点做对比,如果有n个节点,会查找n^2次
    解析snabbdom源码,理解virtual dom 实现_第6张图片
  • snabbdom根据DOM的特点对传统的diff算法进行了优化
    • dom操作时候很少会跨级别操作节点
    • 只会比较同级别的节点
    • 如果同级别不相同的话,直接删除重新创建
    • 同级别的节点只需要比较一次,从而达到减少比较次数的目的
    • 如果有n个节点,只需要查找n次
      解析snabbdom源码,理解virtual dom 实现_第7张图片
snabbdom中对比子节点的具体过程

在对比开始和节点节点比较时候,总共分四种

  • 旧开始/新开始

    如果旧开始和新开始是sameVnode(key和sel相同)

    • 调用patchVnode()对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
      如果不是sameVnode, 开始 旧结束/新结束比较
  • 旧结束/新结束

    如果旧结束和新结束是sameVnode(key和sel相同)

    • 调用patchVnode()对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx-- / oldEndIdx–
      如果不是, 开始 旧开始/新结束比较
  • 旧开始/新结束

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

    • 调用patchVnode()对比和更新节点

    • 把oldStartVnode 对应的dom元素,移动到右边,更新索引

      如果不是, 开始 旧结束/新开始比较

  • 旧结束/新开始

    如果是sameVnode(key和sel相同)

    • 调用patchVnode()对比和更新节点
    • 把oldEndVnode 对应的dom元素,移动到左边,更新索引

如果以上四种情况都不满足

  1. 首先遍历新开始节点,在旧节点数组中依次查找是否有相同key值的节点,
  2. 如果没有,创建新dom元素插入到最前面的位置
  3. 如果找到了,并且判断sel属性是否相同,如果相同,旧节点会被复制给elmToMove,然后调用patchVNode对比节点,然后把elmToMove对应的dom节点移动到最前面
    ——————循环结束后
  • 当老节点的所有子节点先遍历完成,说明新节点有剩余,把剩余节点批量插入到右边
  • 新节点的所有子节点先遍历完成,说明老节点有剩余,把剩余老节点删除

你可能感兴趣的:(Vue.js源码分析,javascript,vue.js)