其他章节请看:

vue 快速入门 系列

虚拟 DOM

什么是虚拟 dom

dom 是文档对象模型,以节点树的形式来表现文档。

虚拟 dom 不是真正意义上的 dom。而是一个 javascript 对象。

正常的 dom 节点在 html 中是这样表示:

<div class='testId'>
<p>你好</p>
<p>欢迎光临</p>
</div>

而在虚拟 dom 中大概是这样:

{
tag: 'div',
attributes:{
class: ['testId']
},
children:[
// p 元素
// p 元素
]
}

我们可以将虚拟 dom 拆分成两部分进行理解:虚拟 + dom。

  • 虚拟: 表示虚拟 dom 不是真正意义上的 dom,而是一个 javascript 对象;
  • dom: 表示虚拟 dom 能以类似节点树的形式表示文档。

虚拟 dom 的作用

前文(初步认识 vue)提到,现在主流的框架都是声明式操作 dom 的框架。我们只需要描述状态与 dom 之间的映射关系即可,状态到视图(真实的 dom)的转换,框架会帮我们做。

最粗暴的做法是将状态渲染成视图,每次更新状态,都重新更新整个视图。

这种做法的性能可想而知。比较好的想法是:状态改变,只更新与状态相关的 dom 节点。虚拟 dom 只是实现这个想法的其中一种方法而已。

具体做法:

  • 状态 -> 真实 dom(最初)
  • 状态 -> 虚拟 dom -> 真实 dom(使用虚拟 dom)

状态改变,重新生成一份虚拟 dom,将上一份和这一份虚拟 dom 进行对比,找出需要更新的部分,更新真实 dom。

vue 中的虚拟 dom

真实的 dom 是由 节点(Node)组成,虚拟 dom 则是由虚拟节点(vNode)组成。

虚拟 dom 在 vue 中主要做两件事:

  • 提供与真实节点(Node)对应的虚拟节点(vNode)
  • 将新的虚拟节点与旧的虚拟节点进行对比,找出需要差异,然后更新视图

“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼 —— vue 官网

vNode

什么是 vNode

上文提到,vNode(虚拟节点)对应的是真实节点(Node)。

vNode 可以理解成节点描述对象。描述了如何创建真实的 dom 节点。

vue.js 中有一个 vNode 类。可以使用它创建不同类型的 vNode 实例,不同类型的 vNode 对应着不同类型的 dom 元素。代码如下:

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
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
} get child (): Component | void {
return this.componentInstance
}
}

从代码不难看出 vNode 类创建的实例,本质上就是一个普通的 javascript 对象。

vNode 的类型

前面我们已经介绍通过 vNode 类可以创建不同类型的 vNode。而不同类型的 vNode 是由有效属性区分。例如 isComment = true 表示注释节点;isCloned = true 表示克隆节点等等。

vNode 类型有:注释节点、文本节点、克隆节点、元素节点、组件节点。

以下是注释节点、文本节点和克隆节点的代码:

/*
注释节点
有效属性:{isComment: true, text: '注释节点'}
*/
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
// 注释
node.isComment = true
return node
}
/*
文本节点
有效属性:{text: '文本节点'}
*/
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
} // optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// 用于静态节点和插槽节点
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
// 克隆节点
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
// 标记是克隆节点
cloned.isCloned = true
return cloned
}

克隆节点其实就是将现有节点的所有属性赋值到新节点中,最后用 cloned.isCloned = true 标记自身是克隆节点。

元素节点通常有以下 4 个属性:

  • tag:节点名称。例如 div、p
  • data:节点上的数据。例如 class、style
  • children:子节点
  • context:在组件内呈现

组件节点与元素节点类似,包含两个独有的属性:

  • componentOptions:组件节点的选项参数,例如propsData、listeners、children、tag
  • componentInstance:组件的实例

patch

前面已经介绍了虚拟 dom 在 vue 中做的第一件事:提供与真实节点(Node)对应的虚拟节点(vNode);接下来介绍第二件事:将新的虚拟节点与旧的虚拟节点进行对比,找出需要差异,然后更新视图。

第二件事在 vue 中的实现叫做 patch,即打补丁、修补的意思。通过对比新旧 vNode,找出差异,然后在现有 dom 的基础上进行修补,从而实现视图更新。

对比 vNode 找差异是手段,更新视图才是目的。

而更新视图无非就是新增节点、删除节点和更新节点。接下来我们逐一分析什么时候新增节点、在哪里新增;什么时候删除节点,删除哪个;什么时候更新节点,更新哪个;

:当 vNode 与 oldVNode 不相同的时候,以 vNode 为准。

新增节点

一种情况是:vNode 存在而 oldVNode 不存在时,需要新增节点。最典型的是初次渲染,因为 odlVNode 是不存在的。

另一种情况是 vNode 与 oldVNode 完全不是同一个节点。这时就需要使用 vNode 生成真实的 dom 节点并插入到 oldVNode 指向的真实 dom 节点旁边。oldVNode 则是一个被废弃的节点。例如下面这种情况:

<div>
<p v-if="type === 'A'">
我是节点A
</p>
<span v-else-if="type === 'B'">
我是与A完全不同的节点B
</span>
</div>

当 type 由 A 变为 B,节点就会从 p 变成 span,由于 vNode 与 oldVNode 完全不是同一个节点,所以需要新增节点。

删除节点

当节点只在 oldVNode 中存在时,直接将其删除即可。

更新节点

前面介绍了新增节点和删除节点的场景,发现它们有一个共同点:vNode 与 oldVNode 完全不相同。

但更常见的场景是 vNode 与 oldVNode 是同一个节点。然后我们需要对它们(vNode 与 oldVNode)进行一个更细致的对比,再对 oldVNode 对应的真实节点进行更新。

对于文本节点,逻辑自然简单。首先对比新旧 vNode,发现是同一个节点,然后将 oldVNode 对应的 dom 节点的文本改成 vNode 中的文本即可。但对于复杂的 vNode,比如界面中的一颗树组件,这个过程就会变得复杂。

新增节点 - 源码分析

思考一下:前面说到 vNode 的类型有:注释节点、文本节点、克隆节点、元素节点、组件节点。请问这几种类型都会被创建并插入到 dom 中吗?

答:只有注释节点、文本节点、元素节点。因为 html 只认识这几种。

由于只有上面三种节点类型,根据类型做响应的创建,然后插入对应的位置即可。

以元素节点为例,如果 vNode 有 tag 属性,则说明是元素节点。则调用 createElement 方法创建对应的节点,接下来就通过 appendChild 方法插入到指定父节点中。如果父元素已经在视图中,那么把元素插入到它下面将会自动渲染出来;如果 vNode 的 isComment 属性是 true,则表示注释节点;都不是则是文本节点;

通常元素里面会有子节点,所以这里涉及一个递归的过程,也就是将 vNode 中的 children 依次遍历,创建节点,然后插入到父节点(父节点也就是刚刚创建出的 dom 节点)中,一层一层的递归进行。

请看源码:

// 创建元素
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
} vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
} var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
// 有 tag 属性,表示是元素节点
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
// 创建元素。nodeOps 涉及到跨平台
: nodeOps.createElement(tag, vnode);
setScope(vnode); /* istanbul ignore if */
{
// 递归创建子节点,并将子节点插入到父节点上
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// 将 vnode 对应的元素插入到父元素中
insert(parentElm, vnode.elm, refElm);
} // isComment 属性表示注释节点
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
// 插入父节点
insert(parentElm, vnode.elm, refElm);
// 否则就是子节点
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
// 插入父节点
insert(parentElm, vnode.elm, refElm);
}
} // 递归创建子节点,并将子节点插入到父节点上。vnode 表示父节点
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children);
}
// 依次创建子节点,并将子节点插入到父节点中
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}

删除节点 - 源码分析

删除节点非常简单。直接看源码:

// 删除一组指定节点
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else { // Text node
// 删除个节点
removeNode(ch.elm);
}
}
}
} // 删除单个节点
function removeNode (el) {
var parent = nodeOps.parentNode(el);
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
// nodeOps里封装了跨平台的方法
nodeOps.removeChild(parent, el);
}
}

更新节点 - 源码分析

有些复杂,而且涉及子节点更新,本文就不展开。

其他章节请看:

vue 快速入门 系列

vue 快速入门 系列 —— 虚拟 DOM的更多相关文章

  1. vue 快速入门 系列 —— vue loader 上

    其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...

  2. vue 快速入门 系列 —— 模板

    其他章节请看: vue 快速入门 系列 模板 前面提到 vue 中的虚拟 dom 主要做两件事: 提供与真实节点对应的 vNode 新旧 vNode 对比,寻找差异,然后更新视图 ①.vNode 从何 ...

  3. vue 快速入门 系列 —— Vue(自身) 项目结构

    其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ...

  4. vue 快速入门 系列 —— Vue 实例的初始化过程

    其他章节请看: vue 快速入门 系列 Vue 实例的初始化过程 书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法.倘若你看过 jQuery 的源码, ...

  5. vue 快速入门 系列 —— 初步认识 vue

    其他章节请看: vue 快速入门 系列 初步认识 vue vue 是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架. 所谓渐进式,就是你可以一步一步.有阶段 ...

  6. vue 快速入门 系列 —— vue 的基础应用(上)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...

  7. vue 快速入门 系列 —— vue 的基础应用(下)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(下) 上篇聚焦于基础知识的介绍:本篇聚焦于基础知识的应用. 递归组件 组件是可以在它们自己的模板中调用自身的.不过它们只能通过 name 选 ...

  8. vue 快速入门 系列 —— vue-cli 下

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  9. vue 快速入门 系列 —— vue-router

    其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...

随机推荐

  1. 1+X 证书制度

    1+X 证书制度 教育部职业技术教育 http://www.cvae.com.cn/zgzcw/tzgg/202001/c0ddd6c87e6c42839f8cc3e09a2dce89.shtml 2 ...

  2. How to use JavaScript to implement precise setTimeout and setInterval

    How to use JavaScript to implement precise setTimeout and setInterval 如何使用 JavaScript 实现精确的 setTimeo ...

  3. js function call hacker

    js function call hacker you don't know javascript function https://developer.mozilla.org/en-US/docs/ ...

  4. git merge & git rebase

    git merge & git rebase bug error: You have not concluded your merge (MERGE_HEAD exists). hint: P ...

  5. puppeteer & screenshot

    puppeteer & screenshot http://localhost:9812/screenshot?url=https://cdn.xgqfrms.xyz/

  6. 10000星光值兑换一个的VAST将如何搅动NGK算力市场?

    加密数字货币是私人而非政府所发行的数字资产,具有自己的"货币"账户单位,在可以预见的未来三年之内,加密数字货币将覆盖至少全世界五分之一的人口.为此,NGK方面也做出了自己的努力,在 ...

  7. PBN旁切转弯的精确化计算

    PBN转弯保护区中使用频率最高的当属旁切转弯,风螺旋的精确算法会对旁切转弯的绘制带来哪些变化,通过今天这个例子我们来了解一下. 图III-3-2-3 旁切转弯保护区   一.基础参数: ICAO816 ...

  8. C++算法代码——关于马的问题

    题目来自:http://218.5.5.242:9018/JudgeOnline/problem.php?id=1285 题目描述 小R参加一个派对,这个派队的参加者需要带上四匹颜色不同的马.小R目前 ...

  9. js 表格插入指定行

    js在table指定tr行上或下面添加tr行 function onAddTR(trIndex)         {             var tb = document.getElementB ...

  10. linux之安装nginx

    nginx官网:http://nginx.org/en/download.html 1.安装nginx所需环境 a)  PCRE pcre-devel 安装 # yum install -y pcre ...