likes
comments
collection
share

Virtual DOM的实现原理总结超详细版(一)

作者站长头像
站长
· 阅读数 32

Virtual DOM的实现原理 made by Q7Long

目标

  • 了解什么是虚拟 DOM ,以及虚拟 DOM 的作用
  • Snabbdom 的基本使用( Vue 内部的虚拟 DOM 是改造了开源库Snabbdom
  • Snabbdom 的源码解析

1、什么是Virtual DOM

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

我们为什么用虚拟 DOM 来模拟真实的 DOM 呢?

因为我们知道一个 DOM 对象中的成员是非常多。所以创建 DOM 对象的成本非常高。

如果使用虚拟 DOM 来描述真实 DOM ,就会发现创建的成员少,成本也就低了。

2、为什么使用Virtual DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 Jquery 等库简化 DOM 操作,但是随着项目的复杂度越来越高, DOM 操作复杂提升,既要考虑 DOM 操作,还要考虑数据的操作。
  • 为了简化 DOM 的复杂操作于是出现了各种的 MVVM 框架, MVVM 框架解决了视图和状态的同步问题, 也就是当数据发生变化,更新视图,当视图发生变化更新数据
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题(当数据发生了变化后,无法获取上一次的状态,只有将页面上的元素删除,然后在重新创建,这时页面有刷新的问题,同时频繁操作 DOM ,性能也会非常低),于是 Virtual Dom 出现了。
  • Virtual Dom 的好处就是当状态改变时不需要立即更新 DOM ,只需要创建一个虚拟树来描述 DOMVirtual Dom 内部将弄清楚如何有效( diff )的更新 DOM 。(例如:向用户添加列表中添加一个用户,只添加新的内容,原有的结构会被重用)
  • 虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地一个 JS 对象中,最终将这个 JS 对象一次性 attchDOM 树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上,操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,交由浏览器去绘制。
  • 下面,我们看一段代码,该代码是使用 jquery 来实现的数据展示与排序,是 纯DOM 操作的方式
<!DOCTYPE html>
<html>
<head>
  <title></title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
  </head>
  <body>
    <div id="app">
    </div>
    <div id="sort" style="margin-top: 20px;">按年纪排序</div>
    <script type="text/javascript">
      var datas = [
        { 'name': 'james', 'age': 32 },
        { 'name': 'Q7Long', 'age': 23 },
        { 'name': 'kobe', 'age': 31 },
        { 'name': 'wade', 'age': 30 }
      ];
      var render = function() {
        var html = '';
        datas.forEach(function(item, index) {
          html += `<li>
                    <div class="u-cls">
                      <span class="name">姓名:${item.name}</span>
                      <span class="age" style="margin-left:20px;">年龄:${item.age}</span>
                      <span class="closed">x</span>
                    </div>
                  </li>`;
        });
        return html;
      };
      $("#app").html(render());
      $('#sort').on('click', function() {
        datas = datas.sort(function(a, b) {
          return a.age - b.age;
        });
        $('#app').html(render());
    })
  </script>
</body>
</html>

你用传统的原生apijQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程

比如当我们在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程

而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attachDOM树上,避免大量的无谓计算

如上 demo 排序,虽然在使用 jquery 时代这种方式是可行的,我们点击按钮,它就可以从小到大的排序,但是它比较暴力,它会将之前的 demo 全部删除,然后重新渲染新的 demo 节点,我们知道,操作 DOM 会影响页面的性能,并且有时候数据根本就没有发生改变,我们希望未更改的数据不需要重新渲染操作。

因此虚拟 DOM 的思想就出来了,虚拟 DOM 的思想是先控制数据再到视图,但是数据状态是通过 diff 比对,它会比对新旧虚拟 DOM 节点,然后找出两者之前的不同,然后再把不同的节点再发生渲染操作。

很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI

如下图所示:

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

  • 总结:

    虚拟 DOM 可以维护程序的状态,跟踪上一次的状态

    通过比较前后两次状态的差异来更新真实 DOM

3、虚拟DOM的作用

维护视图和状态的关系(虚拟 DOM 会记录状态的变化,只需要更新状态变化的内容就可以了)

复杂视图情况下提升渲染性能

下面的一个案例,该案例的功能比较简单,单击按钮后,更新 div 中的内容。

let div=document.querySelector('#app')
let btn=document.querySelector('#btn')
btn.onclick=function(){
    div.textContent='Hello World'
}

以上代码非常简单,而且是使用 DOM 操作的方式来实现的。

如果上面的案例,我们使用虚拟 DOM 来实现,应该怎样处理呢?首先,我们要创建一个虚拟 DOM 的对象,

虚拟 DOM 对象就是一个普通的 JS 对象。当单击按钮的时候,需要对比两次状态的差异。所以说,仅仅是该案例,

我们使用虚拟 DOM 的方式来实现,要比使用纯 DOM 的方式来实现,性能要低。

所以说, 并不是所有的情况下使用虚拟DOM都会提升性能的只有在视图比较复杂的情况下使用虚拟 DOM 才会提升渲染的性能

虚拟 DOM 除了渲染 DOM 以外,还可以实现渲染到其它的平台 虚拟DOM有跨平台特性 ,例如可以实现服务端渲染( ssr ),原生应用( React Native ),小程序( uni-app 等)。以上列举的案例中,内部都使用了虚拟 DOM .

Vue 中虚拟 DOM 生成真实 DOM 的过程

Virtual DOM的实现原理总结超详细版(一)

下面就是一个开源的虚拟 DOM 库--- Snabbdom

Vue2.x 开始内部使用的虚拟 DOM ,就是改造的 Snabbdom 。源码大约200行作用,源码使用 TypeScript开发

4、Snabbdom基本使用

4.1 创建项目

Snabbdom 的基本使用之前,我们先来创建一个项目。

打包工具为了方便使用,使用了 parcel ,也可以使用 webpack

下面创建项目,并安装 parcel

//创建项目目录
md snabbdom-demo
// 进入项目目录
 cd snabbdom-demo
// 创建package.json
npm init -y
//本地安装parcel
npm install parcel-bundler

配置 package.json 中的 scripts

 "srcipts":{
     "test": "echo "Error: no test specified" && exit 1",
     "dev":"parcel index.html --open" , //open打开浏览器
     "build""parcel build index.html"
 }

创建目录结构

index.html
package.json
---js
    basicusage.js

4.2 导入Snabbdom

方法文档:

https://github.com/snabbdom/snabbdom

下面先安装 Snabbdom 。(这里最新版本有问题,可以先安装0.7.4)

npm install snabbdom@0.7.4

在项目的 js 文件夹下的 01-basicusage.js 文件中,添加如下代码:

import  snabbdom  from "snabbdom";
console.log(snabbdom);

以上代码的意思就是导入 snabbdom 这个包,然后打印其内容。

项目的启动

npm run dev

这时,开启的端口号为 1234

http://localhost:1234

在打开的浏览器中,查看控制台的输出,发现输出的内容为 undefined

为什么输出的是 undefined 呢?这里我们需要查看对应的源码

我们在学习一个函数时,可以重点了解该函数的“入参”和“出参”,大致就能判断该函数的作用。

node_modules/snabbdom/snabbdom.js 文件中,

我们可以看到在整个文件中的最下面,并没有使用 export default 的方式进行导出,所以就不能使用 import snabbdom 这种方式进行导入。同时在,源码中,我们可以看到导出了三项内容分别为 h , thunk , init .

分别在源码的第29行,第31行,第311行,进行了对这三项内容函数的单独导出,并没有采用 export default 的形式进行整体导出

exports.h = h_1.h;      //29
exports.thunk = thunk_1.thunk;    //31
exports.init = init;     //311

所以我们就不能使用 import snabbdom 这种方式进行导入,而是应该先对其进行一个解构。导入的代码修改成如下的形式

import { h, thunk, init } from "snabbdom";
console.log(h, thunk, init);

Snabbdom 的核心仅提供最基本的功能,只导出了三个函数 init() , h() , thunk()

  1. init() 函数是高阶函数里面是有很多的函数,结果是返回了一个 patch()

    init() 函数被定义在 package/init.ts 文件中:init() 函数接收一个模块数组 modules 和可选的 domApi 对象作为参数,返回一个函数,即 patch() 函数。 domApi 对象的接口包含了很多 DOM 操作的方法。下面的内容也将对 modules 参数做重点介绍。

高阶函数英文叫Higher-order function。
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

Virtual DOM的实现原理总结超详细版(一)

  1. h() 函数返回虚拟节点 VNode ,这个函数我们在使用 Vue.js 的时候见过。

    h() 函数被定义在 package/h.ts 文件中:

Virtual DOM的实现原理总结超详细版(一)

2.1 h() 函数用于创建虚拟 DOM ,在 Snabbdom 中用 VNode 描述虚拟节点,也就是虚拟 DOM

new Vue({
router,
// h()函数在 Vue.js 中的使用,h() 函数的作用就是创建虚拟 DOM,我们在整个 Snabbdom 里面就是使用 h() 函数返回的 VNode 来描述虚拟DOM
render:h=>h(App)
}).$mount('#app')

2.2 VNode 是什么?这里简单解释下:

VNode,该对象用于描述节点的信息,它的全称是虚拟节点(virtual node)。与 “虚拟节点” 相关联的另一个概念是 “虚拟 DOM”,它是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。“虚拟 DOM” 由 VNode 组成的。
                                            —— 全栈修仙之路 《Vue 3.0 进阶之 VNode 探秘》

其实 VNode 就是一个 JS 对象,在 Snabbdom 中是这么定义 VNode 的类型:

/*
这里举一个 h() 函数传递传递三个参数的例子,方便下面的理解
let vnode = h("div#container", {
    // 第二个参数这里写模块所需要的数据
    style: { backgroundColor: "skyblue" },
    // 注册一个事件
    on: { click: eventHandler }
​
}, [h("h1", "Hello Q7Long"), h("p", "这是一个p标签")]
)
*/export interface VNode {
  // sel选择器,就是我们在调用 h() 函数的时候,传递的第一个参数。比如上面的 "div#container"
​
  sel: string | undefined;
  // data节点的数据,里面就包括了 属性/样式/事件等
  data: VNodeData | undefined;
  // children子节点:VNode是用来描述真实DOM的,如果描述真实DOM中有子节点,我们可以通过     children来表示这些子节点
  children: Array<VNode | string> | undefined;
  // 记录的是VNode对应的真实的DOM,也就是将VNode虚拟节点,转成真实DOM之后,这个真实DOM就会被会存储到elm中
  elm: Node | undefined;
  // 表示的就是节点当中的内容
  text: string | undefined;
  // 优化的作用
  key: Key | undefined;
}
​
export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: Array<any>; // for thunks
  [key: string]: any; // for any other 3rd party module
}
​
export function vnode(sel: string | undefined,
                      data: any | undefined,
                      children: Array<VNode | string> | undefined,
                      text: string | undefined,
                      elm: Element | Text | undefined): VNode {
  let key = data === undefined ? undefined : data.key;
  // 最后返回了一个JS对象,这个其实就是 虚拟DOM
  return {sel, data, children, text, elm, key};
}
​
export default vnode;

在 VNode 对象中含描述节点选择器 sel 字段、节点数据 data 字段、节点所包含的子节点 children 字段等。

在这个 demo 中,我们似乎并没有看到模块系统相关的代码

  1. thunk 函数是一种优化策略,可以在处理不可变数据时使用(用于优化复杂的视图)。

4.3 Snabbdom的基本使用

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Snabbdom的基本使用</title>
</head>
<body>
    <div id="app">Hello World</div>
    <script src="./js/01-basicusage.js"></script>
</body>
</html>

下面我们来看一下 Snabbdom 的基本使用,在 01- basicusage.js 文件中编写如下代码

import { h, thunk, init } from "snabbdom";
// init方法返回值为patch函数,patch函数作用是对比两个vndoe的差异并更新到真实的DOM中。init函数的参数是一个数组,数组中的内容是模块,关于模块内容后面还会讲解
let patch = init([]);
//创建虚拟DOM
// 第一个参数:标签+选择器(id选择器或者是类选择器)
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h("div#container.cls", "Hello Q7Long");
//我们这里需要将创建的虚拟dom,最终要渲染到`index.html`中`app`这个div中,所以这里需要获取一下该div
let app = document.querySelector("#app");
//要想将虚拟DOM渲染到`app`中,需要用到patch函数。
// 我们知道patch函数的作用是对比两个vnode的差异来更新到真实的`DOM`中。
//但是我们目前没有两个虚拟DOM.那么patch方法的第一个参数也可以是真实的DOM.patch方法会将真实的DOM转换成VNode.
// 第二个参数:为VNode
//返回值为VNode
let oldNode = patch(app, vnode);
​

运行上面的代码,可以在浏览器中看到 Hello Q7Long .

可以查看对应生成的元素。

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

下面我们再来看一个问题,假如在某个时刻需要重新获取服务端的数据,并且将获取到的数据重新渲染到该 div 中( id='containerdiv )。

我们这里,就需要重新创建一个 VNode , 然后传递给 patch ,让 patch 比较一下新的 VNode 与原有的 VNode 之间的差异。

补充后的代码如下:

import { h, thunk, init } from "snabbdom";
// console.log(h, thunk, init);/* snabbdom 的基本使用,调用init函数,需要的参数是一个数组,
数组中的内容是模块,这个地方没有用到先写一个空数组 */
let patch = init([])   // 返回的是patch函数,作用:对比两个 vNode 的差异并更新到真实的DOM中/* 创建虚拟DOM,用到h()函数帮助我们创建虚拟DOM  
1. 第一个参数:标签+选择器 加上 .cls 是类选择器 前面加 # 就是 id 选择器
2. 第二个参数:如果写一个字符串就是标签中的内容
h()函数返回的就是一个vNode
*/
let vnode = h("div#container.cls", "Hello Q7Long")
// 我们这里需要将创建的虚拟dom,最终要渲染到index.html中id="app"这个div中,所以这里需要获取一下该div
let app = document.querySelector("#app")
​
/* 将虚拟 DOM 渲染到 app 的 div 中的话,需要用到 patch() 函数 
patch()函数的作用就是对比两个 vnode 的差异,来更新真实的DOM,我们这里只是创建了一个虚拟DOM
那么我们第一个参数就可以写成一个真实的DOM,path()会将真实的DOM转换成 vnode
第二个参数是一个vnode,
path()函数的返回结果也是一个vnode
*/
let oldNode = patch(app, vnode)
// 我们在这里创建一个新的vNode
vnode = h("div", "Hello Vue")
//patch()的两个参数都是vNode 对比新旧两个vNode的差异,新vNode替换旧vNode
patch(oldNode, vnode)

在上面的代码中,我们又创建了一个虚拟 DOM , vnode 。然后把这个 vnodeoldNode 进行对比,最后渲染到页面中。

patch( ) 函数的两个参数都是 vNode 对比新旧两个 vNode 的差异,新vNode 替换 旧vNode

注意启动项目是在 snabbdom-test 文件夹下使用 npm run dev 结果如下:

Virtual DOM的实现原理总结超详细版(一)

js 目录下面在创建一个文件: 02-basicusage.js

实现的代码如下:

// 本案例实现的要求是:在div中设置子元素h1,p
import { h, init } from "snabbdom";
let patch = init([]);
// h函数的第二个参数可以是一个数组,在该数组中添加所要创建的子元素。
let vnode = h("div#container", [
  h("h1", "Q7Long"),
  h("p", "这是一个p标签"),
]);
let app = document.querySelector("#app");
patch(app, vnode);

同时还需要修改 index.html 文件中的引入。

<body>
    <div id="app"></div>
    <script src="./js/02-basicusage.js"></script>
  </body>

这时,可以在浏览器中查看更新后的内容。

Virtual DOM的实现原理总结超详细版(一)

下面我们再来看另外一个问题,就是模拟从服务器获取数据,然后更新页面中的内容。

在上面的代码中,再增加如下的内容:

// 本案例实现的要求是:在div中设置子元素h1,p
import { h, init } from "snabbdom";
let patch = init([]);
// h函数的第二个参数可以是一个数组,在该数组中添加所要创建的子元素。
let vnode = h("div#container", [
  h("h1", "Q7Long"),
  h("p", "这是一个p标签"),
]);
let app = document.querySelector("#app");
// 记录更新后的VNode
let oldVnode = patch(app, vnode);
// 在两秒钟之后进行页面的更新
setTimeout(() => {
  vnode = h("div#container", [h("h1", "Hello Q7Long"), h("p", "Hello p标签")]);
  patch(oldVnode, vnode);
}, 2000);

在上面的代码中,首先记录第一次 patch 方法更新后的 vnode ,同时在2秒钟以后,通过 h 函数重新创建了一个虚拟 DOM ,并且通过 patch 函数与原有的虚拟 DOM 进行比较,然后重新更新页面内容。

2秒钟以后清空节点内容

let a = patch(oldVnode, vnode);
patch(a, h("!"));//第二个参数,表示创建一个注释节点。

02-basicusage.js

/* 02-basicusage.js */
import { h, init } from "snabbdom";
let patch = init([]);
// h函数的第二个参数可以是一个数组,在该数组中添加所要创建的子元素。
let vnode = h("div#container", [
    h("h1", "Q7Long"),
    h("p", "这是一个p标签"),
]);
let app = document.querySelector("#app");
// 记录更新后的VNode
let oldVnode = patch(app, vnode);
// 两秒后清除内容,创建一个注释节点,替换名为 a 的 vNode
setTimeout(() => {
    let a = patch(oldVnode, vnode);
    patch(a, h("!"));//第二个参数,表示创建一个注释节点。
}, 2000);

页面效果:

Virtual DOM的实现原理总结超详细版(一)

4.4 模块

Snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要处理,可以使用模块。

Snabbdom 模块系统是 Snabbdom 提供的一套 **可拓展**、**可灵活组合** 的模块系统,用来为 Snabbdom 提供操作 VNode 时的各种模块支持,如我们组建需要设置 DOM 元素的属性则引入对应的 attributesModule ,需要处理事件,则引入 eventListenersModule ,支持按需引入。

Snabbdom 模块系统的特点可以概括为:支持按需引入、独立管理、职责单一、方便组合复用、可维护性强。

常用模块

官方提供了6个模块

模块名称模块功能
attributesModule设置 DOM 元素的属性,内部使用 setAttribute()来设置属性,,处理布尔类型的属性(可以对布尔类型的属性作相应的判断处理,布尔类型的属性,我们比较熟悉的有 selected, checked 等)。
propsModuleattributes 模块类似,设置 DOM 元素的属性 element[attr]=value ,不处理布尔类型的属性。
classModule切换样式类,注意:给元素设置类样式是通过 sel 选择器。
datasetModule设置 HTML5 中的以 (data- *) 开头的自定义属性,然后可以使用 HTMLElement.dataset 属性访问它们。
eventListenersModule注册和移除事件,为 DOM 元素绑定事件监听器。
styleModule设置行内样式,支持动画(内部创建 transitionend 事件),会增加额外的属性:delayed / remove / destory

使用模块的步骤:

第一步:导入需要的模块

第二步:在 init() 中注册模块

第三步:使用 h 函数创建 VNode 的时候,可以把第二个参数设置为对象(对象中是模块需要的数据,可以设置行内样式、事件等),其它参数往后移。

下面我们要实现的案例,就是给 div 添加一个背景,同时为其添加一个单击事件,当然在 div 中还要创建两个元素分别是 h1p

具体实现的代码如下:

// 模块的基本使用
import { h, init } from 'snabbdom'
// 导入模块
import style from 'snabbdom/modules/style'
import eventListeners from 'snabbdom/modules/eventlisteners'
// 调用 init 方法注册模块
let patch = init([style, eventListeners])
// 使用h()函数创建虚拟DOM的时候,第二个参数是需要传入模块所需要的数据,这些数据需要放进对象中
let vnode = h("div", {
    // 第二个参数这里写模块所需要的数据
    style: { backgroundColor: "skyblue" },
    // 注册一个事件
    on: { click: eventHandler }
​
}, [h("h1", "Hello Q7Long"), h("p", "这是一个p标签")]
)
​
function eventHandler () {
    console.log("点击了我一下")
}
let app = document.querySelector("#app")
patch(app, vnode)

注意:在index.html文件中要引入以上代码所在的js文件,如下所示:

 <body>
    <div id="app"></div>
    <script src="./js/03-modules.js"></script>
  </body>

Virtual DOM的实现原理总结超详细版(一)

5、Snabbdom源码解读

Snabbdom 的核心:

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

5.1 h函数

h 函数介绍

在使用 Vue 的时候加过 h( )函数

new Vue({
   router,
   render:h=>h(App)
}).$mount('#app')

render 函数的参数就是 Snabbdom中的 h 函数,当然在 Vue 中将 h 函数做了一定的修改,可以用来支持组件,原有的 Snabbdom中的 h 函数不支持组件的内容。在 Snabbdomh( ) 函数的作用就是用来创建 VNode.

在看源码之前,先来了解一个概念:函数重载

因为在源码中用到了 函数重载

所谓的函数重载: **参数个数** 或 **类型 ** 不同的函数,称之为函数重载

但是在 JavaScript 中没有重载的概念,在 TypeScript 中是有重载的。

我们来看一段重载的示例代码

function add(a,b){
    return a+b
}
function add(a,b,c){
    console.log(a+b+c)
}
add(1,2)    // NaN
add(1,2,3)  // 6

以上我们是通过参数个数的形式,展示了一段函数重载的代码。

下面我们来看一下 h 函数的源码,h()函数返回的是VNode。

Virtual DOM的实现原理总结超详细版(一)

源码位置:node_modules/snabbdom/src/h.ts

// h函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
// h函数重载的具体实现
// h函数可以接收三个参数,?表示该参数可以不传递
export function h(sel: any, b?: any, c?: any): VNode {
    // 定义变量
  var data: VNodeData = {}, children: any, text: any, i: number;
    // 处理参数,实现重载的机制
    // 如果c这个参数的值不等于undefined,说明是传递了c这个参数。表示传递了三个参数
  if (c !== undefined) {
      // 如果该条件成立,表示处理的就是有三个参数的情况
      // 参数b中存储的就是模块处理的时候,需要的数据,在modules.js中写过例如:行内样式(style: { backgroundColor: "skyblue" },),事件(on: { click: eventHandler })等,关于这块在前面的案例中也写过,在这里将b参数的值赋给了data这个变量
    data = b;
      // 下面是对参数c进行了判断。
      // 关于参数c有三种情况,第一种情况为数组,第二种情况为字符串或者是数字,第三种情况为VNode.
      // 首先判断参数c是否为数组,如果是数组,赋值给了children这个变量,表明c是子元素。
      // 例如前面我们在使用模块的案例中,给h函数指定的第三个参数就为数组:[h("h1", "Hello Vue"), h("p", "这是p标签")]
    if (is.array(c)) { children = c; }         // c 是数组
      // 如果 c 参数是字符串或者是数字,将参数c赋值给了text变量,表明传递过来的内容其实就是标签中的文本内容
    else if (is.primitive(c)) { text = c; }    //  c 参数是字符串或者是数字
      // 如果有sel属性,表明 c 是vnode,在这里需要转换成数组的形式,然后再赋值给children这个变量
    else if (c && c.sel) { children = [c]; }   //  c 参数是vnode
  } else if (b !== undefined) {
      // 如果该条件成立,表明处理的是两个参数的情况 在 02-basicusage.js 中写过
      // 如果b是一个数组,赋值给chilren这个变量:vnode = h("div#container", [h("h1", "Hello Vue"), h("p", "Hello p")]);
    if (is.array(b)) { children = b; }
      // 如果b是字符串或者数字:h("div", "Hello Vue");
    else if (is.primitive(b)) { text = b; }
      // 如果b是Vnode的情况
    else if (b && b.sel) { children = [b]; }
    else { data = b; }
  }
    // 判断children中有值 比如:[h("h1", "Hello Vue"), h("p", "Hello p")]
  if (children !== undefined) {
      // 对chilren进行遍历
    for (i = 0; i < children.length; ++i) {
        // 判断从chilren中取出来的内容是否为:string/number,如果是使用 vnode 创建文本的虚拟节点。
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
      // 如果是svg,添加命名空间
    addNS(data, children, sel);
  }
    // 最后返回的是整个 VNode 所以 h 函数的核心就是调用 vnode方法,来返回一个虚拟节点
  return vnode(sel, data, children, text, undefined);
};
// 导出h函数
export default h;

addNs 方法的实现如下:

function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg';
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      let childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
      }
    }
  }
}

addNs方法中就是给 data添加了命名空间,然后通过递归的方式给 chilren 中的所有子元素都添加了命名空间。

在看 VNode 这个方法的源码前,了解一下看源码必备的快捷键。

  • 光标移动到某个变量处,按 F12 快速定位到该变量的定义位置。
  • ALT + 左方向键,回到上次的代码位置
  • Ctrl + 单击,跳转到某个变量的定义处
  • 选中某个变量或方法名,按 F12 显示出该变量或方法的具体代码

5.2 VNode函数

h 函数的最后调用了 VNode 函数创建了一个虚拟节点,并返回。下面看一下 VNode 函数内部实现。

VNode 函数的代码在 vnode.ts文件中

import {Hooks} from './hooks';
import {AttachData} from './helpers/attachto'
import {VNodeStyle} from './modules/style'
import {On} from './modules/eventlisteners'
import {Attrs} from './modules/attributes'
import {Classes} from './modules/class'
import {Props} from './modules/props'
import {Dataset} from './modules/dataset'
import {Hero} from './modules/hero'export type Key = string | number;
​
/*
这里举一个 h() 函数传递传递三个参数的例子,方便下面的理解
let vnode = h("div#container", {
    // 第二个参数这里写模块所需要的数据
    style: { backgroundColor: "skyblue" },
    // 注册一个事件
    on: { click: eventHandler }
​
}, [h("h1", "Hello Q7Long"), h("p", "这是一个p标签")]
)
*/export interface VNode {
  // sel选择器,就是我们在调用 h() 函数的时候,传递的第一个参数。比如上面的 "div#container"
​
  sel: string | undefined;
  // data节点的数据,里面就包括了 属性/样式/事件等
  data: VNodeData | undefined;
  // children子节点:VNode是用来描述真实DOM的,如果描述真实DOM中有子节点,我们可以通过     children来表示这些子节点
  children: Array<VNode | string> | undefined;
  // 记录的是VNode对应的真实的DOM,也就是将VNode虚拟节点,转成真实DOM之后,这个真实DOM就会被会存储到elm中
  elm: Node | undefined;
  // 表示的就是节点当中的内容
  text: string | undefined;
  // 优化的作用
  key: Key | undefined;
}
​
export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: Array<any>; // for thunks
  [key: string]: any; // for any other 3rd party module
}
​
export function vnode(sel: string | undefined,
                      data: any | undefined,
                      children: Array<VNode | string> | undefined,
                      text: string | undefined,
                      elm: Element | Text | undefined): VNode {
  let key = data === undefined ? undefined : data.key;
  // 最后返回了一个JS对象,这个其实就是 虚拟DOM
  return {sel, data, children, text, elm, key};
}
​
export default vnode;
​

在上面的代码中,我们首先关注的就是接口VNode,该接口中定义了很多的属性,而最终 vnode 这个函数返回的 VNode对象必须都要实现该接口中的这些属性。

下面可以看一下这些属性的含义

export interface VNode {
    //选择器,也就是调用h函数的时候传递的第一个参数
  sel: string | undefined;
    // 节点数据:属性/样式/事件等。
  data: VNodeData | undefined;
    //子节点,和text互斥  VNode是描述真实DOM的,如果所描述的真实DOM中有子节点,通过children来表示这些子节点
  children: Array<VNode | string> | undefined;
    // 记录vnode对应的真是DOM,将Vnode转换成真实DOM以后,会存储到elm这个属性中。关于这一点可以在将VNode转换成真实DOM的时候看到。
  elm: Node | undefined;
    // 节点中的内容,和children只能互斥
  text: string | undefined;
    //优化,关于这个属性可以在将VNode转换成真实DOM的时候看到。
  key: Key | undefined;
}

最后看一下 vnode 这个函数,返回的就是一个 js 对象,该对象中包含了 VNode 这个接口中的属性,而这个 js 对象就行虚拟节点。而这个虚拟的节点是怎样转换成真实的 DOM?后面会重点讲解这块内容。

5.3 复习h函数与Vnode函数应用

下面我们在通过一个案例,复习一下 h 函数与 vnode 函数。

假如,我们创建了如下的一个虚拟 DOM

test.js

// 构造一个虚拟dom
var vnode = h('div#app',
  {style: {color: '#000'}},
  [
    h('span', {style: {fontWeight: 'bold'}}, "my name is Q7Long"),
    ' and xxxx',
    h('a', {props: {href: '/foo'}}, '我是张祺龙')
  ]
);

下面看一下上面的代码的执行流程。

注意:这边先执行的是先内部的调用,然后再依次往外执行调用。

因此首先调用和执行的代码是:

第一步先在 test.js 文件中执行: h('span', {style: {fontWeight: 'bold'}}, "my name is Q7Long"),第二步把参数传递到 h.ts 文件中,进入 h 函数之后:

sel参数的值是: 'span',

b参数的值是 {style: {fontWeight: 'bold'}},

c参数的值是 "my name is Q7Long";

首先判断 if (c !== undefined) {} 代码,然后进入if语句内部代码,如下:

if (c !== undefined) {
  data = b;           // 这里将b的值 {style: {fontWeight: 'bold'}} 给了 data
  if (is.array(c)) { children = c; }
  else if (is.primitive(c)) { text = c; } //这里将c的值"my name is Q7Long"给了text
}

因此data = {style: {fontWeight: 'bold'}}; 然后判断 c 是否是一个数组,可以看到 c 是一个字符串,不是一个数组,因此进入 else if 语句,因此text = "my name is Q7Long"; 从代码中可以看到,这个个条件判断 c 是一个字符串成立,那么就有返回值了,就直接跳过所有的代码了,最后执行 return VNode(sel, data, children, text, undefined); 了,因此会调用 VNode函数进入文件中 snabbdom/vnode.js 代码如下:

/*
 * VNode函数如下:主要的功能是构造VNode, 把输入的参数转化为Vnode
 * @param {sel} 'span'
 * @param {data} {style: {fontWeight: 'bold'}}
 * @param {children} undefined
 * @param {text} "my name is Q7Long"
 * @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;  
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};

因此这里 data 中是没有名字为 key 的属性 var key = data.key = undefined; 最后返回值 key 的值也是一个 undefined 如下:

{ 
  sel: 'span', 
  data: {style: {fontWeight: 'bold'}},
  children: undefined,
  text: "my name is Q7Long",
  elm: undefined,
  key: undefined
}

第二步:调用h('a', {props: {href: '/foo'}}, '我是张祺龙');代码

同理:sel = 'a'; b = {props: {href: '/foo'}}, c = '我是张祺龙'; 然后执行如下代码:

if (c !== undefined) {
  data = b;
  if (is.array(c)) { children = c; }
  else if (is.primitive(c)) { text = c; }
}

因此 data = {props: {href: '/foo'}}; text = '我是张祺龙'; children = undefined; 最后也一样执行返回:

return VNode(sel, data, children, text, undefined);

因此又调用snabbdom/vnode.js 代码如下:

/*
 * VNode函数如下:主要的功能是构造VNode, 把输入的参数转化为Vnode
 * @param {sel} 'a'
 * @param {data} {props: {href: '/foo'}}
 * @param {children} undefined
 * @param {text} "我是张祺龙"
 * @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};

因此执行代码:var key = data.key = undefined; 最后返回值如下:

{
  sel: 'a',
  data: {props: {href: '/foo'}},
  children: undefined,
  text: "我是张祺龙",
  elm: undefined,
  key: undefined
}

第三步调用外层的代码,把参数传递进去,因此代码初始化变成如下,因为实现执行里面的内容,所以说里面的内容 h('span', {style: {fontWeight: 'bold'}}, "my name is Q7Long"),和 h('a', {props: {href: '/foo'}}, '我是张祺龙'); 就会改变了,这时候就会去执行外面的 h() 函数,里面的参数情况分析如下:

var vnode = h('div#app',
  {style: {color: '#000'}},
  [
    { 
      sel: 'span', 
      data: {style: {fontWeight: 'bold'}},
      children: undefined,
      text: "my name is Q7Long",
      elm: undefined,
      key: undefined
    },
    ' and xxxx',
    {
      sel: 'a',
      data: {props: {href: '/foo'}},
      children: undefined,
      text: "我是张祺龙",
      elm: undefined,
      key: undefined
    }
  ]
);

继续把参数传递进去,因此 sel = 'div#app'; b = {style: {color: '#000'}}; c的值变为如下:

c = [
  { 
    sel: 'span', 
    data: {style: {fontWeight: 'bold'}},
    children: undefined,
    text: "my name is Q7Long",
    elm: undefined,
    key: undefined
  },
  ' and xxxx',
  {
    sel: 'a',
    data: {props: {href: '/foo'}},
    children: undefined,
    text: "我是张祺龙",
    elm: undefined,
    key: undefined
  }
];

首先看 if 判断语句,if (c !== undefined) {};因此会进入if语句内部代码;

if (c !== undefined) {
  data = b;
  if (is.array(c)) { children = c; }
  else if (is.primitive(c)) { text = c; }
}

因此 data = {style: {color: '#000'}}; c 是数组的话,就把c赋值给 children ; 因此 children 值为如下

children = [
  { 
    sel: 'span', 
    data: {style: {fontWeight: 'bold'}},
    children: undefined,
    text: "my name is Q7Long",
    elm: undefined,
    key: undefined
  },
  ' and xxxx',
  {
    sel: 'a',
    data: {props: {href: '/foo'}},
    children: undefined,
    text: "我是张祺龙",
    elm: undefined,
    key: undefined
  }
];

我们下面接着看 如下代码:

if (is.array(children)) {
  for (i = 0; i < children.length; ++i) {
    if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
  }
}

如上代码,判断如果 children 是一个数组的话,就循环该数组 children; 从上面我们知道 children 长度为3,因此会循环3次。进入 for 循环内部。判断其中一项是否是数字和字符串类型,因此只有 ' and xxxx'符合要求,这里由于是第二个参数,那么这里的 i 就等于 1 了 ,因此 children[1] = VNode(undefined, undefined, undefined, ' and xxxx',undefined,undefined); 最后会调用 snabbdom/vnode.js代码如下:

module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};

通过上面的代码可知,我们最后返回的是如下:

children[1] = {
  sel: undefined,
  data: undefined,
  children: undefined,
  text: ' and xxxx',
  elm: undefined,
  key: undefined
};

执行完成后,我们最后返回代码:return VNode(sel, data, children, text, undefined);因此会继续调用snabbdom/vnode.js代码如下:

/*
 @param {sel} 'div#app'
 @param {data} {style: {color: '#000'}}
 @param {children} 值变为如下:
 children = [
    { 
      sel: 'span', 
      data: {style: {fontWeight: 'bold'}},
      children: undefined,
      text: "my name is Q7Long",
      elm: undefined,
      key: undefined
    },
    {
      sel: undefined,
      data: undefined,
      children: undefined,
      text: ' and xxxx',
      elm: undefined,
      key: undefined
    },
    {
      sel: 'a',
      data: {props: {href: '/foo'}},
      children: undefined,
      text: "我是张祺龙",
      elm: undefined,
      key: undefined
    }
 ];
 @param {text} undefined
 @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};

因此继续执行内部代码:var key = undefined;最后返回代码:

return {
  sel: sel, 
  data: data, 
  children: children,
  text: text, 
  elm: elm, 
  key: key
};

因此最后构造一个虚拟 dom 返回的值为如下:

vnode = {
  sel: 'div#app',
  data: {style: {color: '#000'}},
  children: [
    { 
      sel: 'span', 
      data: {style: {fontWeight: 'bold'}},
      children: undefined,
      text: "my name is Q7Long",
      elm: undefined,
      key: undefined
    },
    {
      sel: undefined,
      data: undefined,
      children: undefined,
      text: ' and xxxx',
      elm: undefined,
      key: undefined
    },
    {
      sel: 'a',
      data: {props: {href: '/foo'}},
      children: undefined,
      text: "我是张祺龙",
      elm: undefined,
      key: undefined
    }
  ],
  text: undefined,
  elm: undefined,
  key: undefined
}

接着往下执行如下代码:

// 初始化容器
var app = document.getElementById('app');

//这里将虚拟DOM转成真实DOM之后,然后调用patch()函数将app的DOM做一个替换,将vnode patch 到 app 中,但是我们这里只是说了虚拟DOM产生的过程,暂时并没有将虚拟DOM转成真实DOM,所以目前步骤并不完整
patch(app, vnode);

以上就是生成整个虚拟 DOM的过程。

5.3 patch函数的执行过程

将虚拟 DOM 渲染成真实的 DOM ,最主要的就是 patch() 函数

patch() 函数的作用就是将新旧节点进行比较,当我们使用 patch() 函数的时候,会传入两个参数(两个虚拟节点),然后 patch() 函数就会对两个节点进行比较,把节点中变化的内容渲染到真实的 DOM 中,如果没有两个虚拟节点的话,也可以传入一个真实的 DOM ,它会自动将真实 DOM 转换成一个虚拟节点

patchTest.js

patch(oldVnode, newVnode)

patch() 函数的执行过程:

patch() 作用:把新节点中的内容渲染到真实的 DOM 中,返回最后的新节点,新节点也是一个 Vnode ,新节点也会作为下次比较的旧节点

对比新旧两个 Vnode 是否是相同节点(根据节点的 key 和节点的 selsel 是选择器, key 是节点的唯一值,对两块内容进行比较如果一样就是相同节点)

如果不是相同节点,将原来的内容删除掉,重新渲染新的内容

如果是相同节点的话,再比较新的 Vnode 是否有 text ,如果有并且和 oldVnode 中的 text 不同的话,直接更新文本内容

如果新的 Vnodechildren ,紧接着判断子节点是否有变化,判断子节点的过程就是 diff 算法

diff 算法比较的过程只是在同层级进行比较。

如何获取到的 patch() 函数的,是通过调用 init() 函数之后的返回值是 patch() 函数

let patch = init([])

所以我们这里先看一下 init() 函数

5.4 init()函数源码查看

源码位置:node_modules/snabbdom/src/snabbdom.ts 48行

//4. hooks就是一个数组,数组里面有一些内容,这里面都是模块中的钩子函数,hooks里面存储的就是所有模块中钩子函数的名称
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export {h} from './h';
export {thunk} from './thunk';

// 当我们调用函数的时候,我们会传过来一个数组,
// 第二个参数有个? 代表是可有可无的参数
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);
//1. 如果不传入第二个参数,那么这里 domApi 就是 undefined 条件成立,拿到的结果是 htmlDomApi,ctrl进入htmlDomApi
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
//3. 这里是对hooks进行遍历 ctrl找到hooks
/*4.
   hooks = ['create', 'activate', 'update', 'remove', 'destroy']
   遍历这些钩子,然后从 modules 的各个模块中找到相应的方法,比如:directives 中的 create、update、destroy 方法
   让这些方法放到 cb[hook] = [hook 方法] 中,比如: cb.create = [fn1, fn2, ...]
   然后在合适的时间调用相应的钩子方法完成对应的操作
   */
  for (i = 0; i < hooks.length; ++i) {
    //7.比如 cbs.create=[],cbs.update=[]
    cbs[hooks[i]] = [];   
    for (j = 0; j < modules.length; ++j) {
      //8. 每循环一次将模块中的钩子函数拿出来放入 hook 里面
      const hook = modules[j][hooks[i]];
      //9. 这里判断 hooks 不等 undefined 的话,直接取出来放入cbd对应的钩子函数名称里面的数组中
      if (hook !== undefined) {
        //5. 这里说明 钩子函数的名称对应的是数组,值就是为钩子函数添加的一个具体的函数,比如 fn1 fn2,遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组
        (cbs[hooks[i]] as Array<any>).push(hook);
        //10. cbs最后的格式就是 cbs={create:[fn1,fn2],update:[fn]}
      }
    }
  }
  // 11.一些辅助函数
  function emptyNodeAt(elm: Element) {
      ...
  }

  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
      ...
  }

  function addVnodes(parentElm: Node,) {
      ...
  }

  function removeVnodes(parentElm: Node,): void {
      ...
  }

  function updateChildren(parentElm: Node) {
     ...
  }

  function patchVnode(oldVnode: VNode, vnode: VNode) {
      ...
  }
  //12. 返回一个 patch 函数,如果一个函数返回一个函数,那么这个init()函数就被称为高阶函数
  /*13. 使用高阶函数的优点,patch函数里面传入了 oldVNode 和 newVNode 并且这里会用到一些模块,DOM中的Api
  当我们调用init()函数的时候,DOM中的Api和模块,当我们调用init()高阶函数的时候,这两个参数就已经传入进来了
  由于我们patch()函数在用到这两个参数的时候,就不需要再次传入了,直接拿来使用即可
  */
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    }

    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      elm = oldVnode.elm as Node;
      parent = api.parentNode(elm);

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        // 14. 调用了DOM中的Api
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}

源码位置:node_modules/snabbdom/src/htmlDomApi.ts 80行

// 2. 这里面都是对DOM操作的一些方法,虚拟DOM最终都是通过htmlDomApi中提供的方法,将虚拟DOM转成了真实的DOM
export const htmlDomApi = {
  createElement,
  createElementNS,
  createTextNode,
  createComment,
  insertBefore,
  removeChild,
  appendChild,
  parentNode,
  nextSibling,
  tagName,
  setTextContent,
  getTextContent,
  isElement,
  isText,
  isComment,
} as DOMAPI;

export default htmlDomApi;

源码位置:node_modules/snabbdom/src/modules/style.ts 96行

// 这里拿一个styleModule举例,说明模块中存在钩子函数
export const styleModule = {
  pre: forceReflow,
  // 6. 这里说明模块里面确实有钩子函数
  create: updateStyle,
  update: updateStyle,
  destroy: applyDestroyStyle,
  remove: applyRemoveStyle
} as Module;
export default styleModule;

5.5 patch()函数源码查看

snabbdom 中我们 通过 init() 返回了一个 patch() 函数,通过 patch() 进行吧比较两个 虚拟DOM 然后添加的 真实的DOM 树上,中间比较就是我们等下要说的diff

Virtual DOM的实现原理总结超详细版(一)

当创建 Vue 实例的时候,会执行以下代码:

updateComponent = () => {
    const vnode = vm._render();
    vm._update(vnode)
}
vm._watcher = new Watcher(vm, updateComponent, noop)

例如当 data 中定义了一个变量 a,并且模板中也使用了它,那么这里生成的 Watcher 就会加入到 a 的订阅者列表中。当 a 发生改变时,对应的订阅者收到变动信息,这时候就会触发 Watcherupdate 方法,实际 update 最后调用的就是在这里声明的 updateComponent。 当数据发生改变时会触发回调函数 updateComponentupdateComponent 是对patch 过程的封装。patch 的本质是将新旧 vnode进行比较,创建、删除或者更新 DOM节点/组件实例。

源码位置:node_modules/snabbdom/src/snabbdom.ts 280行左右

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    // 定义一个数组,这其实是个队列,保存了新插入节点的队列,为了触发以后的钩子函数
    const insertedVnodeQueue: VNodeQueue = [];
    // 循环执行了模块中的 pre 钩子函数,一般在pre完成一些预处理操作,模块中有pre钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
    // oldVnode 有两种类型,一种是Vnode,一种是Element节点DOM元素
    if (!isVnode(oldVnode)) {
      // 如果传入的不是虚拟节点Vnode,就会转换成真实的Vnode,比如之前我们传入的 
      // let oldNode = patch(app,vnode) 这个app就是真实DOM,会被转换成VNode
      oldVnode = emptyNodeAt(oldVnode);
    }
    // 判断 oldVnode 和 newVnode 是否为同一虚拟节点,通过 key 和 sel 进行判断
    if (sameVnode(oldVnode, vnode)) {
      // 是同一个虚拟节点 调用 patchVnode 方法,找到节点中不同的地方,更新DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      // 如果新旧节点不是同一个节点,那么会将新节点渲染成DOM插入到文档中,并且将老节点从文档中移除
      // 不是同一虚拟个节点 直接暴力拆掉老节点,新节点渲染成DOM插入文档中,老节点不要了
      // oldVnode.elm 获取DOM元素
      elm = oldVnode.elm as Node;
      // elm就是oldVnode的DOM节点,parentNode方法获取当前elm的父节点
      parent = api.parentNode(elm);
      // 把Vnode转换成真实的DOM元素
      createElm(vnode, insertedVnodeQueue);

      // 根据oldVnode的父节点不为空
      if (parent !== null) {
		// 调用了DOM中的Api
        // 把创建好的真实的DOM渲染到parent中,具体渲染到时elm元素的后面
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        // 然后将老节点移除了,即将oldVNode移除
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    // 返回vnode作为 旧的虚拟节点
    return vnode;
  };
// 64行
function emptyNodeAt(elm: Element) {
    // 传递过来的DOM元素,首先获取DOM元素的id,如果有id的话,给id前面加入一个 # 比如传入的一个id="app"->#app
    const id = elm.id ? '#' + elm.id : '';
    // 如果有类选择器,那么在前面加上一个点. 然后按照空格进行分割,分割之后呢,加入点.进行拼接转换成字符串
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    // 最后就转换成了vnode,回去标签上的名字,比如div,连接上id选择器以及类选择器
    // 这里与 let vnode = h("div#container.cls","Hello World")类似,传入到vnode中
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
  }
调试 patch 函数,看清 patch 内部执行过程:

01-basicusage.js

import { h, thunk, init } from "snabbdom";
let patch = init([])   
let vnode = h("div#container.cls", "Hello World")
let app = document.querySelector("#app")
let oldNode = patch(app, vnode)
vnode = h("div", "Hello Vue")
patch(oldNode, vnode)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<div id="app">Hello World</div>
	<script src="./js/01-basicusage.js"></script>
</body>
</html>
// 看源码快捷键操作: F10逐行执行,F11进入函数内部
// 1.进入snabbdom-test文件夹,npm run dev启动项目
// 2. F12,Sources-js-01-basicusage.js
// 3. 在let oldNode = patch(app, vnode)处打一个断点,然后刷新页面

Virtual DOM的实现原理总结超详细版(一)

F11进入源码内部: 可以看到传入patch的第一个参数是 id="app" 的 div  第二个参数是我们自己创建的虚拟节点Vnode

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

按下F10,进行下一步,发现for循环里面,没有定义pre这个钩子函数,所以这个for循环不执行,F10进入下一步,进入isVNode方法,对oldVnode进行判断,看是不是虚拟节点Vnode,如果不是这里需要创建虚拟节点Vnode

Virtual DOM的实现原理总结超详细版(一)

按下F11进入 isVnode函数,我们发现这里传入的DOM元素中,并没有sel属性,那么就说明并不是一个Vnode,而是一个DOM元素,这里使用了 return vnode.sel !== undefined 判断了vnode.sel是否是undefined,即传过来的vnode参数中是否含有sel属性,来判断这是不是一个Vnode
// 这里的返回值结果是false
function a () {
	return 1 !== 2
}
console.log(a())    // true

Virtual DOM的实现原理总结超详细版(一)

判断传入的是否为Vnode之后,if(!isVnode(oldVnode)),条件成立,那么就执行 
oldVnode = emptyNodeAt(oldVnode)将真实的 DOM 转换成 Vnode,按下F11进入emptyNodeAt函数

Virtual DOM的实现原理总结超详细版(一)

进入 emptyNodeAt() 函数之后,这里将真实的 DOM 作为 elm 参数传入,这里的elm就是 "div#app",可以发现这里的 elm 只有 id 选择器,没有类选择器,所以 emptyNodeAt()函数里面的 id在elm.id前面加"#"之后就变成了,id="#app",而 c=" ",然后进入vnode函数

Virtual DOM的实现原理总结超详细版(一)

这里进入 vnode 方法,先执行的是 api.tagName(elm).toLowerCase() 之后,api.tagName(elm)拿到的就是标签的名字,这里的标签名字就是 DIV

Virtual DOM的实现原理总结超详细版(一)

然后进入 vnode 方法,将真实的 DOM 转成真实的 vnode

Virtual DOM的实现原理总结超详细版(一)

// 如果是真实DOM,就需要转为虚拟DOM。
// 如何比较? 需要知道真实DOM和虚拟DOM有何区别,真实DOM中有很多属性,但是肯定没有sel属性,而虚拟DOM有sel属性,根据这个属性来判断是否为虚拟节点
所以这里生成的 oldVnode 是含有 sel 属性的,说明这里将真实的 DOM 转成了真实的 vnode,并且这里也将以前的真实DOM,存入了 oldVnode 的 elm 属性中

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

生成oldVnode(真实的vnode)之后,下一步按F11进入 sameVnode 方法,判断生成的 oldVnode 和 vnode 是否为同一节点

Virtual DOM的实现原理总结超详细版(一)

// 只有是同一个虚拟节点,才进行精细化比较,否则直接删除旧节点,插入新节点
// 判断两个节点是否为同一个,是根据比较选择器,也就是 sel 的值和 key 的值是否都相同,都相等则判断为同一个虚拟节点

Virtual DOM的实现原理总结超详细版(一)

// vnode1对应oldVnode vnode2对应vnode
可以发现这里的 vnode1.key = undefined vnode2.key = undefined
vnode1.sel = "div#app"  vnode2.sel="div#container.cls"  两个sel不相等说明不是同一节点

Virtual DOM的实现原理总结超详细版(一)

vnode 对应的节点是"div#container.cls",而oldVnode是 "#app" 对应的节点,很明显不是同一个

Virtual DOM的实现原理总结超详细版(一)

如果不是同一节点,不执行 patchVnode,而是执行 else 操作,首先取到 oldVnode 的 elm 属性赋值给 elm,然后通过 api.parentNode(elm) 获取 elm即 "div#app" 的父节点

Virtual DOM的实现原理总结超详细版(一)

调用api.parentNode(elm),获取 elm 的父节点,从index.html中可以看出"div#app" 的父节点是body

Virtual DOM的实现原理总结超详细版(一)

所以调用 parentNode() 方法将 body 赋值给了 parent

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

执行createElm方法,将我们传入的 vnode,转换成了真实的 DOM,这里暂时不看createElm,按下F10

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

这里是将 vnode = h("div#container.cls","hello world")在"div#app"之后,插入到 parent 中,parent 其实就是 body 元素,当执行完这一行,我们可以在页面中看到效果

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

// 在执行 removeVnodes 之前,我们可以进入 Elements 里面查看元素情况。可以发现,我们在 body里面 <div id="app"></div> 元素的后面插入了,vnode 转成真实 DOM 后的 <div id="container" class="cls">Hello World</div>

Virtual DOM的实现原理总结超详细版(一)

执行 removeVnodes(parent, [oldVnode], 0, 0); 将 oldVnode 从 body 中移除,并且查看Elements,可以看到效果

Virtual DOM的实现原理总结超详细版(一)

Virtual DOM的实现原理总结超详细版(一)

5.6 createElm()函数源码查看

源码位置:node_modules/snabbdom/src/snabbdom.ts 80行左右

// createElm,将 vnode 转换为 真实的 dom 节点
1. 判断当前的 vnode tag 标签是否是存在的
2. 如果存在,创建对应的节点,然后 设置 样式的 作用域 
3. 遍历子元素,并插入节点之中
4. 触发 create 钩子函数
5. 如果tag 标签不存在,判断是否是 注释节点,然后创建
6. 如果tag 标签不存在,且不是 注释节点,直接创建文本节点
/* 仅仅把虚拟节点Vnode转换成真实的DOM元素,并没有渲染到页面中调用 insertBefore函数的时候,才会插入
  完成三项功能:
  1. 执行用户设置的 init 钩子函数
  2. 把Vnode转换成真实的DOM元素,但是并没有渲染到页面中
  3. 把新创建的DOM元素给返回
  */
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    // data 变量中存储的是vnode中的data,实际上就是使用h()函数的第二个参数模块中的数据,比如一些样式
    if (data !== undefined) {
      // 执行用户设置的 init 钩子函数 hook 和 init 都是用户传递过来的内容
      if (isDef(i = data.hook) && isDef(i = i.init)) {
        // 执行了 init 钩子函数
        i(vnode);
        // 对 data 重新赋值,为什么?考虑到执行完 init 钩子函数,有可能对vnode中data进行修改,重新赋值
        data = vnode.data;
      }
    }
    // 把Vnode转换成真实的DOM对象 star
    // children表示vnode中的子节点
    let children = vnode.children, sel = vnode.sel;
    // sel表示选择器 如果 sel === ! 表示创建的是一个注释节点
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      // 调用createComment 这个 api 创建注释节点
      vnode.elm = api.createComment(vnode.text as string);
      // 如果 sel !== undefined 创建新的 DOM 元素
    } else if (sel !== undefined) {
      // Parse selector(解析选择器) 对选择器进行处理 下面5行代码
      // 比如有可能创建的是 div#container.cls 的形式
      // 那么首先就需要 获取 # 的位置 获取.的位置,获取div标签
      const hashIdx = sel.indexOf('#');
      const dotIdx = sel.indexOf('.', hashIdx);   
      const hash = hashIdx > 0 ? hashIdx : sel.length;
      const dot = dotIdx > 0 ? dotIdx : sel.length;
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
      //获取到标签之后,调用createElement()方法,创建对应的 DOM 元素,并且赋值给了 vnode.elm 属性
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                               : api.createElement(tag);
      // 把 Vnode 转换成真实的 DOM 对象 end
      // 给 DOM 元素设置 id 属性和 class 属性
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/./g, ' '));
      // 取出来 cbs 里面的 create 钩子函数执行 
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      // 如果 vnode中有子节点,前面已经获取children
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i];
          if (ch != null) {
            // 将子节点转成 DOM 元素,并且追加到 elm中
            // 如果 ch != null 子节点不为空的话,再次调用自己 createElm 形成递归调用,把每一个子节点转换成DOM元素,追加到elm上,形成DOM树
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
        // 如果vnode.text的值是 String或者是Number,直接调用createTextNode创建文本节点,并且追加到 elm 的DOM数上
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      // 获取hook,hook里面存储的是钩子函数
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
        // 调用用户传入的 create 钩子函数
        if (i.create) i.create(emptyNode, vnode);
        // i.insert 如果hook中有 insert钩子函数,那么将vnode添加到队列中,为后续执行 insert 钩子函数做准备
        if (i.insert) insertedVnodeQueue.push(vnode);
      }
    // 对应的是 sel === undefined,选择器为空,那么创建的就是一个文本节点
    } else {
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    // 把vnode转化成DOM元素,将vnode的elm中存储的DOM元素返回
    return vnode.elm;
  }

Virtual DOM的实现原理总结超详细版(一)

5.7 removeVnodes()函数源码查看

源码位置:node_modules/snabbdom/src/snabbdom.ts 底部调用 160行左右使用

// 理解一个函数就要先从他的参数开始理解  removeVnodes(parent, [oldVnode], 0, 0)
parentElm:要删除的元素所在的父元素
vnodes: 这个参数是一个数组,存储的时候要删除的DOM元素对应的老节点VNode
startIdx: 数组中要删除的节点的起始位置,循环起始点
endIdx: 数组中要删除的节点的结束位置,循环终止点
function removeVnodes(parentElm: Node,
                        vnodes: Array<VNode>,
                        startIdx: number,
                        endIdx: number): void {
    // 这里是根据用户传入的参数,对要截取的点 这里目前是只循环一次
    for (; startIdx <= endIdx; ++startIdx) {
      // const ch = vnodes[startIdx] 找到要删除的元素节点
      let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
      // 是否有要被删除的元素节点,说明获取到了要删除的节点
      if (ch != null) {
        // 如果有要被删除的元素节点,那么紧接着
        // 根据元素的sel属性判断是否元素节点 or 文本节点 
        // 如果 ch.sel !== undefined 元素节点  ch.sel === undefined 文本节点
        if (isDef(ch.sel)) {
          // 执行所有子节点的 destroy 的钩子函数
          invokeDestroyHook(ch);
          // 记录模块中 remove钩子函数的个数,作用防止重复调用删除节点的方法(createRmCb)
          // 获取cbs.remove的钩子函数 并 +1,这个变量的作用是防止变量重复删除DOM元素
          listeners = cbs.remove.length + 1;
          // createRmCb是个高阶函数,他是一个返回真正删除DOM元素的函数  
          // ch.elm: 要被删除的元素  listeners: 防止变量重复删除DOM元素
          rm = createRmCb(ch.elm as Node, listeners);
          // 遍历remove方法,然后传入ch和rm(也就是createRmCb返回的函数)
          for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          // 这步主要是执行用户传入的remove()钩子函数,如果有传入remove钩子函数返回该函数,没有调用的就执行createRmCb()
          if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
            i(ch, rm);
          } else {
            // 如果没有传入 remove 调用的就是createRmCb
            rm();
          }
        } else { // Text node  如果被删除的节点是文本节点的话
          // 直接调用 api.removeChild(parentElm, ch.elm!) 删除父元素中的elm属性
          api.removeChild(parentElm, ch.elm as Node);
        }
      }
    }
  }
// 上面调用了 createRmCb() 方法
function createRmCb(childElm: Node, listeners: number) {
    return function rmCb() {
      if (--listeners === 0) {
        // 根据传递过来要删除的结点,找到它的父节点
        const parent = api.parentNode(childElm);
        // 在父节点中删除传进来的结点
        api.removeChild(parent, childElm);
      }
    };
  }

5.8 addVnodes()函数源码查看

parentElm: 父元素
before: 参考节点,插入到 before 之前
vnodes:添加的节点
startIdx、endIdx: 开始结束节点 // 循环起始值 终止值
insertedVnodeQueue: 存储刚刚插入的具有inserted钩子函数的节点
通过createElm(ch, insertedVnodeQueue)创建DOM元素,插入到DOM树种
function addVnodes(parentElm: Node,
                     before: Node | null,
                     vnodes: Array<VNode>,
                     startIdx: number,
                     endIdx: number,
                     insertedVnodeQueue: VNodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      // 每一次都从vnodes里面取出来一个节点
      const ch = vnodes[startIdx];
      if (ch != null) {
        // 如果取出来的不等于 null 那么就调用 insertBefore 先调用createElm将vnode转成真实的DOM
        // 再将DOM元素插入到before之前
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

5.9 patchVnodes()函数源码查看

Virtual DOM的实现原理总结超详细版(一)

源码位置:node_modules/snabbdom/src/snabbdom.ts 底部340行左右调用 290行使用

//1. 作用 
比较新旧节点,更新两个节点之间的差异,在patch节点中才比较了新旧两个节点之间的差异
//2. 参数
oldVnode: 旧节点
vnode: 新节点
insertedVnodeQueue: 收集具有inserted钩子函数的节点
//3. 整体流程 在对比中分了很多情况
1. 先执行prepatch()钩子函数和update()钩子函数,然后在整个节点对比完之后也会触发一个钩子函数,也会触发一个postpatch钩子函数,中间内容就是新旧两个节点的对比。

2. 首先会判断新节点中是否有 text 属性,如果有并且不等于旧节点中的 text 属性,这个时候会更新DOM节点中的文本内容,并且这个更新的过程也会比较老节点是否有children属性,如果有的话,也会移除老节点中children属性中的DOM元素,并且会设置新节点中DOM元素的textContent属性(内容)

3. 如果新旧节点中都有children,并且不相等,这个时候就会调用updateChildren()来对比新老节点中的children也就是它们的子节点,找出子节点中的差异,并且更新这些差异

4. 如果只有新节点中有children属性,老节点中没有children属性,那么就会去查找老节点中是否有text属性,那么就会清空text属性中的内容,也就是去清空DOM元素中的textContent,清空文本内容了之后,然后添加所有的子节点

5. 如果只有老节点中有children属性,更新的话就需要将老节点中所有的children都给移除

6. 如果只有老节点中有text属性,新节点中没有text属性,那么就会清空DOM元素中的textContent属性,就是清空所有的文本内容。
// 使用 patchVnode 方法前情提要
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    // 定义一个数组,这其实是个队列,保存了新插入节点的队列,为了触发以后的钩子函数
    const insertedVnodeQueue: VNodeQueue = [];
    // 循环执行了模块中的 pre 钩子函数,一般在pre完成一些预处理操作,模块中有pre钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
    // oldVnode 有两种类型,一种是Vnode,一种是Element节点DOM元素
    if (!isVnode(oldVnode)) {
      // 如果传入的不是虚拟节点Vnode,就会转换成真实的Vnode,比如之前我们传入的 
      // let oldNode = patch(app,vnode) 这个app就是真实DOM,会被转换成VNode
      oldVnode = emptyNodeAt(oldVnode);
    }
    // 判断 oldVnode 和 newVnode 是否为同一虚拟节点,通过 key 和 sel 进行判断
    if (sameVnode(oldVnode, vnode)) {
      // 是同一个虚拟节点 调用 patchVnode 方法,找到节点中不同的地方,更新DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
        .....
    }
}
// 如果发现比较的新旧节点是同一节点的话,那么调用patchVnode方法,里面涉及到 diff 算法
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    let i: any, hook: any;
    // 判断用户是否设置了 patchVnode这个钩子函数,如果设置了,那么会立即执行这个钩子函数
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
    }
    // 获取到DOM元素,并将其赋值给 elm 常量中 elm常量中保存了老节点中的DOM元素
    const elm = vnode.elm = (oldVnode.elm as Node);
    // 获取老节点中所有的子节点
    let oldCh = oldVnode.children;
    // 获取新节点中所有的子节点
    let ch = vnode.children;
    // 判断新旧子节点是否相同节点,比较的是内存地址是否相同,如果相同那么是没有发生任何变化
    // 如果相等那么就说明没有问题,直接返回
    if (oldVnode === vnode) return;
    //新旧子节点不是相同节点时候,判断vnode里面的data是否有值,执行update钩子函数
    if (vnode.data !== undefined) {
      // 先执行模块中的update钩子函数,也会执行用户设置的update钩子函数
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      i = vnode.data.hook;
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    }
    //核心代码 真正对比新旧 vnode 差异的地方,当找到差异之后会立即更新真实DOM 
    //vnode就是新节点 如果新节点是没有有 text 属性
    if (isUndef(vnode.text)) {
      // oldCh ch 在上面是新旧节点的children属性。
      //1. 判断新旧节点是否都有子节点 
      if (isDef(oldCh) && isDef(ch)) {
        // 判断新老节点的子节点是否相同,如果不相同,调用updateChildren()函数
        // 这个函数做了什么? 完成新老节点中的children属性的对比,通过diff算法进行对比
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
      //2. 判断新节点中有children,老节点中没有children ch代表新节点中的children
      } else if (isDef(ch)) {
        // 紧跟着判断老节点中是否有 text 属性,如果有就调用setTextContent方法,设置文本内容,但是内容是空字符串,意思就是清空DOM元素的内容
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
       // 批量添加子节点到页面中,对新节点中的子节点进行一个批量添加的操作,页面中就有了新节点中子节点的内容
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
      //3. 只有老节点中才有子节点
      } else if (isDef(oldCh)) {
      //调用removeVnodes()方法,删除Vnode,批量将子节点从DOM中删除,老节点中的children全部remove删除
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      //4. 只有老节点中有text属性
      } else if (isDef(oldVnode.text)) {
        // 把老节点中的内容清空  elm里面是存的老节点
        api.setTextContent(elm, '');
      }
      // 如果新节点有 text 属性,比较新旧节点的text属性是否不相同,相同则不进行任何操作
      // 新老节点中的text属性不相等的话,那么就会更新文本的操作
    } else if (oldVnode.text !== vnode.text) {
      // 判断老节点是否有子节点,有的话调用removeVnodes移除老节点中的children,把老节点中的DOM全部移除
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      // 将老节点中的DOM元素移除之后,再调用setTextContent()方法,更新文本内容
      api.setTextContent(elm, vnode.text as string);
    }
    // 触发 postpatch 钩子函数
    if (isDef(hook) && isDef(i = hook.postpatch)) {
      i(oldVnode, vnode);
    }
  }

// patchVnode中有一个关键的方法是 updateChildren(),里面涉及到了diff算法,下一篇中讲解
                 Vue虚拟DOM(一) Over  made by Q7Long 2022-10-28 20:26