vue 快速入门 系列 —— 虚拟 DOM
其他章节请看:
虚拟 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 快速入门 系列 —— 虚拟 DOM的更多相关文章
- vue 快速入门 系列 —— vue loader 上
其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...
- vue 快速入门 系列 —— 模板
其他章节请看: vue 快速入门 系列 模板 前面提到 vue 中的虚拟 dom 主要做两件事: 提供与真实节点对应的 vNode 新旧 vNode 对比,寻找差异,然后更新视图 ①.vNode 从何 ...
- vue 快速入门 系列 —— Vue(自身) 项目结构
其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ...
- vue 快速入门 系列 —— Vue 实例的初始化过程
其他章节请看: vue 快速入门 系列 Vue 实例的初始化过程 书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法.倘若你看过 jQuery 的源码, ...
- vue 快速入门 系列 —— 初步认识 vue
其他章节请看: vue 快速入门 系列 初步认识 vue vue 是什么 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架. 所谓渐进式,就是你可以一步一步.有阶段 ...
- vue 快速入门 系列 —— vue 的基础应用(上)
其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...
- vue 快速入门 系列 —— vue 的基础应用(下)
其他章节请看: vue 快速入门 系列 vue 的基础应用(下) 上篇聚焦于基础知识的介绍:本篇聚焦于基础知识的应用. 递归组件 组件是可以在它们自己的模板中调用自身的.不过它们只能通过 name 选 ...
- vue 快速入门 系列 —— vue-cli 下
其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...
- vue 快速入门 系列 —— vue-router
其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...
随机推荐
- setTimeout 实现原理, 机制
setTimeout 实现原理, 机制 JS 执行机制说起 浏览器(或者说 JS 引擎)执行 JS 的机制是基于事件循环. 由于 JS 是单线程,所以同一时间只能执行一个任务,其他任务就得排队,后续任 ...
- HTTPS原理解析
HTTPS 一些概念 http 概述 HTTP是一个客户端(用户)和服务端(网站)之间请求和应答的标准,通常使用TCP协议.其本身位于TCP/IP协议族的应用层. 特点 - 客户端&服务器 - ...
- mysql主从备份说明(win系统)
1. 环境描述: 主机:192.168.2.201 从机:192.168.2.111 Mysql版本:5.7 2. 主机my.ini配置: log-bin=C:\mysqlback expire-lo ...
- 带你认识webpack
一.webpack是什么 webpack是一种前端资源构建工具,一个静态模块打包器(module bundler).在webpack看来,前端的所有资源文件(js/json/css/img/less/ ...
- Linux graphics stack
2D图形架构 早期Linux图形系统的显示全部依赖X Server,X Client调用Xlib提供的借口向 X Server发送渲染命令,X Server根据 X Client的命令请求向硬件设备绘 ...
- 测试平台系列(4) 使用Flask蓝图(blueprint)
使用Flask蓝图(blueprint) 回顾 先来看一下上一篇的作业吧,使用「logbook」的时候,遇到了时区不对的情况.那么我们怎么去解决这个问题呢? 实际上logbook默认采用的是世界标准时 ...
- RocketMQ安装配置过程
官网 官方网站:http://rocketmq.apache.org 下载源码包:https://www.apache.org/dyn/closer.cgi?path=rocketmq/4.8.0/r ...
- Python接口测试-保持登录状态
#coding:utf-8import requestsimport json#登录url ="https://passport.cnblogs.com/user/signin"h ...
- C# 应用 - 使用 WebClient 发起 Http 请求
1. 需要的库类 \Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.dll System.Net.WebCli ...
- Java 运行时数据区和内存模型
运行时数据区是指对 JVM 运行过程中涉及到的内存根据功能.目的进行的划分,而内存模型可以理解为对内存进行存取操作的过程定义.总是有人望文生义的将前者描述为 "Java 内存模型" ...