命令式编程:命令“机器”如何去做事情(how)
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
命令式操作DOM声明式操作DOM
过去jQuery命令式操作DOM,现在Angular、React、Vue三大框架都是声明式操作DOM
虚拟DOM,就是用一个JS对象来描述一个DOM节点
<div class="a" id="b">我是内容</div>
{
tag:'div', // 元素标签
attrs:{
// 属性
class:'a',
id:'b'
},
text:'我是内容', // 文本内容
children:[] // 子元素
}
Vue是数据驱动视图的,在更新视图的时候难免要操作DOM,操作真实DOM非常耗费性能的。
因为浏览器的标准就把 DOM 设计的非常复杂,如图:
一个空的div就有下列属性
DOM操作的执行速度远不及JavaScript的运算速度,用JS的计算性能来换取操作DOM所消耗的性能,尽可能少的操作DOM。
我们可以用JS模拟出一个DOM节点,称之为虚拟DOM节点。当数据发生变化时,我们对比变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图。
通过不同属性的搭配,可以描述不同类型的节点。如注释节点、文本节点、元素节点、组件节点、函数式组件节点、克隆节点
// 源码位置:src/core/vdom/vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag /*当前节点的标签名*/
this.data = data /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.children = children /*当前节点的子节点,是一个数组*/
this.text = text /*当前节点的文本*/
this.elm = elm /*当前虚拟节点对应的真实dom节点*/
this.ns = undefined /*当前节点的名字空间*/
this.context = context /*当前组件节点对应的Vue实例*/
this.fnContext = undefined /*函数式组件对应的Vue实例*/
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key /*节点的key属性,被当作节点的标志,用以优化*/
this.componentOptions = componentOptions /*组件的option选项,如组件的props等*/
this.componentInstance = undefined /*当前节点对应的组件的vue实例*/
this.parent = undefined /*当前节点的父节点*/
this.raw = false /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.isStatic = false /*静态节点标志*/
this.isRootInsert = true /*是否作为跟节点插入*/
this.isComment = false /*是否为注释节点*/
this.isCloned = false /*是否为克隆节点*/
this.isOnce = false /*是否有v-once指令*/
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
get child (): Component | void {
return this.componentInstance
DOM-Diff过程即patch,对应有新增、删除、更改
代码中的nodeOps是Vue为了跨平台兼容性,对所有节点操作进行了封装,例如nodeOps.createTextNode()在浏览器端等同于document.createTextNode()
// 源码位置: /src/core/vdom/patch.js
function createElm (vnode, parentElm, refElm) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode) // 创建元素节点
createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text) // 创建注释节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else {
vnode.elm = nodeOps.createTextNode(vnode.text) // 创建文本节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
}
}
function removeNode (el) {
const parent = nodeOps.parentNode(el) // 获取父节点
if (isDef(parent)) {
nodeOps.removeChild(parent, el) // 调用父节点的removeChild方法
}
}
// 更新节点
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// vnode与oldVnode是否完全一样?若是,退出程序
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// vnode与oldVnode是否都是静态节点?若是,退出程序
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
return
}
const oldCh = oldVnode.children
const ch = vnode.children
// vnode有text属性?若没有:
if (isUndef(vnode.text)) {
// vnode的子节点与oldVnode的子节点是否都存在?
if (isDef(oldCh) && isDef(ch)) {
// 若都存在,判断子节点是否相同,不同则更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
// 若只有vnode的子节点存在
else if (isDef(ch)) {
/**
* 判断oldVnode是否有文本?
* 若没有,则把vnode的子节点添加到真实DOM中
* 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
// 若只有oldnode的子节点存在
else if (isDef(oldCh)) {
// 清空DOM中的子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 若vnode和oldnode都没有子节点,但是oldnode中有文本
else if (isDef(oldVnode.text)) {
// 清空oldnode文本
nodeOps.setTextContent(elm, '')
}
// 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
}
// 若有,vnode的text属性与oldVnode的text属性是否相同?
else if (oldVnode.text !== vnode.text) {
// 若不相同:则用vnode的text替换真实DOM的文本
nodeOps.setTextContent(elm, vnode.text)
}
}
关于递归对比子节点的源码分析
两层循环:
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i];
for (let j = 0; j < oldChildren.length; j++) {
const oldChild = oldChildren[j];
if (newChild === oldChild) {
// ...
}
}
}
创建子节点:找不到与之相同的子节点,插入oldChildren未处理的子节点前
删除子节点:newChildren循环完毕后,发现oldChildren还有未处理的子节点
移动子节点:newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同
更新节点:相同(包括位置),那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。
注意:当节点数量很多时Vue提出了优化算法,避免双重循环数据量大时间复杂度升高带来的性能问题,而选择了从子节点数组中的4个特殊位置互相比对,分别是:新前与旧前,新后与旧后,新后与旧前,新前与旧后。