为什么会出现虚拟DOM这种技术,多复杂啊,增加了代码的难度,也不易于代码的阅读,那么接下来我带着大家看看虚拟DOM的必要性~
整体流程梳理
- 用户修改或者操作数据
- 触发
Object.defineProperty中的setter setter触发depdep.notify()通知watcher更新watcher执行updateupdate让我们重新render,生成虚拟domrender完成后,重新update将虚拟dom转为真实dom
真实DOM是如何解析的

浏览器渲染进程中有GUI渲染线程,负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局与绘制等。
工作流程分为以下5步:
1. 创建DOM树
解析HTML文件,构建DOM树,同时浏览器主进程负责下载css文件
2. 生成css样式表
css文件下载完成,用css解析器,解析css文件和元素上的inline样式,生成页面的样式表
3. 构建Render树
将DOM树和样式表关联起来,构建一颗Render树
4. 布局
根据Render树结构,为每个Render树上的节点确定尺寸,位置等
5. 绘制页面
根据Render树及节点坐标,将它们绘制出来
注意:
理解了GUI的渲染过程,我们也就理解了为什么平时一直强调css文件的引入要放入头部,是为了尽早的完成页面的渲染,而JS文件放到body底部引入,是因为JS引擎执行脚本的时候,GUI渲染线程是被挂起的,两者是互斥的,会影响css文件的引入,所以为了尽快将页面展示出来,css文件放头部,js文件放底部。
虚拟DOM
- 概念
虚拟DOM(virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
-
优点
- 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM对比可以得到最小DOM操作量,从而提升性能与用户体验。
- 页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象速度会更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
- 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台。
- 兼容性:还可以加入兼容性代码增强操作的兼容性。
-
必要性
- vue1.0中有细粒度的数据变化侦测(每一个key一个watcher),它是不需要虚拟DOM的,但是细粒度造成了大量的开销,这对于大型项目来说是不可接受的。因此,vue2.0选择了中等粒度的解决方案,每一个组件一个watcher实例,这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
- 原生JS操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。如果一次操作需要更新10个dom节点,那么浏览器会执行10次渲染流程,因为浏览器执行某次dom操作的时候,是不知道后面还有dom更新的,这样就造成了很大的浪费,而且频繁操作还会出现页面卡顿,影响用户的体验。
-
代价
消耗一些性能、消耗一些cpu,换来的是更好的用户体验。
vue是如何生成虚拟DOM
以下分析依然从vue源码分析,大家可以对照文件位置去看源码,最开始看源码的时候要以囫囵吞枣的方式看,不要扣细节,看关键点即可,后续二刷三刷的时候,再一步步去深入。
1. $mount挂载时执行了mountComponent方法
位置:src/platforms/web/runtime/index.js
功能:$mount的时候只执行了mountComponent方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
2. 渲染、更新组件
位置:src/core/instance/lifecycle.js
功能:mountComponent做了什么?创建一个更新函数,创建一个watcher,两者之间要进行挂钩,watcher收到通知后,就会执行updateComponent,去执行render和update方法
mountComponent:创建一个watcher,创建一个更新函数updateComponent,将来watcher得到通知后,会执行更新函数,更新函数中会执行update方法,render函数返回虚拟dom,updat将虚拟dom转换成真实dom
// 创建更新函数
updateComponent = () => {
// _render() 生成虚拟DOM
// _update() 转换vdom为dom
vm._update(vm._render(), hydrating)
}
// 创建watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
3. render
位置:src/core/instance/render.js
功能:生成虚拟DOM,真正用来创建vnode树的函数是vm.$createElement
Vue.prototype._render = function (): VNode {
// 最终需要计算出的虚拟dom,在vue中称为vnode
let vnode
// 执行render函数,传入参数是$createElement
// render(h){}
vnode = render.call(vm._renderProxy, vm.$createElement)
}
// 声明vm.$createElement
function initRender (vm: Component) {
// 声明了两个方法:_c与$createElement
// _c:编译器生成的render函数用这个
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// $createElement用户编写的render用这个
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
4. createElement方法
位置:src/core/vdom/create-element.js
功能:$createElement()是对createElement函数的封装,createElement就是生成虚拟DOM,createComponent用于创建组件并返回VNode
function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode>{
// 处理传入的data
...
// vnode生成过程
// 传入tag可能是原生的html标签,也可能是自定义组件标签
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 原生标签
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
// 直接创建vnode实例
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
return vnode
}
5. VNode对象
位置:src/core/vdom/vnode.js
功能:render返回的一个VNode实例,它的children还是VNode,最终构成一个树,就是虚拟DOM树,
介绍:
一个VNode的实例对象包含了以下属性:
- tag:当前节点的标签名
- data:当前节点的数据对象
- children:数组类型,包含了当前节点的子节点
- text:当前节点的文本,一般文本节点或注释节点会有该属性
- ns:节点的namespace
- componentOptions:创建组件实例时会用到的选项信息
- child:当前节点对应的组件实例
- parent:组件的占位节点
- raw:raw HTML
- isStatic:静态节点的标识
- isRootInsert:是否作为根节点插入,被包裹的节点,该属性的值为false
- isComment:当前节点是否为克隆节点
- isCloned:当前节点是否为克隆节点
- isOnce:当前节点是否有
v-once指令
VNode分类
VNode可以理解为vue框架的虚拟dom的基类,通过new实例化的VNode大致可以分为几类
-
EmptyVNode: 没有内容的注释节点
-
TextVNode: 文本节点
-
ElementVNode: 普通元素节点
-
ComponentVNode: 组件节点
-
CloneVNode: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true
-
...
/* @flow */
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?: 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
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
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
}
6. 生成虚拟dom呈现
- 真实dom
// 真实dom
<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>
- 虚拟dom
// 虚拟dom
var container = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
- 将上面的
container数据结构打印出来

至此,Vdom生成结束。
vue是如何将虚拟DOM转为真实DOM的
1. vue源码层层调用,是怎么走到patch打补丁的
(1) _update
位置:core\instance\lifecycle.js
功能:update负责更新dom,转换vnode为dom
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染-initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新-updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
(2) patch
位置:platforms/web/runtime/index.js
功能:定义补丁函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
(3) patch之createPatchFunction
位置:src/core/vdom/patch.js
功能:工厂函数createPatchFunction生成了真正的patch
// 传入平台特有的节点操作方法实现跨平台
export const patch: Function = createPatchFunction({ nodeOps, modules })
nodeOps:节点操作(platforms/web/runtime/node-ops.js)
modules:属性操作(platforms/web/runtime/modules/index.js)
(4) createPatchFunction
位置:src/core/vdom/patch.js
功能:返回平台特有patch方法,backend是平台特有节点扩展代码,从700行开始浏览,patch是如何打补丁的
2. 具体分析patch
patch策略
要想实现这么低的时间复杂度,只能平层比较两棵树的节点,放弃了深度遍历,是一种相当高效的算法。

-
- new VNode不存在,old VNode存在,那就是销毁老节点,调用
invokeDestroyHook(oldVnode)
- new VNode不存在,old VNode存在,那就是销毁老节点,调用
-
- old VNode不存在,new VNode存在,那就是创建新节点,调用
createElm(vnode, insertedVnodeQueue)
- old VNode不存在,new VNode存在,那就是创建新节点,调用
-
- new VNode与old VNode都存在
- 判断是不是同一个节点,是则调用
patchVnode来更新 - 不是的话,直接replace
patch-diff算法
patchVnode
说说patchVnode,走到这一步时,我们都知道,两个VNode类型相同,那么操作dom的差异类型有以下3种情况:
- 属性更新(PROPS):修改了节点的属性,例如:删除节点上的class等
- 文本改变(TEXT):改变文本节点的文本内容
- 顺序互换(REORDER):移动、删除、新增子节点
以下不同情况的处理方式:
- 两个节点完全相同,则直接return
if (oldVnode === vnode) {
return
}
- 如果新老节点都是静态的,那么只需要替换elm以及componentInstance即可
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
- 新老节点均有children,则对子节点进行diff操作,调用
updateChildren
if (isDef(oldCh) && isDef(ch)) {
// 更新孩子
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
- 只有老节点有children,移除该DOM节点的所有子节点
if (isDef(oldCh)) {
// 老的有孩子-删除
removeVnodes(oldCh, 0, oldCh.length - 1)
}
- 只有新节点有children,先清空老节点的文本内容,然后为当前DOM节点加入子节点
if (isDef(ch)) {
// 新的有孩子,老的没有, 创建并追加孩子,
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
- 新老节点都是文本节点或注释节点,只需更新文本内容即可
if (oldVnode.text !== vnode.text) {
// 两个都有文本,修改文本
nodeOps.setTextContent(elm, vnode.text)
}
updateChildren
updateChildren主要作用是用一种比较高效的方式比对新旧两个VNode的children,得出最小操作补丁。执行一个双循环,vue中针对web场景特点做了特别的算法优化:

- 情况1:头头比较或者尾尾比较,满足相同节点,则直接将该VNode节点进行patchVnode,不需要再遍历就完成了一次循环

- 情况2:头尾比较,如果oldStartVnode与newEndVnode满足sameVnode,说明节点进行了移动,那么进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode后面

- 情况3:尾头比较,如果oldEndVnode与newStartVnode满足sameVnode,说明节点进行了移动,那么进行patchVnode的同时还需要将真实DOM节点移动到oldStartVnode前面

-
以上情况都不符合:
- newStartVnode在old VNode节点中找不到一致的key,或者是即便key相同却不是 sameVnode,这个时候会调用createElm创建一个新的DOM节点。

- 旧VNode遍历结束后,新的节点还没有找到,说明新节点新增了,直接将剩下的VNode对应的DOM插入到真实DOM中,此时调用addVnodes

- 新VNode遍历结束,老节点还有剩余,需要从文档中删除节点

let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// 循环条件,开始游标<=结束游标
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 老开始与新开始相同:直接patchVnode,游标++
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 老结束与新结束相同,打补丁两者,游标--
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 老开始与新结束相同,打补丁两者,移动老开始到结尾,游标:老开始++,新结束--
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 老结束与新开始相同,打补丁两者,移动老结束到头部,游标:老结束--,新开始++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 以上情况都不是,开始循环比较
// 从新的开头拿一个,与老的做比较
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
// 新增,并追加到头部
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 打补丁,并移动
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 老数组结束,批量创建并追加
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 新数组先结束,批量删除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
节点属性是如何更新的
以上讲述了节点的文本或者位置、子节点不同时是如何更新的,但是没有说到当节点上的属性发生变化时,要如何更新节点上的属性,一起看看~
位置:src/core/vdom/patch.js
从73行开始
// 将属性相关dom操作按hooks归类,在patchVnode时一起执行
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
// modules中是所有节点属性相关操作
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.create = [fn1,fn2,...]
// cbs.update = [fn1,fn2,...]
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// 以上代码的具体拆解
modules= [
attrs,
klass,
events,
domProps,
style,
transition
]
attrs={
create: updateAttrs,
update: updateAttrs
}
klass={
create: updateClass,
update: updateClass
}
events={
create: updateDOMListeners,
update: updateDOMListeners
}
domProps={
create: updateDOMProps,
update: updateDOMProps
}
style={
create: updateStyle,
update: updateStyle
}
transition={
create: _enter,
activate: _enter,
remove (vnode: VNode, rm: Function) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
}
cbs:{
'create':[updateAttrs,updateClass,updateDOMListeners,updateDOMProps,updateStyle,_enter],
'activate':[_enter],
'update':[updateAttrs,updateClass,updateDOMListeners,updateDOMProps,updateStyle],
'remove':[_enter],
'destroy':[]
}
跳至570行——patchVnode
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 更新属性
if (isDef(data) && isPatchable(vnode)) {
// 执行默认的钩子
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 执行用户定义的钩子
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
}
位置:src/platform/web/modules/attrs.js
功能:节点上的属性具体如何更新的,以attr为例
// updateAttrs
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
const opts = vnode.componentOptions
if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {
return
}
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
return
}
let key, cur, old
const elm = vnode.elm
const oldAttrs = oldVnode.data.attrs || {}
let attrs: any = vnode.data.attrs || {}
// clone observed objects, as the user probably wants to mutate it
if (isDef(attrs.__ob__)) {
attrs = vnode.data.attrs = extend({}, attrs)
}
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if (old !== cur) {
setAttr(elm, key, cur)
}
}
// #4391: in IE9, setting type can reset value for input[type=radio]
// #6666: IE/Edge forces progress value down to 1 before setting a max
/* istanbul ignore if */
if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {
setAttr(elm, 'value', attrs.value)
}
for (key in oldAttrs) {
if (isUndef(attrs[key])) {
if (isXlink(key)) {
elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else if (!isEnumeratedAttr(key)) {
elm.removeAttribute(key)
}
}
}
}
// setAttr
function setAttr (el: Element, key: string, value: any) {
if (el.tagName.indexOf('-') > -1) {
baseSetAttr(el, key, value)
} else if (isBooleanAttr(key)) {
// set attribute for blank value
// e.g. <option disabled>Select one</option>
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
// technically allowfullscreen is a boolean attribute for <iframe>,
// but Flash expects a value of "true" when used on <embed> tag
value = key === 'allowfullscreen' && el.tagName === 'EMBED'
? 'true'
: key
el.setAttribute(key, value)
}
} else if (isEnumeratedAttr(key)) {
el.setAttribute(key, convertEnumeratedValue(key, value))
} else if (isXlink(key)) {
if (isFalsyAttrValue(value)) {
el.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else {
el.setAttributeNS(xlinkNS, key, value)
}
} else {
baseSetAttr(el, key, value)
}
}
patch() => patchVnode() => cbs.update[i](oldVnode, vnode)
组件是怎么更新的
组件声明
位置:src/core/global-api/assets.js
功能:使用extend方法,将传入组件配置转换为构造函数
// src/core/index.js
// 全局api注册
initGlobalAPI(Vue)
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
initAssetRegisters(Vue)
}
// src/core/global-api/assets.js
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
// ['component','filter','directive']
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
// component处理:指定name,获取组件构造函数
// Vue.component('comp',{template:''})
definition.name = definition.name || id
// 转换组件配置对象为构造函数
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 全局注册:options[components]=Ctor
this.options[type + 's'][id] = definition
return definition
}
}
})
}
全局注册组件后,在实例的components中可找到注册的组件

组件创建及挂载
- 创建自定义组件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> {
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// resolveAsset(context.$options, 'components', tag):当前组件实例的选项中有没有components的配置,里面有没有tag的配置
// component
// 首先查找自定义组件构造函数声明
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
}
- createComponent
位置:src/core/vdom/create-component.js
// 185行
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// install component management hooks onto the placeholder node
// 安装组件的钩子:
installComponentHooks(data)
}
// 228行
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
// 由于用户也有可能传递自定义钩子函数,所以需要合并一下
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
// 36行
const componentVNodeHooks = {
// 初始化钩子用来创建组件实例和挂载的
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
insert (vnode: MountedComponentVNode) {},
destroy (vnode: MountedComponentVNode) {}
}
首先创建的是根组件,首次_render()时,会得到整棵树的VNode结构
整体流程:new Vue() => $mount() => vm._render() => createElement() => createComponent()
- 创建自定义组件实例
位置:src/core/vdom/patch.js
功能:根组件执行更新函数时,会递归创建子元素和子组件
// patch
// 首次执行_update()时,patch()会通过createEle()创建根元素,子元素创建也就从这里开始
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 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)
)
}
//createElm
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
}
}
// createComponent
// 自定义组件创建
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// 获取data,主要是hook
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 执行初始化钩子函数
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 创建自定义组件实例并挂载
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
// 判断组件已经创建完毕 有组件实例
if (isDef(vnode.componentInstance)) {
// 属性初始化
initComponent(vnode, insertedVnodeQueue)
// dom插入操作
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
// initComponent
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
// invokeCreateHooks
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
结论:
组件创建顺序自上而下
组件挂载顺序自下而上
事件处理整体流程
- 编译阶段
处理为data中的on
(function anonymous() {
with(this){return _c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("事件处理机制")]),_v(" "),
_c('p',{on:{"click":onClick}},[_v("this is p")]),_v(" "), _c('comp',{on:{"myclick":onMyClick}})
],1)} })
-
事件处理分为:普通事件与自定义事件
<div id="demo"> <h1>事件处理机制</h1> <!--普通事件--> <p @click="onClick">this is p</p> <!--自定义事件--> <comp @myclick="onMyClick"></comp> </div>- 普通事件
位置:src/platform/web/runtime/modules/events.js
整体流程:patch() => createElm() => invokeCreateHooks() => updateDOMListeners()
// patch.js——patch function patch (oldVnode, vnode, hydrating, removeOnly) { 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) ) } // patch.js——createElm function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { invokeCreateHooks(vnode, insertedVnodeQueue) } // patch.js——invokeCreateHooks function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } } // src/platform/web/runtime/modules/events.js——updateDOMListeners function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} target = vnode.elm normalizeEvents(on) updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) target = undefined } // src/core/vdom/helpers/update-listeners.js——updateListeners export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event for (name in on) { def = cur = on[name] old = oldOn[name] event = normalizeEvent(name) /* istanbul ignore if */ if (__WEEX__ && isPlainObject(def)) { cur = def.handler event.params = def.params } if (isUndef(cur)) { process.env.NODE_ENV !== 'production' && warn( `Invalid handler for event "${event.name}": got ` + String(cur), vm ) } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } // 执行监听事件 add(event.name, cur, event.capture, event.passive, event.params) } else if (cur !== old) { old.fns = cur on[name] = old } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name) remove(event.name, oldOn[name], event.capture) } } } // src/platform/web/runtime/modules/events.js——add function add ( name: string, handler: Function, capture: boolean, passive: boolean ) { // async edge case #6566: inner click event triggers patch, event handler // attached to outer element during patch, and triggered again. This // happens because browsers fire microtask ticks between event propagation. // the solution is simple: we save the timestamp when a handler is attached, // and the handler would only fire if the event passed to it was fired // AFTER it was attached. if (useMicrotaskFix) { const attachedTimestamp = currentFlushTimestamp const original = handler handler = original._wrapper = function (e) { if ( // no bubbling, should always fire. // this is just a safety net in case event.timeStamp is unreliable in // certain weird environments... e.target === e.currentTarget || // event is fired after handler attachment e.timeStamp >= attachedTimestamp || // bail for environments that have buggy event.timeStamp implementations // #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState // #9681 QtWebEngine event.timeStamp is negative value e.timeStamp <= 0 || // #9448 bail if event is fired in another document in a multi-page // electron/nw.js app, since event.timeStamp will be using a different // starting reference e.target.ownerDocument !== document ) { return original.apply(this, arguments) } } } // addEventListener target.addEventListener( name, handler, supportsPassive ? { capture, passive } : capture ) }- 自定义事件
位置:src/core/instance/events.js
整体流程:patch() => createElm() => createComponent() => hook.init() => createComponentInstanceForVnode() => _init() =>initEvents() => updateComponentListeners()
// 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> { if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // resolveAsset(context.$options, 'components', tag):当前组件实例的选项中有没有components的配置,里面有没有tag的配置 // component // 首先查找自定义组件构造函数声明 vnode = createComponent(Ctor, data, context, children, tag) } } // src/core/vdom/create-component.js——createComponent export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // 安装组件的钩子: installComponentHooks(data) } // src/core/vdom/create-component.js——installComponentHooks function installComponentHooks (data: VNodeData) { const hooks = data.hook || (data.hook = {}) // 由于用户也有可能传递自定义钩子函数,所以需要合并一下 for (let i = 0; i < hooksToMerge.length; i++) { const key = hooksToMerge[i] const existing = hooks[key] const toMerge = componentVNodeHooks[key] if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } // src/core/vdom/create-component.js——componentVNodeHooks const componentVNodeHooks = { // 初始化钩子用来创建组件实例和挂载的 init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {}, insert (vnode: MountedComponentVNode) {}, destroy (vnode: MountedComponentVNode) {} } // src/core/vdom/create-component.js——createComponentInstanceForVnode // src/core/instance/events.js——initEvents export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } } // src/core/instance/events.js——updateComponentListeners export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined } function add (event, fn) { target.$on(event, fn) } // src/core/vdom/helpers/update-listeners.js——updateListeners export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event for (name in on) { def = cur = on[name] old = oldOn[name] event = normalizeEvent(name) /* istanbul ignore if */ if (__WEEX__ && isPlainObject(def)) { cur = def.handler event.params = def.params } if (isUndef(cur)) { process.env.NODE_ENV !== 'production' && warn( `Invalid handler for event "${event.name}": got ` + String(cur), vm ) } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } // 执行事件监听 add(event.name, cur, event.capture, event.passive, event.params) } else if (cur !== old) { old.fns = cur on[name] = old } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name) remove(event.name, oldOn[name], event.capture) } } }事件监听和派发者均是组件实例,自定义组件中一定伴随着原生事件的监听与处理
v-model双向绑定实现原理
编译阶段
- 测试代码:
<body>
<div id="demo">
<h1>双向绑定机制</h1>
<!--表单控件绑定-->
<input type="text" v-model="foo">
<!--自定义事件-->
<comp v-model="foo"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 声明自定义组件
Vue.component('comp', {
template: `
<input type="text" :value="$attrs.value"
@input="$emit('input', $event.target.value)">
`
})
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: 'foo' }
});
</script>
</body>
- 对
v-model进行特殊处理
// 生成的渲染函数 (function anonymous() {
with(this){return _c('div',{attrs:{"id":"demo"}},[ _c('h1',[_v("双向绑定机制")]),_v(" "), _c('input',{directives:[{name:"model",rawName:"v-model",value:
(foo),expression:"foo"}],attrs:{"type":"text"},domProps:{"value":
(foo)},on:{"input":function($event)
{if($event.target.composing)return;foo=$event.target.value}}}),_v(" "),
_c('comp',{model:{value:(foo),callback:function (?v)
{foo=?v},expression:"foo"}})
],1)} })
// input——原生标签
_c('input',{
directives:[{
name:"model",
rawName:"v-model",
value:(foo),
expression:"foo"}],
attrs:{"type":"text"},
domProps:{"value":(foo)},
on:{
"input":function($event){
if($event.target.composing) return;
foo=$event.target.value
} }
})
// comp——自定义组件
// <comp value="foo" @input="">
_c('comp',{
model:{
value:(foo),
callback:function (?v) {foo=?v},
expression:"foo"
}
})
整体流程
- 初始化阶段:对节点赋值及事件监听
- 对节点赋值:platforms\web\runtime\modules\dom-props.js
function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (key === 'value' && elm.tagName !== 'PROGRESS') {
// store value as _value as well since
// non-string values will be stringified
elm._value = cur
// avoid resetting cursor position when value is the same
const strCur = isUndef(cur) ? '' : String(cur)
if (shouldUpdateValue(elm, strCur)) {
elm.value = strCur
}
}
}
- 事件监听:platforms\web\runtime\modules\events.js
function add (
name: string,
handler: Function,
capture: boolean,
passive: boolean
) {
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
- 额外的model指令:platforms\web\runtime\directives\model.js
const directive = {
inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', () => {
directive.componentUpdated(el, binding, vnode)
})
} else {
setSelected(el, binding, vnode.context)
}
el._vOptions = [].map.call(el.options, getValue)
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd)
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true
}
}
}
},
componentUpdated (el, binding, vnode) {
if (vnode.tag === 'select') {
setSelected(el, binding, vnode.context)
// in case the options rendered by v-for have changed,
// it's possible that the value is out-of-sync with the rendered options.
// detect such cases and filter out values that no longer has a matching
// option in the DOM.
const prevOptions = el._vOptions
const curOptions = el._vOptions = [].map.call(el.options, getValue)
if (curOptions.some((o, i) => !looseEqual(o, prevOptions[i]))) {
// trigger change event if
// no matching option found for at least one value
const needReset = el.multiple
? binding.value.some(v => hasNoMatchingOption(v, curOptions))
: binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions)
if (needReset) {
trigger(el, 'change')
}
}
}
}
}
- 自定义组件会转换为属性和事件:core/vdom/create-component.js
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
转换完以后:

- 自定组件事件监听:core\instance\events.js
function add (event, fn) {
target.$on(event, fn)
}
- 自定义组件可以指定
v-model石建明和属性名
model:{
prop:'foo',
event:"change"
}

不同类型输入项编译结果和后续处理是不同的
_c('input', {
directives: [{ name: "model", rawName: "v-model", value: (foo),
expression: "foo" }],
attrs: { "type": "checkbox" },
domProps: { "checked": Array.isArray(foo) ? _i(foo, null) > -1 :
(foo) }, on: {
"change": function ($event) {
var ?a = foo,
?el = $event.target,
?c = ?el.checked ? (true) : (false);
if (Array.isArray(?a)) {
var ?v = null, ?i = _i(?a, ?v);
if (?el.checked) { ?i < 0 && (foo =
?a.concat([?v])) }
else {
?i > -1 && (foo = ?a.slice(0,
?i).concat(?a.slice(?i + 1)))
}
} else {
foo = ?c
}
} }
})
总结
整体流程如下,大家可以对着去理解。
