[TOC]
- Vue 学习笔记
- Vue 源码解析 - 主线流程
- Vue 源码解析 - 模板编译
- Vue 源码解析 - 组件挂载
- Vue 源码解析 - 数据驱动与响应式原理
前言
前文在对 Vue 整体流程进行分析时,我们已经知道对于 Runtime + Compiler 的编译版本来说,Vue 在实例化前总共会经历两轮mount
过程,分别为:
定义于
src\platforms\web\runtime\index.js
的$mount
函数,主要负责组件挂载功能。定义于
src\platforms\web\entry-runtime-with-compiler.js
的$mount
函数,主要负责模板编译 + 组件挂载(其会缓存src\platforms\web\runtime\index.js
中定义的$mount
函数,最后的组件挂载转交给该函数进行处理)功能。
组件挂载
以下我们对src\platforms\web\runtime\index.js
的$mount
函数进行解析,主要分析 组件挂载 部分内容:
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
$mount
函数内部直接通过mountComponent
进行组件挂载功能,其源码如下:
// src/core/instance/lifecycle.js
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有 render 函数,则进行默认设置,并给出警告
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
...
warn(...)
...
}
}
callHook(vm, 'beforeMount')
let updateComponent
...
updateComponent = () => {
...
const vnode = vm._render()
...
vm._update(vnode, hydrating)
...
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent
主要做了如下几件事:
- 如果
vm.$options
没有定义render
函数,则将其render
设置为createEmptyVNode
,一个用于产生空虚拟节点的函数,并给出警告:// src/core/vdom/vnode.js export const createEmptyVNode = (text: string = '') => { const node = new VNode() node.text = text node.isComment = true return node }
- 立即触发
beforeMount
事件,表示即将进入挂载过程。 - 实例化一个
Watcher
实例对象(渲染Watcher
):
// src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// src/core/observer/watcher.js
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
...
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
this.getter = expOrFn
...
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
...
value = this.getter.call(vm, vm)
...
}
...
}
注:在 Vue 中,Watcher
有两种类别:用户Watcher
和 渲染Watcher
。此处就是一个渲染Watcher
,它会引起updateComponent
过程,从而触发render
和update
过程,从而完成数据渲染到视图整个流程。
更多Watcher
信息,请参考:Vue 源码解析 - 数据驱动与响应式原理
简单看下Watcher
源码,可以看到,Watcher
的构造函数内会直接调用参数expOrFn
,对于mountComponent
函数来说,即会直接回调updateComponent
函数,而updateComponent
其实主要就做了两件事:
// src/core/instance/lifecycle.js
updateComponent = () => {
...
const vnode = vm._render()
...
vm._update(vnode, hydrating)
...
}
首先通过vm._render
函数创建一个虚拟节点vnode
,然后将该虚拟节点交给vm._update
函数进行渲染。
- 我们先来看下
vm._render
函数。
我们先查阅一下Vue._render
函数的定义链,首先回到主线流程,src/core/instance/index.js
文件中定义了Vue
之后,会采用 Mixin 方式为Vue
添加一些其他功能,其中就有renderMixin
,源码如下:
// src/core/instance/index.js
import {renderMixin} from './render'
...
function Vue(options) {
this._init(options)
}
...
renderMixin(Vue)
export default Vue
进入renderMixin
函数,查看其源码:
// src/core/instance/render.js
export function renderMixin(Vue: Class) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
...
}
}
到这里,我们就找到了vm._render
的定义之处了,查看下_render
函数源码如下:
// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {render, _parentVnode} = vm.$options
...
// render self
let vnode
...
vnode = render.call(vm._renderProxy, vm.$createElement)
...
return vnode
}
所以,_render
函数内部是通过vm.$options.render
函数渲染出一个虚拟节点vnode
的。
注:我们在主线流程中有讲过,Vue 构建完成后会生成两种 Vue.js 版本:Runtime Only 和 Runtime + Compiler。
在 Vue 源码解析 - 模板编译 中提过,Runtime + Compiler版本会含有两个$mount
函数定义:
- 定义于
src\platforms\web\runtime\index.js
的$mount
函数,主要负责组件挂载功能。 - 定义于
src\platforms\web\entry-runtime-with-compiler.js
的$mount
函数,主要负责模板编译 + 组件挂载(其会缓存src\platforms\web\runtime\index.js
中定义的$mount
函数,最后的组件挂载转交给该函数进行处理)功能。
分析到这里,其实 Vue 中的这种实现意图就已经清楚了,之所以定义了两个$mount
函数,原因就是无论对于哪个版本的 Vue.js,组件挂载都是必需的,而 Vue 创建虚拟节点始终都需要通过一个render
函数进行创建。因此:
- 对于 Runtime + Compiler 版本,
$mount
函数的第一步为模板编译,这一步最终就会生成一个渲染模板的render
函数,然后才可进行组件挂载。 - 而对于 Runtime Only 版本,
render
函数是用户自己手动提供的,因此只需直接进行组件挂载即可。
由于render
函数要么是用户手动提供的,要么就是模板在线自动编译生成的,因此 Vue 源码内部没有对该函数的定义信息,所以如果要了解render
函数的内部调用逻辑,就只能通过官网查询(即手动提供render
函数)或查看模板自动编译生成的render
函数具体内容。
通过官网查看下该函数使用方法如下:
// 比如对于如下模板
{{ blogTitle }}
// 用户自定义:以下函数相当于如上模板
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
// 模板在线编译:以下函数相当于如上模板
(function anonymous() {
with (this) {
return _c('h1', [_v(_s(blogTitle))])
}
})
可以看到,render
函数的内部调用逻辑有两种:
- 对于 用户提供的
render
函数,最终会通过vm.$createElement
进行虚拟节点的创建; - 对于 模板自动编译的
render
函数,其内部最终会通过vm._c
进行虚拟节点的创建。
在Vue.prototype._render
函数内,可以查看到其源码:
// src/core/instance/render.js
export function initRender(vm: Component) {
...
// 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
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.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
vm.c
和vm.$createElement
函数的区别只在于最后一个参数上alwaysNormalize
:
- 对于
vm.c
,由于其被使用的render
函数是模板编译生成的,因此无须始终进行规范化。 - 而对于
vm.$createElement
,由于其被使用的render
函数是由用户手动编写的,因此需要进行规范化,让所有节点都符合VNode
类型。
但无论是vm.c
,还是vm.$createElement
,它们的函数内部都是通过createElement
函数生成虚拟节点,因此,我们查看下createElement
函数源码:
// src/core/vdom/create-element.js
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array {
...
return _createElement(context, tag, data, children, normalizationType)
}
createElement
函数内部通过_createElement
来真正生成vnode
,其源码如下:
// src/core/vdom/create-element.js
export function _createElement(
context: Component,
tag?: string | Class | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array {
...
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
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
...
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
}
_createElement
函数内部主要做了两件事:
规范化子节点:通过
normalizeChildren
或simpleNormalizeChildren
对子节点进行规范化,即将子节点转成VNode
类型。
规范化的细节就不深入进行分析了,其主要作用就是遍历子节点,如果子节点为数组类型,就进行打平,使深度为 1,如果是基本类型,就通过createTextVNode
将其转为VNode
···-
生成虚拟节点:规范化子节点后,就会进行虚拟节点的创建,总共有如下两种情况:
-
如果标签
tag
是string
类型,则:- 如果是内置标签,则直接创建一个
VNode
。 - 如果是本地注册组件标签名,则通过
createComponent
创建一个组件类型的VNode
虚拟节点。 - 否则创建一个未知标签的
VNode
。
- 如果是内置标签,则直接创建一个
如果标签
tag
不是string
类型,则直接通过createComponent
创建一个组件类型的VNode
虚拟节点。
-
注:VNode
即虚拟节点,render
函数的主要作用就是生成虚拟节点,虚拟节点是 Vue 中用来对真实 DOM 节点的映射,之所以采用虚拟节点这一层映射关系,主要是因为 DOM 的量级比较重,并且对真实 DOM 的操作可能会引起页面进行无谓的重新渲染,而页面渲染是很耗费性能的操作,因此,Vue 采用 虚拟DOM 的机制,通过新生成的VNode
对象与旧的VNode
对象间的一系列的赋值对比等操作(不会引起页面重新渲染),就可以准确地识别出需要进行更新渲染的位置,再映射到真实 DOM 上,这样就能大大提升性能。
实际上,虚拟节点就是一个普通的 Javascript 对象,在 Vue 中,其定义如下:
// src/core/vdom/vnode.js
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array;
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,
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 // 映射的真实 DOM 节点
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
}
}
到这里,vm.render
函数生成虚拟节点的过程就分析完毕了。
接下来继续看vm._update
如何将vm.render
生成的虚拟节点渲染成一个真实的 DOM 节点。
在主线流程中,文件core/instance/index.js
定义了Vue
之后,有如下初始化操作:
// src/core/instance/index.js
...
import {lifecycleMixin} from './lifecycle'
function Vue(options) {
...
}
...
lifecycleMixin(Vue)
...
vm._update
函数就定义于lifecycleMixin
函数内:
// src/core/instance/lifecycle.js
export function lifecycleMixin(Vue: Class) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
...
const prevVnode = vm._vnode
...
vm._vnode = vnode
// 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)
}
...
}
...
}
从Vue.prototype._update
源码中可以看到,_update
函数内部主要通过vm.__patch__
进行数据渲染,且总共存在两种数据渲染:
- 首次渲染:首次将虚拟节点渲染到一个真实的 DOM 中。
- 数据更新:对虚拟节点绑定的真实 DOM 节点上的数据进行更新。
这里我们着重介绍 首次渲染 流程,数据更新 流程请参考:Vue 源码解析 - 数据驱动与响应式原理
__patch__
操作是一个平台相关的操作,如下图所示:
这里我们只对 Web 平台进行分析,其源码为:
// src/core/util/env.js
export const inBrowser = typeof window !== 'undefined'
// src/platforms/web/runtime/index.js
...
import { patch } from './patch'
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
...
Web 平台分为浏览器端和服务器端,由于服务器端无须将VNode
渲染为真实 DOM,因此我们这里只分析浏览器端VNode
渲染流程。
可以看到,在浏览器端,负责对VNode
进行渲染的函数为patch
,其源码如下:
// 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})
可以看到,patch
函数是createPatchFunction
函数执行的结果,而createPatchFunction
的参数为{nodeOps,modules}
,在解析createPatchFunction
函数之前,我们先来查看下其参数对象内容:
- 首先看下
nodeOps
参数内容:
// src/platforms/web/runtime/node-ops.js
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)
}
export function parentNode (node: Node): ?Node {
return node.parentNode
}
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
export function tagName (node: Element): string {
return node.tagName
}
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
export function setStyleScope (node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
从源码中可以看到,nodeOps
就是代理了浏览器真实 DOM 的一系列操作。
所以看到这里其实就挺明显了,Vue 最后就是通过nodeOps
进行真实 DOM 的创建/修改...,所以vm._update
函数的最重要的一个功能(即VNode
转换成真实 DOM 节点)就在此处完成了。
- 接下来看下
modules
参数具体内容:
// src/platforms/web/runtime/patch.js
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
参数modules
由platformModules
和baseModules
拼接而成,其中:
-
platformModules
源码如下:
// src/platforms/web/runtime/modules/index.js
// platformModules
export default [
attrs,
klass,
events,
domProps,
style,
transition
]
// src/platforms/web/runtime/modules/attrs.js
export default {
create: updateAttrs,
update: updateAttrs
}
// src/platforms/web/runtime/modules/class.js
export default {
create: updateClass,
update: updateClass
}
// src/platforms/web/runtime/modules/events.js
export default {
create: updateDOMListeners,
update: updateDOMListeners
}
// src/platforms/web/runtime/modules/dom-props.js
export default {
create: updateDOMProps,
update: updateDOMProps
}
// src/platforms/web/runtime/modules/style.js
export default {
create: updateStyle,
update: updateStyle
}
// src/platforms/web/runtime/modules/transition.js
export default inBrowser ? {
create: _enter,
activate: _enter,
remove(vnode: VNode, rm: Function) {
/* istanbul ignore else */
if (vnode.data.show !== true) {
leave(vnode, rm)
} else {
rm()
}
}
} : {}
所以platformModules
其实就是提供了对VNode
的属性attrs
,类klass
,事件监听event
,domProps
,样式style
和变换transition
的一系列创建和更新等操作。
- 再来看下
baseModules
源码:
// src/core/vdom/modules/index.js
// baseModules
export default [
ref,
directives
]
// src/core/vdom/modules/ref.js
// ref
export default {
create(_: any, vnode: VNodeWithData) {...},
update(oldVnode: VNodeWithData, vnode: VNodeWithData) {...},
destroy(vnode: VNodeWithData) {...}
}
// src/core/vdom/modules/directives.js
// directives
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives(vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
所以baseModules
功能就是提供了对VNode
引用和指令directives
的创建,更新和销毁操作。
综上,modules
是一个数组,其内部子元素具备对VNode
进行操作的能力,而nodeOps
具备对真实 DOM 进行操作的能力。
到这里,我们再回过头来看下createPatchFunction
函数:
// 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]])
}
}
}
function emptyNodeAt(elm) {...}
function createRmCb(childElm, listeners) {...}
function removeNode(el) {...}
function isUnknownElement(vnode, inVPre) {...}
let creatingElmInVPre = 0
function createElm(vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {...}
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {...}
function initComponent(vnode, insertedVnodeQueue) {...}
function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {...}
function insert(parent, elm, ref) {...}
function createChildren(vnode, children, insertedVnodeQueue) {...}
function isPatchable(vnode) {...}
function invokeCreateHooks(vnode, insertedVnodeQueue) {...}
// set scope id attribute for scoped CSS.
// this is implemented as a special case to avoid the overhead
// of going through the normal attribute patching process.
function setScope(vnode) {...}
function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {...}
function invokeDestroyHook(vnode) {...}
function removeVnodes(vnodes, startIdx, endIdx) {...}
function removeAndInvokeRemoveHook(vnode, rm) {...}
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {...}
function checkDuplicateKeys(children) {...}
function findIdxInOld(node, oldCh, start, end) {...}
function patchVnode(... {...}
function invokeInsertHook(vnode, queue, initial) {...}
let hydrationBailed = false
// list of modules that can skip create hook during hydration because they
// are already rendered on the client or has no need for initialization
// Note: style is excluded because it relies on initial clone for future
// deep updates (#7063).
const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')
// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate(elm, vnode, insertedVnodeQueue, inVPre) {...}
function assertNodeMatch(node, vnode, inVPre) {...}
return function patch(oldVnode, vnode, hydrating, removeOnly) {...}
}
createPatchFunction
总体上主要做了三件事:
-
创建
VNode
操作对象cbs
:
依据前面的分析,modules
其实就具备了对VNode
进行操作的能力,但是cbs
这里对modules
进行了一些转换:modules
内部是每个子元素都具备不同的对VNode
进行操作的能力,分别为如下:
▪modules[0] = {create: updateAttrs , update: updateAttrs}
:具备对属性attrs
的创建和更新操作
▪modules[1] = {create: updateClass , update: updateClass}
:具备对类class
的更新操作
▪modules[2] = {create: updateDOMListeners , update: updateDOMListeners}
:具备对事件event
的创建和更新操作
▪modules[3] = {create: updateDOMProps , update: updateDOMProps}
:具备对domProps
的创建和更新操作
▪modules[4] = {create: updateStyle , update: updateStyle}
:具备对样式style
的创建和更新操作
▪modules[5] = {create: _enter , activate: _enter , remove}
:具备对transition
的创建,激活和移除操作
▪modules[6] = {create , update , destroy}
:具备对虚拟节点引用VNode
的创建,更新和销毁操作
▪modules[7] = {create: updateDirectives , update: updateDirectives , destroy: unbindDirectives}
:具备对指令directives
的创建,更新和销毁操作cbs
对modules
进行了转换,将对VNode
的各个操作以动作进行分类(modules
是以对VNode
的操作对象进行分类),比如,将创建操作归类到一起,将更新操作归类到一起···,如下图所示:
定义了很多 DOM 操作辅助函数,包含
VNode
和真实 DOM 之间相互转换的函数返回一个
patch
函数:这个patch
函数就是我们需要的那个函数:
// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
到这里,我们就终于得到patch
函数,现在让我们回到vm._update
的主线流程:
注:这里,我们主要对vm._update
的 首次渲染 进行分析:
" src/core/instance/lifecycle.js
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
结合我们上面的渲染函数例子一起进行分析:
注:上述例子的render
函数相当于模板{{ blogTitle }}
前面讲到,vm._update
内部通过调用vm.__patch__
,而vm.__patch__
在 Web 平台下就对应patch
函数,
所以我们接下来看下patch
函数的源码:
// src/core/vdom/patch.js
return function patch(oldVnode, vnode, hydrating, removeOnly) {
...
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
// 获取挂载 DOM 的父节点
const parentElm = nodeOps.parentNode(oldElm)
// 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)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
...
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
对应到我们上面的例子中,patch
函数此时接收到的参数为:
patch(oldVnode, vnode, hydrating, removeOnly)
其中:
-
oldVnode = vm.$el
:就是 DOM 节点:div#app
-
vnode
:就是render
函数渲染生成的节点:{{ blogTitle }}
-
hydrating
:表示服务端渲染,故此处为false
-
removeOnly
:为false
因此,当进入patch
函数后,对于 首次渲染,oldVnode
对应div#app
,是一个真实 DOM 节点,因此,patch
函数主要做了如下几件事:
- 将真实 DOM 节点包装为一个
VNode
:
// src/core/vdom/patch.js
oldVnode = emptyNodeAt(oldVnode)
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
- 将
render
函数渲染生成的虚拟节点vnode
映射到一个真实 DOM 节点:
// 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)
)
注:createElm
的第四个参数为虚拟节点进行映射的参考位置,从具体的实参nodeOps.nextSibling(oldElm)
可以知道,模板对应的真实 DOM 节点位于挂载节点div#app
的后一个兄弟节点,如下图所示:
下面查看下createElm
的源码,看下VNode
是怎样转换为真实 DOM 节点的:
// src/core/vdom/patch.js
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
// 尝试先进行组件创建
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
...
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)
}
...
} 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
转真实 DOM 的操作还是挺简单的,主要就是做了如下判断:
▪ 首先尝试将VNode
转为组件,成功则直接返回;
▪ 判断VNode
有无标签,有则创建其真实 DOM 节点及其子子节点,并插入到 DOM 文档中;
▪ 判断VNode
有无注释,有则创建对应的注释 DOM 节点,并插入到 DOM 文档中;
▪ 其他情况则将VNode
作为一个文本节点进行创建并插入到 DOM 文档中;
- 销毁旧节点,这里其实就是将
div#app
节点进行移除:removeVnodes([oldVnode], 0, 0)
,此时,挂载节点就只剩下模板了,这个效果其实相当于模板节点替换了原生挂载节点div#app
,如下图所示:
到这里,我们从render
函数渲染得到的VNode
到其映射为真实 DOM 节点的整个过程都分析完毕,组件挂载 这个过程也就完成了。
总结
简单来说,组件挂载 就是将render
函数渲染出来的虚拟节点VNode
映射成对应真实节点并挂载到 DOM 文档中,其过程大致包含几个重要步骤:挂载mount
-> 渲染render
-> 得到虚拟节点VNode
-> 更新update
-> patch -> 真实 DOM。
组件挂载 的整个时序图如下所示:
参考
- 数据驱动