4.Virtual DOM
4-1 什么是Virtual DOM
Virtual DOM 是vue2.0以后才引入的概念,一个真实的DOM他的操作性能是比较高的,一个div元素的第一层key,就有将近300个属性,Virtual DOM 是用 JavaScript 对象来表示 DOM 信息和结构,也就是在js和DOM之间做了一层缓存,去操作一个VDOM的代价自然是要小于操作一个真实DOM的,而对于真实DOM的任何细微操作都有可能引起页面的 重绘回流,从性能的角度出发,这是我们不愿意看到的情况,所以通过对象模拟出来的DOM结构,我们可以对他先进行操作,最后统一把修改映射到真实DOM上。Vue2.0的Virtual DOM参考自 snabbdom。
4-2 Virtual DOM相比真实DOM的优势
-
虚拟 DOM 不会立刻进行
回流与重绘操作 -
虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
-
虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部
5.vm._render()
vm._render()最终会返回一个 VNode,传入_update函数
核心部分
vnode = render.call(vm._renderProxy, vm.$createElement)
即vm._renderProxy来调用render方法,并把vm.$createElement作为参数传入
// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
...
let vnode
...
try {
...
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
...
}
...
return vnode
}
5-1._renderProxy
_renderProxy在生产环境下为vm实例本身,在development环境下则执行initProxy(vm)
// src/core/instance/init.js
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
initProxy其实是在开发模式下,通过es6的 Proxy 的has方法,来判断,访问的该值,是否在vm实例上,如果不在,会在控制台报出相应的警告
// src/core/instance/proxy.js
...
initProxy = function initProxy (vm) {
/**
* const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
* function isNative (Ctor) {
* return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
* }
*/
// 首先判断浏览器是否存在es6的Proxy,并且Proxy需要是浏览器的内置类型
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
// 执行hasHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
...
const hasHandler = {
has (target, key) {
//
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
// 如果访问了不存在于vm上的属性,则触发warnNonPresent
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
...
// 这是vue一个比较常见的报错提示
// 当使用了一个并没有在data,props或者methods定义的变量,则会报这个警告
const warnNonPresent = (target, key) => {
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
}
5-2.$createElement
vm.$createElement 和 vm._c最终返回createElement函数唯一不同的是,vm._c在调用createElement最后一个参数传入的是false,而vm.$createElement 最后一个参数传入的为true
// src/core/instance/render.js
...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
// 给tampate编译成的render函数使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// 给用户自定义的render函数使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
createElement函数实际上为_createElement函数处理了传入的参数,实际_createElement才是核心的生成vnode函数。
// src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 如果传入的data是数组,那么在调用_createElement时,data开始每一项参数后移,data置空
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 如果最后一个参数为true,那么调用_createElement的最后一个参数为2,否则为1
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// 最终真正调用 _createElement
return _createElement(context, tag, data, children, normalizationType)
}
我们自己手写一个render函数,看看在_createElement中是如何执行的
new Vue({
el: '#app',
render(c) {
return c(
'div',
{
attrs: {
id: 'app'
}
},
'cherish'
)
}
})
_createElement函数主要做了
- 判断normalizationType,对children进行
normalizeChildren或simpleNormalizeChildren - 判断tag类型,以及
config.isReservedTag(tag)是否是相关原生浏览器保留节点等,决定如何生成相关vnode - 返回vnode
// src/core/vdom/create-element.js
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
// 根据normalizationType来决定对children
// 进行normalizeChildren还是simpleNormalizeChildren
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
...
// 如果tag属性是浏览器的保留原生标签,那么创建相应 VNode
if (config.isReservedTag(tag)) {
...
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
...
}
// 如果tag不为string,则执行创建组件操作
else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
5-2-1 normalizeChildren vs simpleNormalizeChildren
用户写的render函数,会触发vm.$createElement则调用的为createElement(vm, a, b, c, d, true) 执行 _createElement时,normalizationType = ALWAYS_NORMALIZE,则normalizationType = 2,最终会走到children = normalizeChildren(children)
simpleNormalizeChildren是对children进行了一个数组的铺平操作,但是仅仅铺平一层,而normalizeChildren则是对数组进行一个深度递归的铺平操作
// src/core/vdom/helpers/normalize-children.js
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
// 如果发现子元素是一个数组,则进行铺平操作
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
export function normalizeChildren (children: any): ?Array<VNode> {
// 首先判断children是否是一个一个基本数据类型
/**
* function isPrimitive (value) {
* return (
* typeof value === 'string' ||
* typeof value === 'number' ||
* typeof value === 'symbol' ||
* typeof value === 'boolean'
* )
* }
*/
return isPrimitive(children
// 如果是,则创建一个TextVNode
? [createTextVNode(children)]
// 否则判断children是否是一个array
: Array.isArray(children)
// 如果是array则进行normalizeArrayChildren(children)
? normalizeArrayChildren(children)
: undefined
}
normalizeArrayChildren最终返回一个 Array<VNode>,本次示例,children为一个string 'cherish' 最终调用了createTextVNode( new VNode(undefined, undefined, undefined, String(val))),创建了一个本文节点VNode。
// src/core/vdom/helpers/normalize-children.js
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// nested
// 如果c children[i]是一个数组,并且length > 0
// 则递归调用normalizeArrayChildren
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// merge adjacent text nodes
// 如果这组的第一个节点是一个文本节点,并且上一组的最后一个节点也是文本节点
// 那么进行一个合并的操作
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
}
// 对原生节点的处理
else if (isPrimitive(c)) {
...
res.push(createTextVNode(c))
...
}
// 其他情况的处理
else {
...
res.push(c)
...
}
}
// 最终返回一个Array<VNode>
return res
}
5-2-2 VNode
创建出相应的VNode
/ src/core/vdom/create-element.js
...
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
...
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag, // "div"
data, // {attrs: {…}}
children, // [VNode]
text, // undefiend
context // vm
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
...
}
}
至此,vm._render()通过render.call(vm._renderProxy, vm.$createElement)生成了VNode,并成为了 vm._update(vm._render(), hydrating)的第一个参数
6.vm._update()
在首次没有prevVnode的情况下,vm._update()会触发vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)进行初始化
// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
...
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
__patch__的在web下的定义是patch函数,否则是空,这也是为了不同平台下的兼容性所做的处理
// src/platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch函数实际上是createPatchFunction({ nodeOps, modules }),传入了两个参数nodeOps和modules
// src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
nodeOps中提供了很多辅助函数,用于去操作真实的DOM,modules是一些dom生成相关的class,event,attrs等
src/platforms/web/runtime/node-ops.js
/* @flow */
import { namespaceMap } from 'web/util/index'
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
...
createPatchFunction首先解析出传入的 modules 和 nodeOps,中间是许多函数,这些辅助函数,去实现了各种各样的逻辑,最终返回了patch这个方法也就是说Vue.prototype.__patch__ === createPatchFunction({ nodeOps, modules }) === patch,那么为什么要绕这么一大圈去定义patch,这里边使用了函数柯里化的思想,将平台差异化的内容,在createPatchFunction调用的时候去处理好,这样对于最终调用patch这个方法的时候,他无需再去写许多的if else这些判断平台相关的内容,这也是一个典型的适配器模式的运用
src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 初始化每个阶段的钩子
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
以一个简单例子来分析path为我们做了什么
new Vue({
el: '#app',
data() {
return {
message: 'likefan'
}
},
render(c) {
return c(
'div',
{
attrs: {
id: 'app'
}
},
this.message
)
}
})
核心部分 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
// src/core/instance/lifecycle.js
// 首次渲染没有prevVnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
在patch中则先把真实DOM(div#App)转化为Vnode,然后触发createElm方法,把虚拟DOM转换为了真实DOM
// src/core/vdom/patch.js
function emptyNodeAt (elm) {
// 把传入的Node转化为一个空的VNode,new VNode第5个参数传递的是真实DOM
// 则最终生成的VNode的elm属性为之前的真实Dom
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// oldVnode = vm.$el
if (isUndef(oldVnode)) {
...
} else {
// 首次触发 oldVnode 是一个真实element节点,触发else
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
...
} else {
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 把真实DOM转换为了VNode
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
// oldElm是之前的真实DOM,即之前的vm.$el (div #app)
const oldElm = oldVnode.elm
// parentElm 为 oldElm.parentNode 则为body元素
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 把虚拟DOM转换为真实DOM
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
...
}
}
}
createElm方法通过nodeOps.createElement 生成一个真实DOM,挂载到vnode的elm上,然后触发 createChildren(vnode, children, insertedVnodeQueue),最终挂载到el的parentNode上
// src/core/vdom/patch.js
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
const data = vnode.data // {attrs: {…}}
const children = vnode.children // [VNode]
const tag = vnode.tag // 'div'
if (isDef(tag)) {
// 常见的一个报错,使用未注册的组件
if (process.env.NODE_ENV !== 'production') {
...
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// vnode上不存在ns,则触发 nodeOps.createElement(tag, vnode)
// nodeOps.createElement实际是 通过document.createElement(tagName)
// 生成一个真实DOM最终返回
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} 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)
}
}
createChildren方法处理vnode.children,如果是一个文本节点,则直接插入,如果children是一个数组,那么会循环的调用createElm方法,形成一个递归,直到他的children不再是一个vnode而是一个文本节点,这样会先执行子节点的 insert(parentElm, vnode.elm, refElm),最终执行父节点的insert,(insert根据是否传入了参考节点,使用了insertBefore或appendChild) 这样保证一次性插入,不做多余的真实dom操作,引发频繁的重绘和回流(message先插入div,div再插入body)
// src/core/vdom/patch.js
function createChildren (vnode, children, insertedVnodeQueue) {
// 如果children是一个数组
if (Array.isArray(children)) {
...
// 循环调用createElm方法,把children的每一项作为vnode传入
// 把vnode.elm 作为 createElm 的第三个参数 parentElm
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
}
// 否则如果 vnode的text属性是一个基本数据类型
// 那么往这个真实dom节点上插入这个文本节点
else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
执行createElm函数后,页面已经渲染出了新添加的元素节点,但是旧的节点还是没有移除,最终通过removeVnodes删除旧的节点
// src/core/vdom/patch.js
...
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
...
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
...
回顾一下new Vue之后发生了什么
new Vue -> init -> $mount -> compile(template) -> render -> vnode -> patch -> DOM
下一章分析vue组件化的过程