前言
这篇文章直接从挂载开始说起,不会从new Vue
开始,因为已经有很多文章讲解过;其实这篇文章也是这样,但是感觉还是记一下,也算为后面做铺垫了。
知识点
通过这篇文章可以了解如下内容
- Vue 的挂载过程
- Render Watcher 是什么
- Render Watcher 的作用
- 什么是虚拟 DOM
- 虚拟DOM 的作用
- Vue.extend 原理
挂载过程
整个Vue周期中,有3种方式会执行挂载过程
自动调用
当 options
有 el
属性时,在 this._init()
中会自动执行
Vue.prototype._init = function (options?: Object) {
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
手动调用
通过new Vue().$mount('#app')
挂载
创建组件实例时
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
// ...
} else {
// ...
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
insert (vnode: MountedComponentVNode) {},
destroy (vnode: MountedComponentVNode) {}
}
这块会在后面patch过程中说,现在就知道这里会调用子组件实例的$mount
方法去挂载子组件就行
上面这三种最终都是调用$mount
方法去挂载实例。
Vue.prototype.$mount
对于 runtime-with-compiler
版本
先看一下 src/platform/web/entry-runtime-with-compiler.js
文件中定义:
// 缓存 src/platform/web/runtime/index.js 中定义的 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取 DOM 节点
el = el && query(el)
// 如果 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 > template > el
if (!options.render) {
// 如果没有 render 属性,则获取 options.template
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 如果 template 的值是以 # 开头,说明 template 的属性值是一个 id
// 根据 template 获取 template.innerHTML
template = idToTemplate(template)
// 即没有定义 options.render,也没有 options.template 对应的 innerHTML 则报错
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// 如果 template 是一个 DOM 对象,则直接获取 innerHTML
template = template.innerHTML
} else {
// 如果 template 即不是字符串,又不是 DOM,则报错
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 如果 没有定义 render 也没有定义 template,则根据 el 去获取 el.outerHTML
template = getOuterHTML(el)
}
if (template) {
// 将 template 转为 render 函数(编译过程,后面会说)
const { render, staticRenderFns } = compileToFunctions(template, {}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 执行 src/platform/web/runtime/index.js 中定义的 $mount 方法
return mount.call(this, el, hydrating)
}
这段代码首先缓存了原型上的 $mount
方法,再重新定义该方法
重新定义的方法对 el
做了限制,Vue 不能挂载在 body
、html
这样的根节点上。 如果没有定义 render
方法,则会把 el
或者 template
字符串转换成 render
方法,并把 render
函数绑定到 options
上,然后执行src/platform/web/runtime/index.js
中定义的 $mount
方法
原先原型上的 $mount
方法在 src/platform/web/runtime/index.js
中定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only
版本的 Vue 直接使用
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取 el 的 DOM 对象
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
$mount
方法支持传入 2 个参数,第一个是 el
,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query
方法转换成 DOM 对象。第二个参数是和服务端渲染相关,在浏览器环境下不需要传第二个参数。
接下来调用 mountComponent
方法,并把当前 Vue 实例、el
、hydrating
传入
mountComponent
方法定义在 src/core/instance/lifecycle.js
文件中:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 在这里会设置一次 $el
vm.$el = el
if (!vm.$options.render) {
// ... 如果没有 render 函数就报错
}
// 执行 beforeMount,先父后子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
/* ... */
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 创建 Render Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher 这里将是 true */)
hydrating = false
// 只有根组件没有 $vnode
if (vm.$vnode == null) {
// 表示此组件已经挂载完成
vm._isMounted = true
// 根组件的 mounted 函数 先子后父
callHook(vm, 'mounted')
}
return vm
}
从上面的代码可以看到,mountComponent
核心就是先实例化一个Render Watcher
,实例化过程中会调用 updateComponent
方法,在此方法中调用 vm._render
方法生成VNode,最终调用 vm._update
更新 DOM。
Watcher
在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中被监测数据发生改变时也会执行回调函数。
函数最后,会将根实例的 vm._isMounted
设置为 true
, 表示这个实例已经挂载了,同时执行根实例的mounted
钩子函数。 这里注意 vm.$vnode
是组件占位符VNode,所以它为 Null
则表示当前是根 Vue 的实例。
挂载过程总结
对于 runtime-with-compiler
版本的挂载过程是,根据template
、el
获取render
函数,创建Render Watcher
,在创建Render Watcher
的过程中触发挂载过程;即调用render
函数获取渲染VNode,调用patch
函数创建节点并渲染到页面上。
对于runtime
版本,由于在打包构建的时候已经将模版转成了render
函数,所以省略了获取render
的步骤;会直接创建Render Watcher
,在创建Render Watcher
的过程中触发挂载过程;即调用render
函数获取渲染VNode,调用patch
函数创建节点并渲染到页面上。
在创建
Render Watcher
之前会调用组件的 beforeMount 钩子函数
Render Watcher 是什么
每一个组件都对应一个 Watcher 对象,包含如下内容
- 存储组件的 Vue 实例
- 触发视图更新的方法
- 存储当前组件使用到的响应式属性的 Dep 实例
Render Watcher 的作用
Vue使用发布订阅模式实现数据的双向绑定。其中 Render Watcher 就是里面的订阅者。当数据更新会通知Render Watcher更新视图。在Vue 源码(二)响应式原理中会详细说明
render 函数是怎么创建 VNode 的
render 函数的目的是创建并返回 VNode,总共有两个过程会执行render
函数:
- 我们知道每个组件都会创建一个Render Watcher。也就是说每个组件在创建Render Watcher的时候都会执行
updateComponent
方法 - 当组件更新时,会再次执行
updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
updateComponent
函数内部,调用_render
函数创建VNode。
_render
_render()
方法是实例的一个私有方法,它用来把实例渲染成一个VNode。它的定义在 src/core/instance/render.js
文件中:
Vue.prototype._render = function (): VNode {
const vm: Component = this
// 获取render函数和组件占位符VNode(如果有的话)
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
// 规范化插槽作用域和插槽
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// 将 组件的占位符 vnode 赋值给 vm.$vnode
vm.$vnode = _parentVnode
let vnode
try {
currentRenderingInstance = vm
// 执行组件的 render 函数
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
} finally {
currentRenderingInstance = null
}
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// 将 组件的占位符 vnode 赋值给 vnode.parent
vnode.parent = _parentVnode
return vnode
}
_render
方法的核心就是执行组件的render
函数,并返回 VNode。这个 VNode 包含当前组件中普通标签VNode,以及组件标签的占位符VNode;这两种VNode 的数据结构会在后面说一下
定义/编译后的render
函数接收createElement | h
方法,而这个方法就是vm.$createElement
方法
vnode = render.call(vm._renderProxy, vm.$createElement)
vm.$createElement
:
export function initRender (vm: Component) {
// 给被模板编译成的 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)
}
这两个函数接收的参数相同,就是render
函数中传给createElement | h
方法的参数。参数文档
唯一的区别就是最后一个参数,对于开发者手写的render
函数始终为true
,而通过编译生成的render
函数,已经设置好参数d
(子节点)了。createElement
最后一个参数表示对子节点的规范化方式,类型不同规范的方法也就不一样,后面会说
注意点
执行vm._render
时,有两个需要注意的点
vm.$vnode = _parentVnode
组件实例的$vnode
属性指向组件的占位符 VNodevnode.parent = _parentVnode
组件渲染VNode的parent
属性指向组件的占位符 VNode
继续向下,执行定义/编译的render
函数时,会执行createElement
方法
createElement
createElement
函数定义在 src/core/vdom/create-element.js
中
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
//如果没有传 data 参数,改变实参和形参顺序
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 如果传入的 alwaysNormalize 为 true,则将 normalizationType 赋值成数字 2
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
createElement
方法实际上是对 _createElement
方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement
:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData, // data 是标签上的属性或者 render 函数的第二个参数
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 不能将响应式对象当作 VNode 参数传入,报错并返回空 VNode 节点
if (isDef(data) && isDef((data).__ob__)) {
process.env.NODE_ENV !== 'production' && warn()
return createEmptyVNode()
}
// 动态组件
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
return createEmptyVNode()
}
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 在这里规范化 children
// ALWAYS_NORMALIZE = 2
// SIMPLE_NORMALIZE = 1
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)) {
// 创建 VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果 tag 是字符串,并且不是平台保留标签,则说明是一个组件标签
// resolveAsset 根据传入的 tag 去 options.components 属性中查找对应组件的导出内容
// 此时 Ctor 可能是一个对象(同步组件),也可能是一个函数(异步组件)
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
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()
}
}
这里我们先缕一下主要逻辑其他分逻辑我们下面会详情说明
首先_createElement
函数会规范化children
,主要根据normalizationType
的不同,调用不同的规范化函数;
接下来就是根据tag
做不同的处理:
- 如果
tag
是字符串:tag
是平台保留标签,创建渲染VNodetag
不是平台保留标签,并且vm.$options.components
中有名为tag
的属性,创建组件占位符VNode- 如果上述都不是,创建渲染VNode
- 如果
tag
不是字符串:说明传入的tag
是一个组件的对象,比如render: h => App
,创建组件占位符 VNode - 最后返回VNode
createComponent
组件占位符 VNode 通过 createComponent
方法创建
createComponent
定义在 src/core/vdom/create-component.js
中
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// src/core/global-api/index.js 中的 initGlobalAPI
// 指向 Vue 构造函数
const baseCtor = context.$options._base
// 如果 Ctor 是一个对象,则通过 Vue.extend 创建子组件构造函数,并赋值给 Ctor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') {
// ...如果 Ctor 不是函数则报错
return
}
// 异步组件相关,后面会说
let asyncFactory
if (isUndef(Ctor.cid)) {/* ... */}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// 处理组件 v-model
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// 提取传入的 props 值
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 拿到自定义事件
const listeners = data.on
// 将 nativeOn 赋值给 data.on
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 给组件占位符 VNode 添加 hook 钩子
installComponentHooks(data)
// 获取 组件名称
const name = Ctor.options.name || tag
// 创建组件占位符 VNode
// 注意这里将 { Ctor, propsData, listeners, tag, children } 赋值给了 VNode 的 componentOptions 属性
// 并且 children 为 undefined
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
createComponent
函数接收 5 个参数:
- Ctor:组件信息
- data:标签上的属性或者
render
函数的第二个参数 - context:父组件 Vue 实例
- children:组件子节点
- tag:组件名
createComponent
函数作用:
- 为当前组件创建构造函数 go
- 处理组件
v-model
- 提取传入的
props
值 go - 获取自定义事件,并将
nativeOn
赋值给data.on
- 给
data
添加 hook 钩子函数 go - 创建组件占位符VNode,并将
{ Ctor, propsData, listeners, tag, children }
赋值给了 VNode 的componentOptions
属性 - 返回组件占位符VNode
_render
函数的总流程大致就是上图的样子
小结
先看下组件占位符 VNode 和 渲染VNode 的数据结构
组件占位符 VNode 数据结构
假设组件标签如下
<HelloWorld id="js_hello" :flag="flag" msg="Welcome to Your Vue.js App"/>
{
// 组件占位符 VNode 特有的属性
componentInstance: undefined, // 子组件的 Vue 实例,子组件Vue实例创建完成后赋值
componentOptions: { // 组件占位符 VNode 特有的属性
propsData: { // 根据子组件定义的 props 属性,获取传入子组件的值
flag: 1,
msg: "Welcome to Your Vue.js App"
}
listeners: undefined, // 自定义事件
tag: 'HelloWorld', // 组件名
children: undefined, // 插槽内容
Ctor: ƒ // 构建子组件 Vue实例 的构造函数
},
context: {…}, // 父组件的 Vue实例,
data: { // 组件标签上的属性或者 render 函数的第二个参数
attrs: { // 组件标签上的属性,不包含子组件中定义的 props 属性
id: 'js_hello'
},
on: undefined, // 有 .native 修饰符的事件
hook: {…} // 钩子函数,在 patch 的不同时机触发
},
elm: DOM, // 组件根元素,在 patch 过程创建
tag: "vue-component-2-HelloWorld"
}
组件占位符VNode 里面存储子组件Vue实例的构造函数、以及存储传给子组件的数据。他的作用其实就是一个占位符。当子组件的DOM创建完成,会将 DOM 赋值给 组件占位符VNode 的elm
属性。而渲染时使用的就是这个elm
。
渲染VNode 数据结构
假设组件内容如下
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
{
children: [{ // 子元素
context: {},
data: { // 组件标签上的属性或者 render 函数的第二个参数
staticClass: 'hello',
},
elm: undefined, // 根的 DOM 元素
parent: undefined, // 组件的组件占位符VNode,只有组件 根VNode 才有
tag: 'h1',
children:[{
text: "Welcome to Your Vue.js App",
context: undefined,
data: undefined,
elm: undefined,
parent: undefined,
tag: undefined
}]
}],
context: {}, // 当前组件的Vue实例
data: { // 组件标签上的属性或者 render 函数的第二个参数
staticClass: 'hello',
},
elm: undefined, // 根的 DOM 元素
parent: {}, // 组件的组件占位符VNode,只有组件 根VNode 才有
tag: 'div'
}
组件占位符VNode 和渲染VNode 的区别
- 组件占位符VNode,是一个占位符;描述的是组件标签。存储传递给子组件的信息
componentOptions
- 渲染VNode,描述普通标签。存储标签信息。
DEMO
假设有这样的组件关系
new Vue({
render: h => h(App)
}).$mount('#app')
# App.vue
<div>
<HelloWorld id="js_hello" :flag="flag" msg="Welcome to Your Vue.js App"/>
</div>
# helloWorld.vue
<div class="hello">
<h1>{{ msg }}</h1>
</div>
VNode 创建流程如下:
首先创建根实例的Render Watcher,执行render
函数,创建App
组件的组件占位符VNode。根据VNode构建 DOM 树。构建 DOM 树过程中发现有组件占位符VNode(App
组件的),会根据组件占位符VNode的信息创建App
组件的Vue实例;并执行App
组件的挂载过程,也就是mount
方法;然后创建App
组件的Render Watcher,执行render
函数,创建App
组件的渲染VNode,其中就包含helloWorld
的组件占位符VNode。之后都是这个流程。在构建App
组件的 DOM 树时,根据helloWorld
的组件占位符VNode 创建helloWorld
组件的Vue实例,并执行挂载过程;创建helloWorld
组件的渲染VNode;然后构建helloWorld
组件的 DOM 树。并将构建的 DOM 树挂载到helloWorld
组件占位符VNode 的elm
属性上。
说一下支线
规范化 Children
Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement
接收的第 4 个参数 children
是任意类型的(可能是字符串、数组、嵌套数组等),因此需要把它们 规范成深度只有一层的 VNode 数组。
_createElement
方法中根据 normalizationType
的不同,调用了 normalizeChildren(children)
和 simpleNormalizeChildren(children)
方法
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
simpleNormalizeChildren
它们的定义都在 src/core/vdom/helpers/normalzie-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
}
simpleNormalizeChildren
把整个 children
数组打平,让它的深度只有一层;这种针对于某些通过编译生成的 render
函数,比如没有v-for
、作用域插槽
的render
函数
normalizeChildren
而 normalizeChildren
相对就比较复杂了
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children) // 判断 children 是不是基本数据类型
? [createTextVNode(children)] // 如果是将 children 修改成文本节点并返回
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
normalizeChildren
最终返回的是 规范化好的 VNode 数组,而且深度只有一层;
逻辑如下:
children
是基本数据类型,根据children
创建文本节点并返回;children
是数组,调用normalizeArrayChildren
方法- 即不是数组又不是基本数据类型,返回
undefined
normalizeArrayChildren
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
// 获取 res 数组中最后一个 VNode
last = res[lastIndex]
// 如果 c 是一个数组
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// 如果 c 的第一个元素和 last (res 的 最后一个元素)都是文本节点,就合并两个节点
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
// 删除 c 的第一个节点
c.shift()
}
// 将 c 中所有元素 push 到 res 中
res.push.apply(res, c)
}
} else if (isPrimitive(c)) { // 如果 c 是基本数据类型
if (isTextNode(last)) {
// 如果 res 最后一个元素是 文本节点,则将最后一个元素和 c 合并
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// last 不是文本节点
res.push(createTextVNode(c))
}
} else { // c 既不是数组也不是基本数据类型
if (isTextNode(c) && isTextNode(last)) {
// 合并
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// 如果是循环列表,列表还存在嵌套的情况,并且c 没有 key 属性,则根据 nestedIndex 去设置它的 key
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
normalizeArrayChildren
主要的逻辑就是遍历 children
,获得单个节点 c
,然后判断 c
的类型,如果是一个数组,则递归调用 normalizeArrayChildren
; 如果是基础类型,则通过 createTextVNode
方法转换成 VNode 类型;否则就已经是 VNode 类型了,如果 children
是一个列表并且列表还存在嵌套的情况,则根据 nestedIndex
去更新它的 key。这里需要注意一点,在遍历的过程中,对这 3 种情况都做了如下处理:如果存在两个连续的 text
节点,会把它们合并成一个 text
节点。
经过对 children
的规范化,children
变成了一个VNode数组。
创建组件构造函数 - Vue.extend
在创建组件占位符 VNode 时,会先通过Vue.extend
创建对应组件的构造函数,并赋值给 Ctor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
Vue.extend
函数的定义,在src/core/global-api/extend.js
中
export function initExtend (Vue: GlobalAPI) {
Vue.cid = 0
let cid = 1
Vue.extend = function (extendOptions: Object): Function {
// extendOptions: 组件导出的内容
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
// 获取缓存的组件列表
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
// 如果 cachedCtors 中有 key 是 SuperId 的属性值,直接返回
// 避免多次继承同一个构造函数
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
// 定义 组件构造函数
const Sub = function VueComponent (options) {
this._init(options)
}
// 原型继承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 设置组件构造函数的 cid
Sub.cid = cid++
// 合并 options 配置
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 初始化 props
if (Sub.options.props) {
initProps(Sub)
}
// 初始化 computed
if (Sub.options.computed) {
initComputed(Sub)
}
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// 注册 Vue.component,Vue.directive, Vue.filter 函数
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// 递归组件,是通过 name 去寻找
if (name) {
Sub.options.components[name] = Sub
}
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 将 Sub 缓存到 extendOptions._Ctor 中,key 是 Super.cid
cachedCtors[SuperId] = Sub
return Sub
}
}
Vue.extend 原理
Vue.extend
的作用就是构造一个Vue
的子类,它使用原型继承的方式创建了一个继承于Vue
的构造器 Sub
,并返回。然后对Sub
这个对象本身扩展了一些属性,如扩展options
、添加全局 API 等;并且对配置中的props
和computed
做了初始化工作;将其代理到构造函数的原型上,目的是,每次实例化组件实例时不需要再次代理,从而减少代码执行次数。最后对Sub
构造函数做了缓存,避免多次继承同一个构造函数。
initProps
这是一种优化手段
// state.js
// 设置代理,将 key 代理到 target 上
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 当前 js
function initProps (Comp) {
const props = Comp.options.props
for (const key in props) {
proxy(Comp.prototype, `_props`, key)
}
}
这个函数比较简单,就是通过Object.defineProperty
将 _props
代理到 Sub
的原型上。在这里做的目的是,每次实例化当前组件时不需要再对 _props
做代理,因为原型中已经做过代理了
// 创建构造函数期间将 props 挂载到了原型上并添加了代理,所以通过 in 方法会访问到相应的 key
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
initComputed
作用和initProps
相同,将options
中的计算属性代理到Sub
的原型上,并设置 存描述符 getter
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
// 将组件的计算属性挂载到 组件构造函数的原型上
defineComputed(Comp.prototype, key, computed[key])
}
}
提取传入的 props 值
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
extractPropsFromVNodeData
根据子组件的options.props
中的属性,获取父组件传递给子组件的prop
数据
export function extractPropsFromVNodeData (
data: VNodeData,
Ctor: Class<Component>,
tag?: string
): ?Object {
// 获取组件中 props 配置
const propOptions = Ctor.options.props
if (isUndef(propOptions)) {
return
}
const res = {}
// 获取 attrs 和 props
const { attrs, props } = data
if (isDef(attrs) || isDef(props)) {
for (const key in propOptions) {
// 如果 key 是驼峰式,将其转为 连线符的形式
const altKey = hyphenate(key)
if (process.env.NODE_ENV !== 'production') {
// 将 key 转为小写 aBc -> abc、a-b -> a-b
const keyInLowerCase = key.toLowerCase()
// key !== keyInLowerCase 说明 key 是驼峰式
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
// key 是驼峰式,但传入的属性即不是驼峰式,也不是连线符的形式,则报错
tip(/* ... */)
}
}
// 从 props 和 attrs 中根据 key 查找传入子组件的属性,并赋值给 res
// props 优先级高于 attrs,如果 props 中存在对应的 key,则不会再去 attrs 中查找
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false)
}
}
return res
}
function checkProp (
res: Object, // 结果对象
hash: ?Object, // props、attrs
key: string,
altKey: string, // 连线符形式的 key
preserve: boolean // 为 true, 删除 hash 中属性名为 key 的属性
): boolean {
if (isDef(hash)) {
if (hasOwn(hash, key)) {
res[key] = hash[key]
if (!preserve) {
delete hash[key]
}
return true
} else if (hasOwn(hash, altKey)) { // 查找连线符形式的 key
res[key] = hash[altKey]
if (!preserve) {
delete hash[altKey]
}
return true
}
}
// 没找到的话返回 false
return false
}
extractPropsFromVNodeData
根据组件定义的 props
属性,从 data
中的 props
、attrs
查找属性值为key
的属性,添加到 res
中并返回。
优先从 data.props
中查找
- 如果没找到
checkProp
返回false
,会执行checkProp(res, attrs, key, altKey, false)
,再从data.attrs
中查找; - 如果
data.props
中找到了,返回true
,查找结束 data.props
找到的话,不会删除data.props
中的属性,而data.attrs
会删除
在createComponent
内,extractPropsFromVNodeData
的返回值最终会放到 组件占位符 VNode 的componentOptions
中
添加组件钩子函数
installComponentHooks(data)
在初始化组件占位符 VNode 的过程中会将以下这几个钩子函数添加到 data.hook
中,并在 patch
过程的对应时机,执行对应钩子函数
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {},
insert (vnode: MountedComponentVNode) {},
destroy (vnode: MountedComponentVNode) {}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
installComponentHooks
整个 installComponentHooks
的过程就是把 componentVNodeHooks
的钩子函数根据合并策略合并到 data.hook
中
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
// 遍历 componentVNodeHooks 中的属性名
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
// 已有的 hook 不等于 componentVNodeHooks[key],并且已有的 hook 没有 _merged 属性
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
f1(a, b)
f2(a, b)
}
// 添加一个标识,防止重复添加 componentVNodeHooks 中的 hook 钩子
merged._merged = true
return merged
}
总结
什么是虚拟 DOM
用 js 对象模拟真实 DOM 树
虚拟 DOM 的作用
在react,vue等技术出现之前,要改变页面展示的内容只能通过遍历查询 DOM 树的方式找到需要修改的 DOM 然后修改样式行为或者结构,来达到更新 UI 的目的。
这种方式相当消耗计算资源,因为每次查询 DOM 几乎都需要遍历整颗 DOM 树,如果建立一个与 DOM 树对应的虚拟 DOM 对象( js 对象),以对象嵌套的方式来表示 DOM 树及其层级结构,那么每次 DOM 的更改就变成了对 js 对象的属性的增删改查,这样一来查找 js 对象的属性变化要比查询 DOM 树的性能开销小。
Vue 的挂载过程
-
对于
runtime-with-compiler
版本的挂载过程是,根据template
、el
获取render
函数,创建Render Watcher
,在创建Render Watcher
的过程中触发挂载过程;即调用render
函数获取渲染VNode,调用patch
函数创建节点并渲染到页面上。 -
对于
runtime
版本,由于在打包构建的时候已经将模版转成了render
函数,所以省略了获取render
的步骤;会直接创建Render Watcher
,在创建Render Watcher
的过程中触发挂载过程;即调用render
函数获取渲染VNode,调用patch
函数创建节点并渲染到页面上。
Render Watcher 是什么
每一个组件都对应一个 Watcher 对象,包含如下内容
- 存储组件的 Vue 实例
- 触发视图更新的方法
- 存储当前组件使用到的响应式属性的 Dep 实例
Render Watcher 作用
Vue使用发布订阅模式实现数据的双向绑定。其中 Render Watcher 就是里面的订阅者。当数据更新会通知Render Watcher更新视图。在Vue 源码(二)响应式原理中会详细说明
Vue.extend 原理
Vue.extend
的作用就是构造一个Vue
的子类,它使用原型继承的方式创建了一个继承于Vue
的构造器 Sub
,并返回。然后对Sub
这个对象本身扩展了一些属性,如扩展options
、添加全局 API 等;并且对配置中的props
和computed
做了初始化工作;将其代理到构造函数的原型上,目的是,每次实例化组件实例时不需要再次代理,从而减少代码执行次数。最后对Sub
构造函数做了缓存,避免多次继承同一个构造函数。
最后
接下来会说data
、props
、computed
、watch
的响应式原理。在创建VNode整个过程中,很多点都是和后面说的API有关联,在说到对应API时,会再过一遍。