其余章节请看:javascript
vue 快速入门 系列html
dom 是文档对象模型,以节点树的形式来表现文档。vue
虚拟 dom 不是真正意义上的 dom。而是一个 javascript 对象。java
正常的 dom 节点在 html 中是这样表示:node
<div class='testId'> <p>你好</p> <p>欢迎光临</p> </div>
而在虚拟 dom 中大概是这样:app
{ tag: 'div', attributes:{ class: ['testId'] }, children:[ // p 元素 // p 元素 ] }
咱们能够将虚拟 dom 拆分红两部分进行理解:虚拟 + dom。框架
前文(初步认识 vue)提到,如今主流的框架都是声明式操做 dom 的框架。咱们只须要描述状态与 dom 之间的映射关系便可,状态到视图(真实的 dom)的转换,框架会帮咱们作。dom
最粗暴的作法是将状态渲染成视图,每次更新状态,都从新更新整个视图。async
这种作法的性能可想而知。比较好的想法是:状态改变,只更新与状态相关的 dom 节点。虚拟 dom 只是实现这个想法的其中一种方法而已。源码分析
具体作法:
- 状态 -> 真实 dom(最初)
- 状态 -> 虚拟 dom -> 真实 dom(使用虚拟 dom)
状态改变,从新生成一份虚拟 dom,将上一份和这一份虚拟 dom 进行对比,找出须要更新的部分,更新真实 dom。
真实的 dom 是由 节点(Node)组成,虚拟 dom 则是由虚拟节点(vNode)组成。
虚拟 dom 在 vue 中主要作两件事:
“虚拟 DOM”是咱们对由 Vue 组件树创建起来的整个 VNode 树的称呼 —— vue 官网
上文提到,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 是由有效属性区分。例如 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 个属性:
组件节点与元素节点相似,包含两个独有的属性:
前面已经介绍了虚拟 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); } }
有些复杂,并且涉及子节点更新,本文就不展开。
其余章节请看: