Vue入口
首先,先来分析当执行如下代码的时候,内部vue是如何初始化的?
import Vue from 'vue'
在 node_modules
目录下找到vue的安装目录,查看 package.json
文件,scripts标签如下:
{
"name": "vue",
"version": "2.6.14",
...
"scripts": {
...
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
...
},
...
}
查看 build 相关的命令,可以看出构建命令实际是执行的 scripts/build.js
文件,其他只是添加了不同的参数。
在 build.js 文件中,只是对构建过程和参数做了一些基本的过滤和判断,实际读取了 config.js
下的配置:
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const rollup = require("rollup");
const terser = require("terser");
if (!fs.existsSync("dist")) {
fs.mkdirSync("dist");
}
// 此处读取了所有配置内容
let builds = require("./config").getAllBuilds();
// filter builds via command line arg
if (process.argv[2]) {
const filters = process.argv[2].split(",");
builds = builds.filter((b) => {
return filters.some(
(f) => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1
);
});
} else {
// filter out weex builds by default
builds = builds.filter((b) => {
return b.output.file.indexOf("weex") === -1;
});
}
build(builds);
在 config.js
文件中:
const builds = {
...
// Runtime+compiler CommonJS build (CommonJS)
"web-full-cjs-dev": {
entry: resolve("web/entry-runtime-with-compiler.js"),
dest: resolve("dist/vue.common.dev.js"),
format: "cjs",
env: "development",
alias: { he: "./entity-decoder" },
banner,
},
"web-full-cjs-prod": {
entry: resolve("web/entry-runtime-with-compiler.js"),
dest: resolve("dist/vue.common.prod.js"),
format: "cjs",
env: "production",
alias: { he: "./entity-decoder" },
banner,
},
...
};
config.js 文件中配置的是针对不同环境和场景的 Vue.js 构建的配置,遵循了 Rollup 构建规则,entry
是构建入口。运行时+编译时
的配置项web/entry-runtime-with-compiler.js
并不是真正的文件路径,查看 resolve 函数是如何寻找真实地址的:
const aliases = require("./alias");
const resolve = (p) => {
const base = p.split("/")[0]; // 按 / 分割传入的字符串,首个为 base
// aliases 为 base 配置的别名,通过别名配置加载真实的文件地址
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1));
} else {
return path.resolve(__dirname, "../", p);
}
};
alias.js
如下:
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'), // web端别名配置
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
所以,web 端的真实源码位置在 src/platforms/web/
下,即 web-full-cjs-dev
配置对应的入口文件就是 src/platforms/web/entry-runtime-with-compiler.js
,因此当执行 import Vue from 'vue'
时,就是从此文件开始的:
// src/platforms/web/entry-runtime-with-compiler.js
/* @flow */
import Vue from "./runtime/index";
import { query } from "./util/index";
import { compileToFunctions } from "./compiler/index";
...
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component { ... };
...
Vue.compile = compileToFunctions;
export default Vue;
可见,此文件并不是 Vue 定义的地方,只是对 Vue 对象的扩展,继续向下寻找,./runtime/index.js
:
// src/platforms/web/runtime/index.js
/* @flow */
import Vue from "core/index";
import config from "core/config";
...
// install platform specific utils
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop;
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
...
export default Vue;
此文件依旧是对 Vue 对象的扩展,继续向下寻找,core/index.js
:
// src/core/index.js
import Vue from "./instance/index";
import { initGlobalAPI } from "./global-api/index";
import { isServerRendering } from "core/util/env";
import { FunctionalRenderContext } from "core/vdom/create-functional-component";
initGlobalAPI(Vue); // 初始化全局API
...
Vue.version = "__VERSION__";
export default Vue;
继续查看 ./instance/index.js
:
// src/core/instance/index.js
import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
export default Vue;
至此,找到了 Vue 定义的地方,是一个函数,并且发现,Vue 相关的像生命周期、初始化、状态、事件、渲染等功能都分散在不同的模块中,通过 xxxMixin
函数进行注册和初始化,这样做很好的分离了代码结构,从功能上看更加清晰和直观,方便管理和维护,这也是使用 Function
而非 Class
作为构造函数的原因。
另外,在 src/core/index.js
文件中,initGlobalAPI(Vue)
的执行,是对 Vue 全局API的扩展:
// src/core/global-api/index.js
/* @flow */
...
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
此文件中注册的全局API,看上去就比较熟悉了,在官网的文档中都可以找到。至此,Vue 初始化过程就基本梳理完了。
接下来,我们看看 new Vue()
的过程发生了什么?
new Vue()
通过上面的分析,我们知道 Vue 本质是一个函数,但我们使用 Vue 的时候,只能通过 new Vue()
来使用,在 javascript 中类确实可以用函数来实现,但函数却不只用来声明类,通过上面的分析应该明白了其中的关键,如果还不清楚,请再看一遍上面的内容,哈哈哈。
new Vue()
后,执行 Vue 函数,内部执行了 this._init()
方法:
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
initMixin(Vue); // 该函数为Vue的注入了_init()
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
initMixin(Vue)
在Vue的原型上注入了_init()
方法:
// src/core/instance/init.js
/* @flow */
...
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this;
// a uid
vm._uid = uid++;
let startTag, endTag;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`;
endTag = `vue-perf-end:${vm._uid}`;
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// 合并配置
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
// 一系列初始化
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(`vue ${vm._name} init`, startTag, endTag);
}
// 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
可以看出,_init()
初始化过程主要包括合并配置和一系列初始化,包括生命周期、事件、渲染、data、props等,并且执行了 beforeCreate
和 created
两个生命周期钩子。最后通过判断是否存在 el
属性,调用$mount
方法挂载 vm
,挂载的过程实际就是把模版渲染成真实DOM的过程。
通过一系列初始化可以发现,在 beforeCreate
生命周期钩子内是无法访问到data、props、methods
等配置的,因为还没有初始化,在 created
钩子中可以。
接下来看看 vm
的挂载流程都做了什么?
在构建的分析过程中,构建入口文件 src/platforms/web/entry-runtime-with-compiler.js
中,有对 mount
方法的定义:
// src/platforms/web/entry-runtime-with-compiler.js
/* @flow */
...
// 先拿出原型链上的$mount方法,再自定义$mount方法
// 最后 return mount.call(this, el, hydrating); 再执行原$mount
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el);
// 不可以挂载在body和html元素上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== "production" &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
);
return this;
}
const options = this.$options;
// 这里的render是 new Vue({ render:(){} }) 时可用户自定义的
// 如果没有定义render函数,会判断有没有template,最后通过 compileToFunctions 方法把template编译成render函数
if (!options.render) {
let template = options.template;
if (template) {
if (typeof template === "string") {
if (template.charAt(0) === "#") {
template = idToTemplate(template);
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== "production") {
warn("invalid template option:" + template, this);
}
return this;
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
mark("compile");
}
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV !== "production",
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
mark("compile end");
measure(`vue ${this._name} compile`, "compile", "compile end");
}
}
}
return mount.call(this, el, hydrating);
};
...
export default Vue;
从代码中可以看出,mount方法并不是此处首次定义,在源码中也发现有多个位置定义了此方法,其中原因主要是挂载流程是跟平台和构建流程相关的,不同的平台下,有不同的挂载方法。这样不同的构建流程中,对挂载有不同的处理,处理完异同后,再合流到统一的挂载中去。所以,在当前代码中的构建入口,先缓冲原型链上的mount方法,在定义自己的mount方法,执行完自定义的mount方法后,再通过 call
方法执行缓存的mount方法。其中有几个关键点:
- Vue 不能挂载在 body、html 节点上
- 如果用户没有自定义 render 方法,判断有没有 template ,如果没有,再判断有没有 el ,最后都会拿到一个 template ,然后通过
compileToFunctions
方法把 template 编译成 render 。在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件.vue
方式开发组件,还是写了el 或者 template属性
,最终都会转换成 render 方法;
执行完自定义的mount方法, 会调用原型链上定义的原始mount方法,此方法是在 src/platform/web/runtime/index.js
中定义的:
// src/platform/web/runtime/index.js
// 公共的mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
然后去调用 mountComponent
方法:
// src/core/instance/lifecycle.js
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el;
// 执行 beforeMount 钩子
callHook(vm, "beforeMount");
let updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
// updateComponent 声明
updateComponent = () => {
...
const vnode = vm._render(); // 关键:执行_render()生成vnode
vm._update(vnode, hydrating); // 关键:根据vnode执行_update生成真实dom
...
};
} else {
// updateComponent 声明
updateComponent = () => {
vm._update(vm._render(), hydrating); // 关键:同上
};
}
// 创建渲染Watcher实例
new Watcher(
vm,
updateComponent,
noop,
{
before() {
// 根据_isMounted的不同执行beforeUpdate钩子
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
hydrating = false;
// 根据vnode的状态,执行mounted钩子
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted");
}
return vm;
}
这就是挂载的流程,核心是先实例化一个渲染Watcher,然后Watcher的回调函数中会执行updateComponent方法,这个方法里执行的比较关键,是调用_render
方法生成vnode,然后把vnode传入_update
方法更新DOM。渲染Watcher顾名思义会监测数据的变化,然后执行回调函数,然后执行不同的生命周期钩子。在此阶段的挂载过程中,会有三个生命周期钩子 beforeMount、mounted、beforeUpdate
在不同时机执行。
接下来继续看 _render
(生成VNode)和 _update
(更新DOM)。
_render
方法是Vue实例的一个私有方法,目的是生成虚拟dom,定义在 src/core/instance/render.js
文件中,其中最关键的是执行mount阶段生成的 render
方法:
Vue.prototype._render = function (): VNode {
...
let vnode
try {
// 关键:调用mount阶段生成的render的函数,此render函数可用户自定义,或者通过template编译生成
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
...
}
// 如果不是有效的vnode,则生成一个空vnode兜底
if (!(vnode instanceof VNode)) {
...
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
从这里可以看出,我们的业务代码中:
new Vue({
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
})
render函数中传入的 createElement
参数就是 vm.$createElement
方法,此方法定义如下:
// 这个是通过template模版编译而成的render函数使用的
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 这个是用户自定义的render函数使用的
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
最终又调到了createElement
方法:
// src/core/vdom/create-element.js
export 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
}
// 对参数做一些规范处理后,实际调用_createElement()
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 规范化children
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)
// 内置元素,直接创建vnode
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
// 创建组件vnode
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)
}
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()
}
}
此方法的核心流程包括两个:
- children的规范化:由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。children的规范化是指要把
createElement
函数传入的第四个参数children
(任意类型)规范为VNode类型。比如:render函数是用户自己手写的,当子节点children只有一个节点的时候,Vue允许使用基本类型创建单个文本节点,这种情况会调用createTextVNode
创建一个文本节点的 VNode。 - VNode的构建:虚拟Node的构建会根据 tag 的不同做判断:
- 如果 tag 是 string 类型,则接着判断,如果是内置的一些节点,则直接创建一个普通 VNode;
- 如果 tag 是为已注册的组件名,则通过
createComponent
创建一个组件类型的 VNode; - 如果 tag 是一个 Component 类型,则直接调用
createComponent
创建一个组件类型的 VNode 节点。
对于 createComponent
创建组件类型的 VNode 的过程,本质上它还是返回了一个 VNode。
因此,createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree。
VNode创建完成后,接下来就是把虚拟dom转换成真实dom,此过程是在 _update
方法中完成的:
// src/core/instance/lifecycle.js
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;
// 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);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
可以发现 _update
的核心就是调用 vm.__patch__
方法,这个方法实际上在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js
中:
// src/platforms/web/runtime/index.js
...
Vue.prototype.__patch__ = inBrowser ? patch : noop
...
其中,patch
方法是 createPatchFunction
方法的返回值,这里传入了一个对象backend,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现。
// src/core/vdom/patch.js
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) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
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) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
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)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.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 方法接收 4个参数:
- oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;
- vnode 表示执行 _render 后返回的 VNode 的节点;
- hydrating 表示是否是服务端渲染;
- removeOnly 是给 transition-group 用的;
patch
方法的目的是生成真实dom,是通过 createElm
方法实现的,并且完成的父子节点的连接,在createElm
内部又会调用createChildren
生成子元素,然后通过insert
方法插入父节点。这样通过递归循环调用,完成dom树的构建。
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)) {
...
// nodeOps封装的dom操作方法生成真实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)
}
}
至此,new Vue()
完成了真实dom的创建。