最近在看一些底层方面的知识。因此想作个系列尝试去聊聊这些比较复杂又很重要的知识点。学习就比如是座大山,只有本身去爬山,才能看到不同的风景,体会更加深入。今天咱们就来聊聊Vue中比较重要的vue虚拟(Virtual )DOM和diff算法。vue
Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)做为基础的树,用对象属性来描述节点,至关于在js和真实dom中间加来一个缓存,利用dom diff算法避免没有必要的dom操做,从而提升性能。固然算法有时并非最优解,由于它须要兼容不少实际中可能发生的状况,好比后续会讲到两个节点的dom树移动。node
上几篇文章中讲vue的数据状态管理结合Virtual DOM更容易理解,在vue中通常都是经过修改元素的state,订阅者根据state的变化进行编译渲染,底层的实现能够简单理解为三个步骤:react
举例子:有一个 ul>li 列表,在template中的写法是:git
<ul id='list'>
<li class='item1'>Item 1</li>
<li class='item2'>Item 2</li>
<li class='item3' style='font-size: 20px'>Item 3</li>
</ul>
复制代码
vue首先会将template进行编译,这其中包括parse、optimize、generate三个过程。github
parse会使用正则等方式解析template模版中的指令、class、style等数据,造成AST,因而咱们的ul> li 可能被解析成下面这样算法
// js模拟DOM结构
var element = {
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
class: 'item',
id: 'list'
},
children: [ // 该节点的子节点
{tagName: 'li', props: {class: 'item1'}, children: "Item 1"},
{tagName: 'li', props: {class: 'item2'}, children: "Item 2"},
{tagName: 'li', props: {class: 'item3', style: 'font-size: 20px'}, children: "Item 3"},
]
}
复制代码
optimize过程其实只是为了优化后文diff算法的,若是不加这个过程,那么每层的节点都须要作比对,即便没变的部分也得弄一遍,这也违背了Virtual DOM 最初本质,形成没必要要的资源计算和浪费。所以在编译的过程当中vue会主动标记static静态节点,我的理解为就是页面一些不变的或者不受state影响的节点。好比咱们的ul节点,不论li如何变化ul始终是不会变的,所以在这个编译的过程当中能够个ul打上一个标签。当后续update更新视图界面时,patch过程看到这个标签会直接跳过这些静态节点。数组
最后经过generate 将 AST 转化成 render function 字符串,获得结果是 render 的字符串以及 staticRenderFns 字符串。你们听起来可能很困惑,首先前两步你们应该都差很少知道了,当拿到一个AST时,vue内部有一个叫element ASTs的代码生成器,犹如名字同样generate函数拿到解析好的AST对象,递归AST树,为不一样的AST节点建立不一样的内部调用的方法,而后组合可执行的JavaScript字符串,等待后面的调用。最后可能会变成这个样子:浏览器
function _render() {
with (this) {
return __h__(
'ul',
{staticClass: "list"},
[
" ",
__h__('li', {class: item}, [String((msg))]),
" ",
__h__('li', {class: item}, [String((msg))]),
"",
__h__('li', {class: item}, [String((msg))]),
""
]
)
};
}
复制代码
整个Virtual DOM生成的过程代码中可简化为以下,有兴趣的同窗能够去看具体对应的Vue源码,源码位置在src/compiler/index.js缓存
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.parse,模板字符串 转换成 抽象语法树(AST)
const ast = parse(template.trim(), options)
// 2.optimize,对 AST 进行静态节点标记
if (options.optimize !== false) {
optimize(ast, options)
}
// 3.generate,抽象语法树(AST) 生成 render函数代码字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
复制代码
在最初的diff算法实际上是"不可用的",由于时间复杂度是O(n^3)。假设一个dom树有1000个节点,第一遍须要遍历tree1,第二遍遍历tree2,最后一遍就是排序组合成新树。所以这1000个节点须要计算1000^3 = 1亿次,这是很是庞大的计算,这种算法基本也不会用。bash
后面设计者们想出了一些方法,将时间复杂度由O(n^3)变成了O(n),那么这些设计者是若是实现的?这也就是diff算法的优点所在,也是日常咱们所理解到一些知识:
这就是一个简单的diff。经过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,因此时间复杂度只有 O(n)。
以前在Virtual DOM中讲到当状态改变了,vue便会从新构造一棵树的对象树,而后用这个新构建出来的树和旧树进行对比。这个过程就是patch。比对得出「差别」,最终将这些「差别」更新到视图上。patch的过程也是vue及react的核心算法,理解起来比较困难。先看一些简单的图形了解diff是如何比较新旧VNode的差别的。
场景1:更新删除移动
场景2:删除新建
在简单的理解了diff算法实际操做的过程。为了让你们更好的掌握,由于这块仍是比较复杂的。接下来将用伪代码的形式分析diff算法是如何进行深度优先遍历,记录差别, Vue的VDOM的diff算法借鉴的是snabbdom,不妨先从snabbdom Example入手
在vue中首先会对新旧两棵树进行深度优先的遍历,这样每一个节点都会有一个惟一的标记。在遍历的同时,每遍历一个节点就会把该节点和新的树进行对比,有差别的话就会记录到一个对象里。
/* 建立diff函数,接受新旧量两棵参数 */
function diff (oldTree, newTree) {
var index = 0 //当前节点的标志
var patches = {} //用来记录每一个节点差别的对象
dfsWalk(oldTree, newTree, index, patches) // 对两棵树进行深度优先遍历
return patches //返回不一样的记录
}
function dfsWalk (oldNode, newNode, index, patches) {
var currentPatch = [] // 定义一个数组将对比oldNode和newNode的不一样,记录下来
if (newNode === null) {
// 当执行从新排序时,真正的DOM节点将被删除,所以不须要在这里进行调整
} else if (_.isString(oldNode) && _.isString(newNode)) {
// 判断oldNode、newNode是不是字符串对象或者字符串值
if (newNode !== oldNode) {
//节点不一样直接放入到数组中
currentPatch.push({ type: patch.TEXT, content: newNode })
}
} else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// 节点是相同的,diff区分旧节点的props和子节点
// diff处理props
var propsPatches = diffProps(oldNode, newNode)
if (propsPatches) {
currentPatch.push({ type: patch.PROPS, props: propsPatches })
}
// diff处理子节点,若是有‘ignore’这个标志的。diff就忽视这个子节点
if (!isIgnoreChildren(newNode)) {
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
)
}
} else {
// 节点不相同,用新节点直接替换旧节点
currentPatch.push({ type: patch.REPLACE, node: newNode })
}
}
function isIgnoreChildren (node) {
return (node.props && node.props.hasOwnProperty('ignore'))
}
/* 处理子节点diffChildren函数 */
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
var diffs = listDiff(oldChildren, newChildren, 'key')
newChildren = diffs.children
if (diffs.moves.length) {
var reorderPatch = { type: patch.REORDER, moves: diffs.moves }
currentPatch.push(reorderPatch)
}
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
leftNode = child
})
}
/* 处理子节点的props diffProps函数 */
function diffProps (oldNode, newNode) {
var count = 0
var oldProps = oldNode.props
var newProps = newNode.props
var key, value
var propsPatches = {}
// Find out different properties
for (key in oldProps) {
value = oldProps[key]
if (newProps[key] !== value) {
count++
propsPatches[key] = newProps[key]
}
}
// Find out new property
for (key in newProps) {
value = newProps[key]
if (!oldProps.hasOwnProperty(key)) {
count++
propsPatches[key] = newProps[key]
}
}
// If properties all are identical
if (count === 0) {
return null
}
return propsPatches
}
// 暴露diff函数
module.exports = diff
复制代码
感兴趣的话你也可查看简化版的diff。 完整简化版的diff算法