前言
vdom 是 vue 和 React 的核心,先讲哪个都绕不开它
vdom 比较独立,使用也比较简单
如果面试问到 vue 和 React 和实现,免不了问 vdom,带着三个问题去深入了解
问题
- vdom 是什么?为何会存在 vdom?
- vdom 的如何应用,核心 API 是什么
- 介绍一下 diff 算法
1、vdom 是什么?为何会存在 vdom?
- virtual dom,虚拟 DOM
- 用JS模拟DOM的结构
- DOM 变化的对比,放在 JS 层来做(图灵完备语言)
- 提高重绘性能
DOM结构
<ul id='list'>
<li class='item'>item1</li>
<li class='item'>item2</li>
</ul>
JS模拟
{
tag:'ul',
attrs:{
id:'list'
},
children:[
{
tag:'li',
attrs:{
className: 'item'
},
children:['item1']
},{
tag:'li',
attrs:{
className: 'item'
},
children:['item2']
}
]
}
设计一个需求场景




遇到的问题
- DOM的操作是“昂贵”的,js运行效率高
- 尽量减少DOM的操作,而不是推倒重来
- 项目越复杂,影响越严重
- vdom即可解决这些问题
问题解答
- virtual dom , 虚拟 DOM
- 用 JS 模拟 DOM 结构
- DOM 操作非常“昂贵”
- 将 DOM 对比操作放在 JS 层,提高效率
2、vdom 的如何应用,核心 API 是什么
- 介绍 snabbdom (vdom的一个库)
- 重做之前的 demo
- 核心 API
snabbdom 一个注重简单性、模块化、强大功能和性能的虚拟DOM库。地址




重做demo
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
var snabbdom = window.snabbdom
// 定义 patch
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义 h
var h = snabbdom.h
var container = document.getElementById('container')
// 生成 vnode
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2')
])
patch(container, vnode)
document.getElementById('btn-change').addEventListener('click', function () {
// 生成 newVnode
var newVnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item B'),
h('li.item', {}, 'Item 3')
])
patch(vnode, newVnode) // 找出差异,渲染差异
})
// jquery例子改造
var snabbdom = window.snabbdom
// 定义关键函数 patch
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义关键函数 h
var h = snabbdom.h
// 原始数据
var data = [{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 把表头也放在 data 中
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
var container = document.getElementById('container')
// 渲染函数
var vnode
function render(data) {
var newVnode = h('table', {}, data.map(function (item) {
var tds = []
var i
for (i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''))
}
}
return h('tr', {}, tds)
}))
if (vnode) {
// re-render
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}
// 存储当前的 vnode 结果
vnode = newVnode
}
// 初次渲染
render(data)
var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function () {
data[1].age = 30
data[2].address = '深圳'
// re-render
render(data)
})

- 使用 data 生成 vnode
- 第一次渲染,将 vnode 渲染到 #container 中
- 并将 vnode 缓存下来
- 修改 data 之后,用新 data 生成 newVnode
- 将 vnode 和 newVnode 对比
核心API:h 函数、patch 函数
- h(‘<标签名>’, {…属性…}, […子元素…])
- h(‘<标签名>’, {…属性…}, ‘….’)
- patch(container, vnode)
- patch(vnode, newVnode)
介绍一下 diff 算法
什么是diff算法


- linux diff 命令
- git diff (对比两个文件之间差异)

去繁就简
- diff 算法非常复杂,实现难度很大,源码量很大
- 去繁就简,讲明白核心流程,不关心细节
- 面试官也大部分都不清楚细节,但是很关心核心流程
- 去繁就简之后,依然具有很大挑战性,并不简单
vdom 为何用 diff 算法
- DOM 操作是“昂贵”的,因此尽量减少 DOM 操作
- 找出本次 DOM 必须更新的节点来更新,其他的不更新
- 这个“找出”的过程,就需要 diff 算法


diff 算法的实现流程
- patch(container, vnode)
- patch(vnode, newVnode)
核心逻辑:createElement 和 updateChildren
// diff 算法实现
// code demo
function createElement(vnode) {
var tag = vnode.tag // 'ul'
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 创建真实的 DOM 元素
var elem = document.createElement(tag)
// 属性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 给 elem 添加属性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加子元素
elem.appendChild(createElement(childVnode)) // 递归
})
// 返回真实的 DOM 元素
return elem
}
// vnode newVnode compare
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
if (childVnode.tag === newChildVnode.tag) {
// 深层次对比,递归
updateChildren(childVnode, newChildVnode)
} else {
// 替换
replaceNode(childVnode, newChildVnode)
}
})
}
function replaceNode(vnode, newVnode) {
var elem = vnode.elem // 真实的 DOM 节点
var newElem = createElement(newVnode)
// 替换
}

- 节点新增和删除
- 节点重新排序
- 节点属性、样式、事件变化
- 如何极致压榨性能
- ......
answer:
- 知道什么是 diff 算法,是 linux 的基础命令
- vdom 中应用 diff 算法是为了找出需要更新的节点
- vdom 实现过程,createElement 和 updateChildren
- 与核心函数 patch 的关系