摘要: 介绍 snabbdom、虚拟 DOM、diff 算法
本文重点是什么 ?
1. 虚拟 DOM
是什么,作用 ?
2. 虚拟DOM
如何产生 ?—— 以h
函数为例(其实,vue.js
中有vnode
类,可以创建不同类型的vnode
实例)
3. 虚拟DOM
最核心,将vnode
渲染成真实的DOM
— patch
函数
vnode
是一个类,可以生成不同类型的vnode
实例,而不同类型的vnode
表示不同类型的真实DOM
元素。DOM
有元素节点、文本节点和注释节点等。vnode
实例也对应有着元素节点、文本节点和注释节点。(1)注释节点
(2)文本节点
(3)元素节点
(4)组件节点
(5)函数式组件
(6)克隆节点
元素节点
为例,存在4个有效属性:(1)tag:节点名,例如
p
、ul
等(2)data:包含节点上的数据,比如
attrs
、class
和style
等(3)children:当前节点的子节点列表
(4)context:当前组件的的
vue.js
实例
真实的元素节点:
<div class="box">
<h3>我是一个标题h3>
<ul>
<li>牛奶li>
<li>咖啡li>
<li>可乐li>
ul>
div>
对应的vnode
实例:
{
children:[vnode,vnode],
context:{...},
data:{...},
tag:"p",
...
}
由于Vue.js
对组件采用了虚拟DOM
来更新视图,当属性发生变化时,整个组件都要进行重新渲染操作,但是组件内并不是所有DOM
节点都需要更新,因此,只对需要更新的部分进行DOM
操作可以提升很多性能。
其实没有那么复杂,它主要做了两件事。
DOM
节点对应的虚拟节点 vnode
;vnode
和旧的虚拟节点oldVnode
进行对比,更新视图。两个虚拟节点之间的对比是虚拟dom
中最核心的算法(patch
),它可以判断出哪些节点发生了变化,从而只对变化的节点进行更新操作。
每次渲染视图时,都是先创建 vnode
,然后使用它创建真实的DOM
插入到页面中,所以将上一次渲染视图时创建的 vnode
存储起来,之后每次重新渲染时,将新旧 vnode
进行对比,
区别于真实DOM
DOM
,提高开发效率;DOM
实际是 JavaScript
对象,可以进行跨平台操作。使用开源库 snabbdom
模拟,虚拟DOM
生成vnode
import { h } from 'snabbdom/h' // helper function for creating vnodes
// 使用 h 函数创建虚拟节点
const vnode = h('ul', {}, [
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D')
])
其中,vnode
便是h
函数创建的虚拟DOM
:vnode
实例
对比两个vode
之间的差异只是patch
的一部分,只是手段,不是目的。patch
的目的是修改DOM
节点,可以理解为渲染视图。
操作:创建节点、删除节点、修改节点。
两个虚拟节点完全不同,以新节点为标准渲染视图,是需要执行:将旧节点删除或者创建新增节点。
新旧两个节点相同,需要进行细致化对比,然后对oldVode
在新视图中对应的真实节点进行更新。
计算最小更新 DOM 的方式。
流程图:
其中包含 patch
函数、pachVnode
函数、UpdateVnode
函数(未详细介绍)
使用 snabbdom 虚拟 DOM 库 :snabbdom
/** src/index.js */
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h' // helper function for creating vnodes
// 1、创建出 patch 函数
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule
])
// 2、使用 h 函数创建虚拟节点
const vnode1 = h('ul', {}, [
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D')
])
// 3.创建空的容器
const container = document.getElementById('container')
// 4.将 DOM 塞入container
patch(container, vnode1)
const vnode2 = h('ul', {}, [
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D'),
h('li', {}, 'E')
])
// 点击按钮时,将vnode1变为vnode2
const btn = document.getElementById('btn')
btn.onclick = function () {
// 使用最小改变 来达到 新的vnode 覆盖 旧的 vnode2
patch(vnode1, vnode2)
}
接收参数存在: element(真实 DOM ) 和 vnode(虚拟 DOM ) 两种类型
首次插入
:判断 key 和 sel 是相同(2种情况)
1、vnode.key 和 vnode.sel 都相同 | 2、vnode.key 或 vnode.sel 不同 | |
---|---|---|
是否同一个Vnode | 是 | 否 |
结果 | 更新(调用 pachVnode ) |
创建新的DOM,删除老的DOM |
虚拟节点 text 和 children 最多只能有一个
1. 保存(旧的相关联)的 DOM 元素 eml 给新的 Vnode(eml 记录新的 Vnode 插入位置)
2. 比较新、旧 vnode 的 children 情况(3种情况)
1、新 vnode === 旧 vnode(全等) | 2、新 Vnode 无 text | 3、新 Vnode 有 text | |
---|---|---|---|
结果 | 不需更新,return | 1.新、老 Vnode 都有 children,即调用(updateChildren );2.老 Vnode 无 children,即添加 children ; 3.新的无 children ,即删除 children |
删除 children ,更新新的 text |