Virtual DOM 的实现原理

概念

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

为什么使用Virtual 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库

虚拟DOM的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能:当视图比较简单情况下,因为还要对比前后两次VIrtual DOM的差异再去操作真实的DOM,开销比直接操作DOM大,所以在复杂视图情况下才能体现VIrtual DOM的优点
  • 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js),原生应用(Weex/React Native),小程序(mpvue/uni-app)等

虚拟DOM库

  • snabbdom
  • virtual dom

Snabbdom基本使用

创建项目

  • 打包工具为了方便使用parcel(趴修)
  • 创建项目,并且安装parcel
# 创建项目目录
md snabbdom-demo

# 进入项目目录
cd md snabbdom-demo

# 创建 package.json
yarn init -y

# 本地安装parcel
yarn add parcel-bundler
  • 配置package.json
"scripts": {
  "dev": "parcel index.html  --open"
  "build": "parcel build index.html"
}
  • 创建目录结构

├── index.html (入口文件)
├── package.json
├── src
     └── 01-basicusage.js
  • 小技巧:在终端输入code . 就可以打开vscode,并且将当前目录放置到vscode中
  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>snabbdom-demo</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="./src//01-basicusage.js"></script>
  </body>
</html>

导入Snabbdom

  • 安装 snabbdom
yarn add snabbdom
  • 导入snabbdom
import { init } from 'snabbdom/init';
import { h } from 'snabbdom/h'; // helper function for creating vnodes

如果遇到下面的错误

Cannot resolve dependency 'snabbdom/init’

因为模块路径并不是 snabbdom/int,这个路径是作者在 package.json 中的 exports 字段设置的,而我们使用的打包工具不支持 exports 这个字段,webpack 4 也不支持,webpack 5 beta 支持该字段。该字段在导入 snabbdom/init 的时候会补全路径成 snabbdom/build/package/init.js。

{
  "exports": {
    "./init": "./build/package/init.js",
    "./h": "./build/package/h.js",
    "./helpers/attachto": "./build/package/helpers/attachto.js",
    "./hooks": "./build/package/hooks.js",
    "./htmldomapi": "./build/package/htmldomapi.js",
    "./is": "./build/package/is.js",
    "./jsx": "./build/package/jsx.js",
    "./modules/attributes": "./build/package/modules/attributes.js",
    "./modules/class": "./build/package/modules/class.js",
    "./modules/dataset": "./build/package/modules/dataset.js",
    "./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
    "./modules/hero": "./build/package/modules/hero.js",
    "./modules/module": "./build/package/modules/module.js",
    "./modules/props": "./build/package/modules/props.js",
    "./modules/style": "./build/package/modules/style.js",
    "./thunk": "./build/package/thunk.js",
    "./tovnode": "./build/package/tovnode.js",
    "./vnode": "./build/package/vnode.js"
  }
}

解决方法一:安装 snabbdom@0.7.4 版本

解决方法二:导入 init、h,以及模块只要把把路径补全即可。

import { h } from 'snabbdom/build/package/h';
import { init } from 'snabbdom/build/package/init';
import { classModule } from 'snabbdom/build/package/modules/class';
  • snabbdom的核心仅提供最基本的功能,只导出了三个函数init(),h(),thunk()

    • init()是一个高阶函数,返回patch()
    • h()返回虚拟节点Vnode,这个函数我们在使用Vue.js时候见过
    • thunk()是一种优化策略,可以在处理不可变数据时使用
  • 注意:导入的时候不能使用import snabbdom from “snabbdom”

    • 原因: node_modules/snabbdom.ts末尾导出使用语法是export导出API,没有使用 export default 导出默认输出

代码演示

  • 使用snabbdom@0.7.4 版本
  • init()
    • 参数:数组=>模块
    • 返回值:patch函数,作用是对比两个vnode的差异更新到真实DOM
  • path():对比前后两个Vnode,并将差异渲染到页面上
    • 正常情况下,第一个参数和第二个参数都是VNode=>oldVnode在前,newVnode在后
    • 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
    • 第二个参数:VNode(修改后的VNode)
    • 返回值:VNode
  • h()函数:创建VNode
    • 第一个参数:标签+选择器
    • 第二个参数:如果是字符串的话就是标签中的内容
  • 基础实例–hello world
  • vNode(虚拟dom)用来描述真实dom
yarn dev
import { h, init } from 'snabbdom';
// 1. hello world
//  init()函数
// 参数:数组,模块
// 返回值: patch函数,作用对比两个VNode的差异更新到真实DOM
let patch= init([])
// h()函数
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h("div#container.cls", "hello world")
let app = document.querySelector("#app")
// path()函数,正常情况下,第一个参数和第二个参数都是VNode=>oldVnode在前,newVnode在后
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNode
let oldVnode = patch(app, vnode)

在这里插入图片描述

  • 创建newVnode,对比oldVnode,重新渲染页面
import { h, init } from 'snabbdom';
// 1. hello world
//  init()函数
// 参数:数组,模块
// 返回值: patch函数,作用对比两个VNode的差异更新到真实DOM
let patch = init([])
// h()函数
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
var vnode = h("div#container.cls", "hello world")
let app = document.querySelector("#app")
// path()函数,正常情况下,第一个参数和第二个参数都是VNode=>oldVnode在前,newVnode在后
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNode
let oldVnode = patch(app, vnode)
vnode = h("div", "hello snabbdom")
patch(oldVnode, vnode)

在这里插入图片描述

  • 清空页面,创建注释虚拟节点,替换h("!")
import { h, init } from 'snabbdom';
// 1. hello world
//  init()函数
// 参数:数组,模块
// 返回值: patch函数,作用对比两个VNode的差异更新到真实DOM
let patch = init([])
// h()函数
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
var vnode = h("div#container.cls", "hello world")
let app = document.querySelector("#app")
// path()函数,正常情况下,第一个参数和第二个参数都是VNode=>oldVnode在前,newVnode在后
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNode
let oldVnode = patch(app, vnode)
// 创建注释节点
vnode = h("!")
patch(oldVnode, vnode)

在这里插入图片描述

模块

  • Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块
  • Snabbdom中的模块可以用来扩展Snabbdom的功能
  • Snabbdom中的模块的实现是通过全局注册的钩子函数来实现的
  • 常用的模块-官方提供了6个模块
    • attributes
      • 设置DOM元素的属性,使用过setAttribute()
      • 处理布尔类型属性
    • props
      • 和attributes模块相似,设置DOM元素的属性, element[attr]=value
      • 不处理布尔类型的属性
    • class
      • 切换类样式
      • 注意:给元素设置类样式是通过 sel 选择器
    • dataset
      • 设置 data-* 的自定义属性
    • eventlisteners
      • 注册和移除事件
    • style
      • 设置函数行内样式,支持动画
      • delayed/remove/destroy
import { h, init } from 'snabbdom';
// 1. 导入模块
import style from "snabbdom/modules/style"
import eventlisteners from "snabbdom/modules/eventlisteners"
// 2. 注册模块
let patch = init([style, eventlisteners])
// 3. 使用h()函数的第二个参数传入模块需要的数据(对象)
let vnode = h("div", {
  style: {
    backgroundColor: "red"
  },
  on: {
    click: clickHandler
  }
}, [
  h("h1", "hello Snabbdom"),
  h("p", "这是p标签")
])

function clickHandler () {
  console.log("click")
}

let app = document.querySelector("#app")
patch(app, vnode)

在这里插入图片描述

Snabbdom 源码解析

  • Snabbdom的核心
    • 使用h()函数创建JavaScript对象(VNode)描述真实DOM
    • init()设置模块,创建patch()
    • patch()比较新旧两个VNode
    • 把变化的内容更新到真实DOM树上
  • Snabbdom源码-src目录结构
    在这里插入图片描述

必备快捷键

当前模块定位

  • 查找模块导入的位置,选中模块=>F12
  • 返回上次定义的地方 Alt+向左键

跨模块查找函数定位

  • 查找模块函数在源模块的位置,选中模块=>F12
  • 返回上次定义的地方 Alt+向左键
  • 仅仅想看到模块的源码,而不跳转模块,Ctrl键+鼠标悬停到模块函数

h 函数

  • h()函数
    • snabbdom中的 h()函数是用来创建 VNode
    • Vue中的h()函数相对于 snabbdom中的 h()函数有所增强,实现了组件的机制,snabbdom中不支持组件的机制
new Vue({
router,
store,
render:h=>h(App)
}).$mount("#app")
  • 函数重载
    • 概念
      • 参数个数或类型不同的函数
      • JavaScript 中没有重载的概念
      • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
    • 重载的示意
function add(a, b) {
  console.log(a + b);
}
function add(a, b, c) {
  console.log(a + b + c);
}
add(1, 2);
add(1, 2, 3);

h函数源码

在这里插入图片描述
- 通过export和export default导出h函数是为了导入的时候两种形式都可以
- h函数第一个参数命名是sel,是因为第一个参数必须是选择器,第二个和第三个参数不确定,所以用b,c命名

h函数的三个私有变量
  • d data: VNodeData = {} //模块数据
  • children: any // 子节点
  • text: any // 子节点
传入三个参数
  • 调用定义的第四个h函数=>export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
  • 第二个参数一定是模块数据=>data=b
  • 传入的第三个参数(c)有三种情况:1:原数值 2:数组 3:VNode 当传入Vnode的时候,源码中会把VNode转换成数组的形式,这样便于以后的统一处理
if (c !== undefined) {
    // 有三个参数的情况
    // 将b的值,存在data中=>模块数据
    data = b;
    // 1.如果参数c是数组,将c赋值给children变量
    if (is.array(c)) { children = c; }
    // 2.如果参数c是原始值(string/number,不包含boolean),将c赋值给text变量
    else if (is.primitive(c)) { text = c; }
    // 3.如果参数c是vNode,即是虚拟节点(有sel属性),将该虚拟节点作为数组的元素,将数组赋值给children变量
    else if (c && c.sel) { children = [c]; }
  }
传入两个参数
  • 当b参数树对象,则为模块数据=>data=b 调用定义的第三个h函数=>export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
  • 当参数是非对象,即是子节点,和上面三个参数的c参数处理相同,调用定义的第二个h函数=>export function h(sel: string, data: VNodeData): VNode;
else if (b !== undefined) {
    // 1. 如果参数b是数组,将b赋值给children变量
    if (is.array(b)) { children = b; }
    // 2.如果参数b是原始值(string/number,不包含boolean),将b赋值给text变量
    else if (is.primitive(b)) { text = b; }
    // 3.如果参数b是vNode,即是虚拟节点(有sel属性),将该虚拟节点作为数组的元素,将数组赋值给children变量
    else if (b && b.sel) { children = [b]; }
    // 当参数b不是子节点,而是模块数据(对象)
    else { data = b; }
  }
变量children中的原始值(string/number)处理
if (children !== undefined) {
    // 处理children中的原始值(string/number)
    for (i = 0; i < children.length; ++i) {
      // 如果child是原始值,创建虚拟文本节点
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  • 创建Vnode节点来描述文本节点,因为我们希望children中的元素都是Vnode
    • 正常情况下h函数的children中的形式是[h(1),h(2)],所以children中的元素类型是Vnode,但有时可能传入的是[“string1”,“string2”],元素类型是文本节点,我们需要将文本节点转换成Vnode
    • 上面介绍的当h函数第三个参数传入的是Vnode的话(含有sel属性),我们也是将该Vnode最为元素保存在children中
  • 重点:children中的元素类型必须保证是Vnode
h函数第一个参数sel处理
  • 如果是svg,添加命名空间
 // 如果第一个参数传入的是svg,则给svg添加命名空间=>addNS()函数
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    // 给data添加了一个ns属性
    addNS(data, children, sel);
  }
  // 返回VNode
  return vnode(sel, data, children, text, undefined);
h函数的核心
  • 调用vnode()函数,返回虚拟节点
完整代码
  • node_modules\snabbdom\src\h.ts
import {vnode, VNode, VNodeData} from './vnode';
export type VNodes = Array<VNode>;
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
import * as is from './is';

// 辅助函数
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);
      }
    }
  }
}
// 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;
export function h(sel: any, b?: any, c?: any): VNode {
  // data是模块需要处理的数据
  var data: VNodeData = {}, children: any, text: any, i: number;
  // 处理参数,实现重载机制
  if (c !== undefined) {
    // 有三个参数的情况
    // 将b的值,存在data中=>模块数据
    data = b;
    // 1.如果参数c是数组,将c赋值给children变量
    if (is.array(c)) { children = c; }
    // 2.如果参数c是原始值(string/number,不包含boolean),将c赋值给text变量
    else if (is.primitive(c)) { text = c; }
    
    
    else if (c && c.sel) { children = [c]; }
  } else if (b !== undefined) {
    // 1. 如果参数b是数组,将b赋值给children变量
    if (is.array(b)) { children = b; }
    // 2.如果参数b是原始值(string/number,不包含boolean),将b赋值给text变量
    else if (is.primitive(b)) { text = b; }
    // 3.如果参数b是vNode,即是虚拟节点(有sel属性),将该虚拟节点作为数组的元素,将数组赋值给children变量
    else if (b && b.sel) { children = [b]; }
    // 当参数b不是子节点,而是模块数据(对象)
    else { data = b; }
  }
  if (children !== undefined) {
    // 处理children中的原始值(string/number)
    for (i = 0; i < children.length; ++i) {
      // 如果child是原始值,创建虚拟文本节点
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  // 如果第一个参数传入的是svg,则给svg添加命名空间=>addNS()函数
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    // 给data添加了一个ns属性
    addNS(data, children, sel);
  }
  // 返回VNode
  return vnode(sel, data, children, text, undefined);
};
export default h;

  • vnode函数将讲解如何创建虚拟节点

vnode函数

  • node_modules\snabbdom\src\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;
// VNode接口
export interface VNode {
  // 选择器
  sel: string | undefined;
  // 节点数据:属性/样式/事件等,必须满足VNodeData接口
  data: VNodeData | undefined;
  // 子节点,和text只能互斥
  children: Array<VNode | string> | undefined;
  // 记录 vnode 对应的真实dom
  elm: Node | undefined;
  // 节点中的内容,和children只能互斥
  text: string | undefined;
  // 优化用
  key: Key | undefined;
}
// VNodeData接口
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
}
// vnode函数返回值必须满足VNode接口
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对象
  return {sel, data, children, text, elm, key};
}

export default vnode;
  • vnode函数最终返回的是一个js对象,即使用一个vnode来描述一个虚拟节点
  • 下面的patch()函数会讲解vnode(虚拟节点)如何转换成真实dom

patch的整体过程 - VNode渲染真实DOM

整体流程

  • patch(oldVnode,oldVnode)
  • 打补丁,把新节点中变化的内容渲染到真实DOM,返回新节点作为下一次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的 key和 sel 相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容(因为一个节点的children和text是互斥的)
    • 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
    • diff过程只进行同级比较(因为很少把父节点渲染到子节点上或者把子节点渲染到父节点父节点上),例如排除,添加和移除元素
      在这里插入图片描述

init 函数

  • 文件路径:node_modules\snabbdom\src\snabbdom.ts
  • 因为init()函数内部返回了patch函数,所以学习init函数之前,先学习init函数
  • init()函数内部返回了patch函数,其他的函数都是辅助函数
  • init函数的两个参数
    • 参数一:modules=>模块数组,每个模块(Module )都是一个对象 键:钩子名称 值:钩子函数
    • 参数二:domApi=>dom操作的api,经常init函数没有传第二个参数,则使用默认的dom操作api
      在这里插入图片描述
export interface Module {
  pre: PreHook;
  create: CreateHook;
  update: UpdateHook;
  destroy: DestroyHook;
  remove: RemoveHook;
  post: PostHook;
}
export interface DOMAPI {
  createElement: (tagName: any) => HTMLElement;
  createElementNS: (namespaceURI: string, qualifiedName: string) => Element;
  createTextNode: (text: string) => Text;
  createComment: (text: string) => Comment;
  insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void;
  removeChild: (node: Node, child: Node) => void;
  appendChild: (node: Node, child: Node) => void;
  parentNode: (node: Node) => Node;
  nextSibling: (node: Node) => Node;
  tagName: (elm: Element) => string;
  setTextContent: (node: Node, text: string | null) => void;
  getTextContent: (node: Node) => string | null;
  isElement: (node: Node) => node is Element;
  isText: (node: Node) => node is Text;
  isComment: (node: Node) => node is Comment;
}
  • 所有的钩子实际上都是函数,比如create

比如我们在init函数中传入了style模块

// init初始化,返回patch函数
import style from "snabbdom/modules/style"
const patch = init([style]);
  • 我看查看style模块(对象)的生命周期钩子,该钩子函数在将来patch时候会触发
    在这里插入图片描述

  • init函数中的局部变量cbs:回调函数对象, 键:钩子名称 值:钩子函数数组,因为init的第一个参数可能传入多个模块,所以需要是钩子函数组成的数组

  // 把传入的所有模块的钩子函数,统一存储到 cbs 对象中
  // 最终构建的 cbs 对象形式 cbs={craete:[fn1,fn2],update:[],...}
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) {
        (cbs[hooks[i]] as Array<any>).push(hook);
      }
    }
  }

patch函数

  • 保存新插入节点的队列,为了触发挂载在节点上钩子函数,例如vue组件中的created等
    const insertedVnodeQueue: VNodeQueue = [];
  • 执行模块的pre钩子函数,pre:预处理,处理虚拟节点之前执行的第一个钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
  • 判断patch函数传入的第一个参数是否是VNode,因为oldVnode可以是两种类型:虚拟节点和真实的dom元素
    1.判断是否是虚拟节点;依据:传入的节点是否有sel属性
// 判断
if (!isVnode(oldVnode)) {
	// 真实dom元素转换成空虚拟节点
      oldVnode = emptyNodeAt(oldVnode);
    }
function isVnode(vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}
  1. 如果传入的oldVnode不是虚拟节点,而是真实的dom元素,我们将该真实dom元素转换成虚拟节点,
function emptyNodeAt(elm: Element) {
    // dom元素的id存在吗
    const id = elm.id ? '#' + elm.id : '';
    // dom元素的类存在吗
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    // 拼接上真实dom元素的id和类,确保唯一性 div#app.container,并且将真实dom元素赋值到创建的vnode节点的elm属性中
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
  }
  • 如果新旧节点是相同的节点(key和sel相同),如果我们没有在data(模块数据中传key值),这说明新旧两个节点的key都是undefined,js语法中 undefined===undefined
 // 如果新旧节点是相同的节点(key和sel相同)
    if (sameVnode(oldVnode, vnode)) {
      // 找节点的差异并更新(重新渲染)DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      // 如果新旧节点不同,vnode创建对应的dom元素
      elm = oldVnode.elm as Node;
      // 获取dom元素的父节点
      parent = api.parentNode(elm);
      // 创建vnode对应的DOM元素,并触发init/create钩子函数
      // createElm会将创建好的真实DOM设置到vnode.elm属性里面
      createElm(vnode, insertedVnodeQueue);
      // 如果parent有值
      if (parent !== null) {
        // 如果父节点不为空,把vnode对应的DOM插入到文档中
        // 把刚刚createElm创建的dom插入到文档中真实DOM元素之后
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        // 移除老节点
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
```typescript
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  // 两个节点的key和sel属性均相同
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
  1. 如果是相同节点,找差异,重新渲染DOM
 patchVnode(oldVnode, vnode, insertedVnodeQueue);
  1. 如果不是相同节点,把新节点渲染成DOM保存到该虚拟节点的elm属性中,把vnode.elm对应的dom元素插入到文档中来,并且将老节点从文档中移除
  • 执行钩子函数,返回vnode
    // 执行用户设置的 insert 钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    // 执行模块的post钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    // 返回vnode,作为下一次处理的老节点
    return vnode;

调试pacth

在这里插入图片描述
在这里插入图片描述

createElm函数

  • 步骤1:执行用户设置的init()函数
   // 执行用户设置的 init 钩子函数
    if (data !== undefined) {
      // data是模块数据,hook和init都是用户创建vnoded的时候传递的=>h()函数的第二个参数
      if (isDef(i = data.hook) && isDef(i = i.init)) {
        i(vnode);
        // 因为init和hook是用户传过来的,可能修改vnode中的data值,所以需要将vnode.date重新赋值给data变量
        data = vnode.data;
      }
    }
  • 把vnode转换成真实DOM对象(没有渲染到页面)
  1. 当sel(选择器)为" ! ",则创建注释节点
 if (sel === '!') {
      // 创建注释节点
      if (isUndef(vnode.text)) {
      // 确保能正常调用createCommen(text)函数
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text as string);
    }
  1. 当sel不存在时,创建文本节点
  // 如果选择器为空,则创建文本节点
    vnode.elm = api.createTextNode(vnode.text as string);
  1. 当sel不为空的情况下,要创建dom元素
  • 返回新创建的vnode
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    // 执行用户设置的 init 钩子函数
    if (data !== undefined) {
      // data是模块数据,hook和init都是用户创建vnoded的时候传递的=>h()函数的第二个参数
      if (isDef(i = data.hook) && isDef(i = i.init)) {
        i(vnode);
        // 因为init和hook是用户传过来的,可能修改vnode中的data值,所以需要将vnode.date重新赋值给data变量
        data = vnode.data;
      }
    }
    // 把vnode转换成真实的DOM对象(没有渲染到页面)
    let children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
      // 创建注释节点
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text as string);
    } else if (sel !== undefined) {
      // Parse selector
      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;
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                               : api.createElement(tag);
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
      // 以上的部分创建dom元素
      // 执行模块的的 create 钩子函数
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      // 如果vnode中有字节点,创建子vnode对应的DOM元素并追加到DOM树上
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i];
          if (ch != null) {
            // 递归调用createElm将虚拟节点转换成对应的的DOM元素并添加到elmz中
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
        // 执行用户传入的钩子create
        if (i.create) i.create(emptyNode, vnode);
        // 当前vnode中data属性如果有insert钩子函数,把当前vnode节点追加到insertedVnodeQueue这个对象中,在patch函数中页面渲染完成后,执行这个队列中每个vnode中data属性中的insert钩子
        if (i.insert) insertedVnodeQueue.push(vnode);
      }
    } else {
      // 如果选择器为空,则创建文本节点
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    // 返回新创建的DOM
    return vnode.elm;
  }

在这里插入图片描述

  • 不管是模块(style等)自带的钩子还是用户h函数的data.hooks中自定义的init/insert钩子都是在patch函数中触发
    在这里插入图片描述

createElm调试

  • src\01-basicusage.js
import { h, init } from 'snabbdom';
let patch = init([])
// h的第二个参数是模块数据,即往data中添加动态数据
let vnode = h("div#container.cls", {
  hook: {
    init (vnode) {
      // vnode当前节点
      console.log(vnode.elm)
    },
    create (emptyVnode, vnode) {
      console.log(vnode.elm)
    }
  }
}, "hello world")
let app = document.querySelector("#app")
let oldVnode = patch(app, vnode)

在这里插入图片描述

removeVnodes函数和addVnodes函数

  • removeVnodes :批量删除节点 xi
  • addVnodes:批量新增节点

removeVnodes(parentElm:Node, vnodes: Array, startIdx: number,endIdx: number): void

  • 参数一:真实dom的父节点
  • 参数二:要删除虚拟节点(vnode)组成的数组
  • 参数三:开始下标
  • 参数四:结束下标
function removeVnodes(parentElm: Node,
    vnodes: Array<VNode>,
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        // 如果 sel 有值
        if (isDef(ch.sel)) {
          // 执行 destory 钩子函数(会执行所有的detory钩子函数)
          invokeDestroyHook(ch);
          // 模块中remove钩子数组的长度,防止多次删除节点
          listeners = cbs.remove.length + 1;
          // 创建删除的回调函数
          rm = createRmCb(ch.elm as Node, listeners);
          // 触发模块中所有的remove钩子函数
          for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
            // 执行用户传入的remoce钩子函数
            i(ch, rm);
          } else {
            // 如果用户没有传入remove钩子,则执行rm回调函数
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm as Node);
        }
      }
    }
  }
 function invokeDestroyHook(vnode: VNode) {
    let i: any, j: number, data = vnode.data;
    if (data !== undefined) {
      // 执行用户传入的destroy的钩子函数
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
      // 执行模块的destroy钩子函数
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      // 如果该节点有子节点,子节点递归代用invokeDestroyHook函数
      if (vnode.children !== undefined) {
        for (j = 0; j < vnode.children.length; ++j) {
          i = vnode.children[j];
          if (i != null && typeof i !== "string") {
            invokeDestroyHook(i);
          }
        }
      }
    }
  }
  function createRmCb(childElm: Node, listeners: number) {
    return function rmCb() {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }

addVnodes函数

 function addVnodes(parentElm: Node,
    before: Node | null,
    vnodes: Array<VNode>,
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

patchVnode函数

  • 作用:当新旧两个节点不同时,patchVnode处理差异并更新视图
  • 语法 patchVnode(oldVnode, vnode, insertedVnodeQueue);
    • 参数一:旧节点
    • 参数二:新节点
    • 参数三:具有insert钩子节点的队列

在这里插入图片描述

  • 代码
 function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    let i: any, hook: any;
    //  首先执行用户设置的prepatch钩子函数
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
    }
    // 将老节点的elm属性赋值给新节点的elm属性-因为通过h函数创建的新节点的elm为undefined,老节点elm属性实际上对应的是视图上显示的DOM元素
    const elm = vnode.elm = (oldVnode.elm as Node);
    let oldCh = oldVnode.children; //老节点的children
    let ch = vnode.children; // 新节点的children
    // 如果新老节点内存地址是否相同,直接返回,与patch函数中新老节点是否相同(节点sel和key都相同)不一样
    if (oldVnode === vnode) return;
    // 当新老节点的内存地址
    if (vnode.data !== undefined) {
      // 执行模块的update钩子函数
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      i = vnode.data.hook;
      // 执行用户设置的undate钩子函数
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    }
    // 对比新旧节点的处理过程
    if (isUndef(vnode.text)) {
      // 如果新节点没有text属性
      if (isDef(oldCh) && isDef(ch)) {
        // 新老节点都有children属性(子节点)
        // 如果新老节点的子节点不相同,使用updateChildren来对比新旧节点的子节点-使用diff算法对比
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
      } else if (isDef(ch)) {
        // 只有新节点有childern属性(子节点).老节点没有children属性(老节点)
        // 如果老节点有text,清空text对应的DOM元素
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 批量新增子节点
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        // 只有老节点有childern属性(子节点).新节点没有children属性(老节点)
        // 批量删除老节点
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      } else if (isDef(oldVnode.text)) {
        // 如果老节点有text属性,清空DOM元素
        api.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新节点有text属性且不等于老节点的text属性
      if (isDef(oldCh)) {
        // 如果老节点有children,移除老节点children对应的DOM元素
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      // 将新节点的text渲染到视图上
      api.setTextContent(elm, vnode.text as string);
    }
    // 最后执行用处设置的postpatch钩子函数
    if (isDef(hook) && isDef(i = hook.postpatch)) {
      i(oldVnode, vnode);
    }
  }

updateChildren整体分析

  • 功能:diff算法的核心,对比新旧节点的children,更新DOM
  • 虚拟dom中为什么需要使用Diff算法
    渲染真实dom的开销很大,dom操作会引起浏览的重排和重绘,也就是浏览器的重新渲染,浏览器重新渲染页面是非常耗性能的,因为要重新绘制整个页面,当数据变化后,尤其是大量数据变化后,如列表的数据,如果直接操作dom,浏览器会重新渲染整个列表,虚拟dom中diff的核心是当数据发生变化后,不直接操作dom.而是用js对象来描述真实dom,当数据发生变化后,会先比较js对象是否发生变化,找到所有变化后的位置,最后只去最小化的去更新变化的位置,从而提高性能
  • 执行过程
    • 对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二棵树的每一个节点比较,但是这样的时间复杂度为o(0^3)
    • 在DOM操作的时候我们很少会把一个父节点移动/更新到某一个子节点
    • 因此只需要找同级别的子节点依次比较,然后再找下一级别子节点比较,这样算法的时间复杂是为o(n)
      在这里插入图片描述
  • 在进行同级别节点比较的时候,首先会对显老节点数组的开始和结束节点设置标记索引,遍历的过程中移动索引
  • 在对开始和结束节点比较的时候,总共有四种情况
    • oldStartVnode/newStartVnode(旧开始节点/新开始节点)
    • oldEndVnode/newStartVnode(旧结束节点/新开始节点)
    • oldStartVnode/newOldVnode(旧开始节点/新结束节点)
    • oldEndVnode/newStartVnode(旧结束节点/新开始节点)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值