Vue.js高阶特性及原理实现 - Virtual DOM

349 阅读9分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

Virtual DOM

课程目标

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

Virtual DOM - 虚拟DOM

虚拟DOM:由普通的js对象描述DOM对,减小Dom操作的性能开销

// 使用virtual Dom来描述真实DOM,表示真实对象内部的一些核心属性

{
  sel: 'div',
  data: {},
  children: undefined,
  text: 'hello Vitual Dom',
  elm: undefined,
  key: undefined
}

为什么使用Virtual DOM

  • Dom书写复杂,操作起来消耗性能
  • MVVM框架解决视图和状态同步问题
  • 模版引擎可以简化视图操作,但没办法跟踪状态
  • 虚拟DOM可以跟踪状态变化

可以参考gitHub上的Vittual-Dom

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

虚拟DOM作用

  • 维护视图和状态的关系
  • 在视图复杂的情况下提升渲染性能
  • 跨平台:浏览器、服务端、原生应用、小程序等

虚拟DOM库

snabbdom库

  • Vue2.xx使用的虚拟DOM就是基于snabbdom改造的
  • 核心功能行数大约200行
  • 通过模块可扩展:使用核心功能以外的功能可通过模块机制添加
  • 源码使用TypeScript开发
  • 最快的virtual dom之一

virtual-dom库

snabbdom基本使用

安装parcel打包工具

使用webpack也可以,但是parcel提供一个配置好的环境,方便打包

  • 初始化项目生成package.json:npm i init -y
  • 安装parcel作为开发依赖:npm i parcel-bundler -D

配置scripts

根目录新建入口文件index.html,引入需要的js文件

package.json 配置scripts字段,添加两条命令

"scripts": {
    // 使用parcel,index.html是入口文件,自动打开浏览器
    "dev": "parcel index.html --open",
    // 打包,入口文件为index.html
    "build": "parcel build index.html"
}

**安装引入并使用snabbdom**也可以参考中文文档

学习任何一个库都要先看文档,通过文档了解库的作用,看文档中提供的示例,自己快速创建一个demo,通过文档查看API使用

安装snabbdomnpm i snabbdom

//  文档demo解析,我自己的理解,但是渲染机制还不清楚

// 引入功能
import {
  // init 和 h 都是snabbdom的核心功能
  init,
  h,
  // 下面四个都是第三方功能
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
} from "snabbdom"

// 初始化snabbdom,并将功能注入进去(注意:初始化必须写,哪怕不需要注入第三方功能)
const patch = init([classModule, propsModule, styleModule, eventListenersModule])

// 获取节点
const container = document.getElementById("container");

// 使用 h 函数创建一个节点 - div元素 - id为container - 类名为 two和classes
const vnode = h("div#container.two.classes", {
  // 事件对象,在里面添加要实现的事件
  on: {
    // 点击事件
    click: someFn
  }
}, [ 
  // 这里面书写子节点
  h("span", {
    // 样式
    style: {
      // 书写样式
      fontWeight: "bold"
    }
  }, "旧文本"),// 最后可以书写内容文本
  // 也可以直接书写内容文本
  "旧的",
  // 再创建一个节点
  h("a", {
    // 属性设置
    props: {
      // 设置指向地址
      href: "/foo"
    }
  }, "旧文本"),
])
// 进行第一次比对渲染
patch(container, vnode);


// 再进行创建新节点
const newVnode = h(
  "div#container.two.classes", {
    on: {
      click: anotherEventHandler
    }
  },
  [
    h(
      "span", {
        style: {
          fontWeight: "normal",
          fontStyle: "italic"
        }
      },
      "新文本"
    ),
    " 新的",
    h("a", {
      props: {
        href: "/bar"
      }
    }, "新文本"),
  ]
)
// 再进行比对渲染
patch(vnode, newVnode)


// 两个事件函数
function someFn() {
  console.log(111)
}

function anotherEventHandler() {
  console.log(222)
}

基本使用

// 引入功能:init 和 h 都是snabbdom的核心功能
import { init, h } from "snabbdom"

// 创建虚拟节点
let vnode = h('div#app.content', '我是虚拟节点内容')
// 获取当前存在的节点
const app = document.getElementById('app')
// 初始化 snabbdom,必须写,就算 [] 为空也要写
const patch = init([])
// 进行比对更新节点,这里使用 oldnode 接收一下此次状态,以便下次进行比对
let oldnode = patch(app, vnode)
// 重新设置虚拟节点
vnode = h('p.text', '我是新的虚拟节点内容')
// 再次进行比对渲染,同样存储这次的状态
oldnode = patch(oldnode, vnode)

注意:如果init和h引入出现问题可能需要修改引入路径,我这里按照最新的版本使用没有发现问题

包含子节点

// 引入功能:init 和 h 都是snabbdom的核心功能
import { init, h, vnode } from "snabbdom"

// 获取当前存在的节点
const app = document.getElementById('app')

// 初始化 snabbdom,必须写,就算 [] 为空也要写
const patch = init([])

// 创建虚拟节点
let vNode = h('div#box', [
  // 把参数2设置为数组可以在里面书写子节点
  // 使用 h 函数创建虚拟节点
  h('h1', '标题'),
  h('p', '内容'),
  '文本可以直接传入,不需要使用h函数'
])

// 进行虚拟节点渲染
let oldNode = patch(app, vNode)


// 特殊写法,设置为注释绩点可实现清空
patch(vNode, h('!'))

模块使用

模块作用

  • snabbdom核心功能无法处理DOM的属性、样式、事件等
  • 使用snabbdom中默认提供的模块来实现,模块可以扩充snabbdom功能
  • 模块的实现是利用注册在全局的生命周期钩子函数来实现的

官方提供以下模块

  • attribures:进行固有属性处理
  • props:和上面一样,但是无法处理布尔类型的属性
  • dataset:用于处理data-开头的自定义属性
  • class:进行类切换,但是本身核心功能就已经有了,所以一般不用
  • style:进行样式处理
  • eventlisteners:进行事件处理

使用步骤

  • 导入需要的模块
  • init中注册模块
  • h函数第二个参数使用模块
// 引入功能:init 和 h 都是snabbdom的核心功能
// 引入模块的时候要注意引入名称不要写错
import { init, h, classModule ,propsModule ,styleModule ,eventListenersModule } from "snabbdom"

// 获取当前存在的节点
const app = document.getElementById('app')

// 在初始化的时候注入模块
const patch = init([classModule ,propsModule ,styleModule ,eventListenersModule])

// 创建虚拟节点
let vNode = h('div#box', {
  // 参数2的位置使用对象进行模块使用
  // 使用style模块
  style: {
    backgroundColor: 'red',
    // 注意这里添加的是行内样式,需要写单位
    width: '200px',
    height: '200px'
  },
  // 使用事件模块
  on: {
    // 添加click方法
    click: c
  }
}, [
  // 子节点
  h('h1', '标题'),
  h('p', '内容'),
  '文本可以直接传入,不需要使用h函数'
])

// 进行虚拟节点渲染
let oldNode = patch(app, vNode)


// 事件函数
function c() {
  console.log(1111)
}

Snabbdom源码解析

学习源码

  • 宏观了解
  • 带着目标看源码
  • 看源码的过程要不求甚解
  • 调试
  • 参考资料

核心功能

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

源码克隆

使用官方githyb源码地址 - 课程使用版本v2.1.0 - 我使用的v3.0.1

  • 克隆代码git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git

  • 命令解析:克隆 + v2.1.0版本 + 最后一次提交 + 地址

  • 结构分析:

    • .vscode目录:编辑器配置(不需要关注)
    • examples目录:官方提供的示例demo
    • perf目录:性能测试(不需要关注)
    • src目录:源码文件
    • 剩下一些配置文件不需要关注
  • 安装依赖:npm i

  • 打包:npm run compile - 这里打包命令是compile,和vue不一样,打包后的vuild目录就是我们在其他项目安装依赖的时候使用的依赖了

  • 运行examples/reorder-animation/index.html查看效果,使用liveserver运行,不要使用文件直接打开,我们可能会发现点击排序、删除按钮都无效,这是因为这个示例里面的代码没有跟随版本更新而更新,需要我们手动修改一下

  • 可以尝试修改examples/reorder-animation/index.js里面的代码,找到里面的所有click事件,将click: [remove, movie]修改为click () { remove(movie) }的形式

h()函数

用来创建vNode - 虚拟DOM对象

Vue中使用h函数是在Vue实例中添加了render选项,在render选项中使用了h函数,简单来说就是Vue中依赖reader选项内部使用h函数做虚拟DOM操作

new Vue({
  router,
  store,
  // 使用h函数进行DOM操作
  render: h => h(App)
}).$mount('#app')

函数重载:参数个数或者参数类型不同的同名函数,在调用的时候根据传入的参数不同而执行不同的代码

js中没有重载的概念,但是type script中有,不过重载的实现还需要代码调整参数

h函数中利用ts函数重载,在内部通过判断传入的参数数量、类型不同而最终返回一个vnode的函数调用,真正的虚拟节点创建是由vnode实现的

vnode函数

函数内部只不过规定了虚拟DOM节点内部的一些属性

patch整体过程分析

语法:patch(旧节点,新节点)

  • 把新节点的比那花内容渲染到真是节点,最后返回新节点作为下一次比对的旧节点
  • 对比新旧节点是否相同,利用节点的节点唯一标识(key)和节点类型(sel)是否相同判断
    • 如果不是相同节点(sel不同)删除之前内容,重新渲染
    • 如果是相同节点
      • 对比新节点是否有text,如果有和旧节点进行比对,不同直接跟新文本内容
      • 如果新节点有子节点,判断子节点的变化

init()函数

init用于创建patch,patch函数作为init的返回值

init函数中封装了一些生命周期函数,patch作为返回值形成的闭包可以使用这些生命周期返回值,并且init接受一个数组接收其他函数功能

patch函数

进行新旧节点比对

creatElm

当新旧节点不同

根据传入的新虚拟节点以及内部子节点创建DOM元素并且返回

patchvnode

当新旧节点相同的时候负责找出差异并渲染

updattChildren整体分析

新旧虚拟节点内部同时存在子节点,进行对比

Diff算法

虚拟DOM的Diff算法:查找两棵树之间节点的差异

Snabbdom根据DOM的特点对传统的diff算法进行了优化

  • Dom操作时很少会跨级别操作节点
  • 只对比同级别节点

我听懵了。算了。 人间不值得