重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
之前一个简单的 vue 是如何渲染到页面上的,从本篇开始,开始过下 vue 内部的组件化逻辑,组件化也是 vue 的一大特性,组件化就是把页面拆分成多个组件,每个组件依赖的资源都放在一起,组件之间是互相独立的,可复用可嵌套。本篇以 vue-cli 为例,分析一下组件初始化的过程:
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 这里的 h 是 createElement 方法
render: h => h(App)
})
它也是通过 render 函数去渲染的,和之前不一样的是,他传入的不是一个标签,而是一个 App 组件。
createElement
之前说 createElement 会调用一个 _createElement 方法,中间有一段:
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 (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)
}
如果 tag 是一个字符串(也就是一个html标签),就返回一个实例化的VNode节点,否则就通过 createComponent 方法创建一个组件VNode。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
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
来看下这个方法都做了什么事情,它有几个参数,第一个可以传入组件、方法、对象和空,第二个是一个节点数据,第三个是当前上下文实例,第四个是一个子的数组vnode,最后是一个标签。
上面的代码简化一下,其实只有三步:构造子类构造函数,安装组件钩子函数**,实例化vnode**。
构造子类构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
一般情况下编写组件都会 import 一个组件,然后用 components 去声明它,然后页面上才可以使用:
import HelloWorld from './components/HelloWorld'
export default {
name: 'app',
components: {
HelloWorld
}
}
export 的是一个对象,所以 createComponent 里的代码逻辑会执行到 baseCtor.extend(Ctor),这里 baseCtor 实际上就是 Vue,它来自哪里呢?从 ~/vue/src/core/global-api/index.js 里可以看到一个 Vue.options._base = Vue,还记得vue在初始化的时候有一个合并操作么:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
它就是把上面的 Vue.options._base 和 options 合并到了一起,放在了 vm.$options 上,所以 context.$options._base 拿到的就是 Vue,也就是说这个 baseCtor 其实就是一个 Vue。后面如果 Ctor 是一个对象,就执行 baseCtor.extend(Ctor),也就是 Vue.extend(),把 Ctor 转化为一个新的构造器。接着看下 Vue.extend 的定义:
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
可以看到,传入一个对象可以返回一个函数(Vue的子类)。这里注意一下 const Super = this,这个 this 不是指的 vm,而是当前的 Vue。 后面把传进来的 name 做了一层校验 validateComponentName,判断这个组件名称是否合法,校验规则在 src/core/util/options.js:
export function validateComponentName (name: string) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
)
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
接着后面定义了一个子构造函数 Sub,调用了 _init,和Vue的 _init 很像,接着做了一层原型继承,也就是将这个 _init 变成 Vue.protype._init,后面把自身的 options 和 Vue的 options 合并,然后对自身的 props 和 computed 也做了一层初始化,后面把全局的一些静态方法都赋值给 Sub,目的就是让 Sub 拥有和Vue一样的能力。
用 Vue.extend 的好处是,如果很多地方用同一个组价的时候,其实调用 Vue.extend 方法传入的对象是一样的,就只会执行一次:
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
因为每次进来其实已经定义过了 _Ctor 了,并且已经有值了,并且 SuperId 是有的,所以直接返回这个构造器(如果SuperId一致,说明它们的父构造器是同一个)。
安装组件钩子
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
接着看,如果这个构造器不是一个方法,就报一个错,说组件定义不合法,后面是一个对异步组件的操作,跳过,后面:
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
对 options 重新计算,因为可能被全局mixin影响,跳过,后面对 v-model 、 propsData 、options 和 自定义事件 的处理跳过,到 installComponentHooks,安装组件钩子开始。之前说到 patch 的过程中会暴露一堆钩子函数, 这里就是安装这些钩子:
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
}
}
}
它遍历了 hooksToMerge,来看下是个啥:
// inline hooks to be invoked on component VNodes during patch
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) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
也就是遍历了 init,prepatch,insert,destroy。也就是说组件默认会有这四个钩子,然后遍历钩子,把钩子合并到 data.hook 上,来看下具体的合并过程:
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}
很简单就是依次执行就好了。
生成vnode
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
最后就是生成一个 vnode,这个和之前的不太一样,它的名字会打一个标识 vue-component-,后面就是传入了一些参数,回顾一下 vnode 是如何定义参数的:
//...
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
//...
}
children 为空,也就是 组件vnode的children是个空,然后 text 是空,elm 也是空,componentOptions 是 { Ctor, propsData, listeners, tag, children },最后返回这个vnode。
至此组件创建完毕。
捋一遍:子组件的构造器生成 baseCtor,它是继承于 Vue 的,返回一个组件构造器,其次组件vnode的data有一堆 hook,它会合并到组件的 data.hook 上,最后就是生成一个组件vnode,它没有 children,多了一个 componentOptions。