1.什么是虚拟dom
- 模板转换成视图的过程
1.Vue.js将tetmplate模板编译为渲染函数Render,执行渲染函数生成一棵虚拟dom树
2.在对Model操作时,触发对应Dep中的Watcher对象,Watcher对象会调用对应的update更新视图
渲染函数
:用来生成虚拟树
vnode
虚拟节点:代表一个真实的dom节点,通过createElement方法将vnode渲染成dom节点
patch
虚拟dom最核心部分,可以将vnode渲染成真实的dom,整个过程就是对比新旧虚拟节点的不同,根据对比结果找到需更新的节点
- Virtual DOM 是什么
Virtual DOM
实际是一棵以js对象(vnode节点)为基础的树,用对象属性来描述节点,至少包含标签名Tag
、属性attrs
、子元素对象children
三个属性
2.为什么要引入虚拟dom
虚拟节点的最终目标就是将虚拟节点渲染至视图上。如:一个ul标签有多个li标签,其中一个li标签发生变化,如果使用新的ul替代旧的ul,因为不必要的dom操作造成性能上的浪费
为了不必要的dom操作,虚拟dom在虚拟节点映射至视图的过程中,将虚拟节点与上一次渲染视图所需要的旧虚拟节点对比,找到需要更新的节点来进行dom操作
虚拟dom在vue.js中主要完成了两件事:
1)提供与真实dom节点所对应的虚拟节点vnode
2)将虚拟节点vnode和旧虚拟节点对比,然后更新视图
优势:
若一次操作中有N次更新dom行为,虚拟dom不会立即执行操作dom,而是将这N次更新的diff内容保存至本地的一个js对象中,将这个js对象一次性attch到dom树上,再进行后续操作。
1.跨平台的优势
2.将dom对比操作放在js层,提高效率
3.提升渲染性能
vue的diff算法基于snabbdom,仅在同级的vnode间做diff,递归进行,最终实现整个dom树的更新
diff算法的实现步骤:
- 使用js对象结构表示dom树结构,然后使用此树构建一棵真正的dom树
- 当状态变更的时候,重新构造dom树,将新的树和旧的树对比,记录差异
- 把所记录的差异应用至所构建的真正dom树上,视图更新
diff两个核心函数实现
patch(container,vnode)
:初次渲染,将vdom渲染成真正的dom插入至容器中
function createElement(vnode){
var tag = vnode.tag
var attrs = vnode.attrs || {}
var children =vnode.children || {}
if(!tag){
return null
}
// 创建真正的dom元素
var elem = document.createElement(tag)
// 遍历属性
for (var 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
}
patch(vnode,newVnode)
:再次渲染,将新的vnode和旧的vnode对比,然后将差异应用至真正的dom树上
// 这里我们只考虑vnode与newVnode如何对比的情况
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
// 遍历现有的children
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
// 两者tag一样
if (childVnode.tag === newChildVnode.tag) {
// 深层次对比,递归
updateChildren(childVnode, newChildVnode)
} else {
// 两者tag不一样
replaceNode(childVnode, newChildVnode)
}
}
)}
3.Vue.js中的虚拟dom
- vue中的虚拟dom如何创建?
vue-loader
可以在template中编写模板字符串,将内容提取至vue-template-compiler,Vue编译转换为渲染函数中的内容,执行这个函数得到vnode树,
<template>
<img src='../image.png'>
</template>
// --------编译为
createElement('img', {
attrs: {
src: require('../image.png')
}
})
- vue中的Vnode类
vnode类型有 1)注释节点 2)文本节点 3)元素节点 4)组件节点 5)函数组件 6)克隆节点
this.isComment = false; // true 代表注释节点
this.isCloned = false; // true 代表克隆节点
this.componentOptions = componentOptions; // 组件节点的选项参数,propsData、tag、children
this.componentInstance = undefined;// 组件实例
this.context = context; // 当前组件的Vue实例
this.fnContext = undefined;// 函数式组件实例
this.fnOptions = undefined;// 函数式组件选项参数
- patch
patch对比新旧vnode的差异,修改dom节点
修改dom节点需要完成三件事:
1)创建新节点
oldVnode不存在而vnode存在
当oldVnode和vnode完全不是同一个节点
2)删除已废弃的节点 vnode中不存在的节点
3)修改需更新的节点 vnode和oldVnode是同一个节点,进行详细对比
只有三种类型的节点会被创建并插入到DOM中:元素、文本、注释
-
如何判断节点类型:
1)vnode是否为元素节点,是否具有tag属性
2)不具有tag属性则是文本或注释节点,根据vnode的isComment=true则为注释节点,false则是文本节点 -
创建新增节点
- createElement ——元素
- createTextNode ——文本
- createComment ——注释
function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var ch = vnodes[startIdx];
if (isDef(ch)) {
removeNode(ch.elm);
}
}
}
var nodeOps = {
removeChild: removeChild,
appendChild: appendChild,
parentNode: parentNode
}
function removeNode (el) {
var parent = nodeOps.parentNode(el);
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
nodeOps.removeChild(parent, el);
}
}
1 . vnode有
文本属性
,则无论oldVnode子节点是什么,直接setTextContent
方法更新文本
2 .vnode是无children的元素
,则oldVnode中无论有子节点还是文本,直接都删除即可。
- vnode是
有children的元素
(1)oldVnode没有children
,那么oldVnode要么是空标签,要么有文本子节点,则直接清空,并将vnode中的children直接插入到视图中。
(2)oldVnode有children
,则需要详细的对比,可能会新增、删除、或者移动。
更新策略
对比两个子节点列表,首先循环newChildren,每循环一个新的子节点,就去oldChildren中找到相同的旧子节点
- 找不到相同的旧子节点,则直接新增子节点,并插入到oldChildren中所
未处理节点对应的DOM前面
- 找到且位置相同,直接更新dom
- 找到但位置不同,移动子节点
Node.insertBefore()
将节点移动到所有未处理节点对应DOM的最前面。