Vue源码学习笔记之Vue构造函数及其实例化
学习资源:vue@2.5.17
引言: 早听说,Vue 的源码极其优雅和精辟,再者自己使用 Vue 框架有段时间了,我想只停留在使用层面会大大局限我对它的认识,所以决定拜读源码尝试理解其中的原理。
入口文件
首先从 package.json 入手,打开 package.json 找到:
"main": "dist/vue.runtime.common.js"
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs",
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
...
}
从 scripts 字段可以看到,项目使用 rollup 工具进行打包,我们需要找到找到编译打包成 dist/vue.runtime.common.js 的入口文件,从 rollup -w -c scripts/config.js 可以知道 scripts/config.js 应该是输出最终的配置,打开’scripts/config.js’:
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.js'),
format: 'cjs',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.js'),
format: 'cjs',
alias: { he: './entity-decoder' },
banner
},
...
}
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry,
external: opts.external,
plugins: [
replace({
__WEEX__: !!opts.weex,
__WEEX_VERSION__: weexVersion,
__VERSION__: version
}),
flow(),
buble(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
}
}
if (opts.env) {
config.plugins.push(replace({
'process.env.NODE_ENV': JSON.stringify(opts.env)
}))
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
return config
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
可以看出 ‘dist/vue.runtime.common.js’ 的入口文件是 ‘web/entry-runtime.js’。
Vue构造函数的创建
1⃣打开 ‘web/entry-runtime.js’:
import Vue from './runtime/index'
export default Vue
引入 ‘./runtime/index’ 的 Vue 并导出,Vue 就是最终暴露给用户的 Vue 构造函数。
2⃣打开 ‘./runtime/index’:
/* @flow */
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser, isChrome } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// 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
1、引入 ‘core/index’ 的 Vue
2、给 Vue 的 config 属性绑定了 mustUseProp、isReservedTag、isReservedAttr、getTagNamespace、isUnknownElement 属性
3、合并平台指令和组件
4、给 Vue.prototype 绑定 __patch__ 方法
5、给 Vue.prototype 绑定 $mounted 方法
6、导出 Vue
3⃣打开 ‘core/index’:
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)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
1、引入 ‘./instance/index’ 的 Vue
2、初始化全局 API,展开 initGlobalAPI 函数:
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
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)
}
1、给 Vue 绑定 config 属性
2、给 Vue 绑定 util 属性
3、给 Vue 绑定 set 方法
4、给 Vue 绑定 delete 方法
5、给 Vue 绑定 nextTick 方法
6、给 Vue 绑定 options 属性并将其初始化为空对象
7、给 Vue 的 options 属性绑定 component、directive、filter 属性并将其初始化为空对象
8、给 Vue 的 options 属性绑定 _base 属性并赋值为 Vue
9、将 keep-alive 虚拟组件合并到 Vue.options.components
10、给 Vue 绑定 use 方法
11、给 Vue 绑定 mixin 方法
12、给 Vue 绑定 extend 方法
13、给 Vue 绑定 component、directive、filter 方法
3、给 Vue.prototype 绑定 $isServe 属性
4、给 Vue.prototype 绑定 $ssrContext 属性
5、给 Vue 绑定 FunctionalRenderContext 属性
6、给 Vue 绑定 version 属性
7、导出 Vue
4⃣打开 ‘./instance/index’:
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 构造函数,通过五个 *mixin(Vue),
initMixin(Vue)
给 Vue.prototype 绑定 _init 方法
stateMixin(Vue)
给 Vue.prototype 绑定 $data 属性并赋值为 this._data
给 Vue.prototype 绑定 $props 属性并赋值为 this._props
给 Vue.prototype 绑定 $set 方法
给 Vue.prototype 绑定 $delete 方法
给 Vue.prototype 绑定 $watch 方法
eventsMixin(Vue)
给 Vue.prototype 绑定 $on 方法
给 Vue.prototype 绑定 $once 方法
给 Vue.prototype 绑定 $off 方法
给 Vue.prototype 绑定 $emit 方法
lifecycleMixin(Vue)
给 Vue.prototype 绑定 _update 方法
给 Vue.prototype 绑定 $forceUpdate 方法
给 Vue.prototype 绑定 $destroy 方法
给 Vue.prototype 绑定 $emit 方法
renderMixin(Vue)
执行 installRenderHelpers(Vue.prototype)
给 Vue.prototype 绑定 $nextTick 方法
给 Vue.prototype 绑定 _render 方法
至此,我们知道以上过程创建了 Vue 构造函数对其加以设计,且为其绑定了一些全局方法和属性。
实例化
现在结合实际使用案例从正面看看实例化一个vm到底发生了什么。
以 vue-cli 初始化的项目的 main.js 为例:
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})
当 new Vue() 时会执行 this._init(options) 并生成一个vm实例。options 是我们给 Vue 传的参数,即:
{
el: '#app',
components: { App },
template: '<App/>'
}
_init() 是在上面提到的 ‘./instance/index’ 中绑定在 Vue.prototype 上的方法,当调用 this._init() 时会执行该函数,我们来看一下做了什么,
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
// merge options
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
}
// expose real self
vm._self = vm
initLifecycle(vm) // 在vm上挂载了一些生命周期及组件层级的属性
initEvents(vm) // 在vm上挂载了一些事件相关的属性并作更新事件列表操作
initRender(vm) // 在vm上挂载了一些node相关的属性和方法
callHook(vm, 'beforeCreate') // 执行beforeCreate绑定的函数
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 如果el存在,挂载;如果不存在,则需要手动挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
_init 方法在 vm 上挂载了一些属性和方法,这里主要说一下 vm.$options,vm.$options 被赋值为 mergeOptions() 的返回值,先看看传入的参数都是什么,
mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
**argument1: ** resolveConstructorOptions(vm.constructor),vm.constructor 其实就是 Vue 构造函数。
接着看 resolveConstructorOptions 函数
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
...
}
return options
}
这里简化了代码,Ctor.super 此时为 undefined, 所以 resolveConstructorOptions(Vue) 返回值为 Vue.options,还记得 Vue.options 是什么样吗?
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: vm
}
**argument2: ** options 是实例化 vm 时传入构造函数Vue的参数,
{
el: '#app',
components: { App },
template: '<App/>'
}
argument3: vm 是当前实例
所以
mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
实际上是
mergeOptions(
{
components: {KeepAlive},
directives: {},
filters: {},
_base: vm
},
{
el: '#app',
components: { App },
template: '<App/>'
},
vm
)
下面再来看 mergeOptions 函数到底做了什么
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
// 如果child也是Vue构造函数
if (typeof child === 'function') {
child = child.options
}
// 格式化child.props
normalizeProps(child, vm)
// 格式化child.inject
normalizeInject(child, vm)
// 格式化child.directives
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
这里需要注意的是不同的选项字段用的合并策略是不同的,
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
从上面几行代码可以看出取决于 strats[key],在 options.js 中可以找到
strats.el = strats.propsData = function (parent, child, vm, key) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
即 strats.el = strats.propsData = defaultStrat(parent, child)
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
strats.data = mergeDataOrFn(parentVal, childVal, vm)
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
生命周期选项使用的是 mergeHook 合并函数
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
components、directives、filters 使用的是 mergeAssets 合并函数
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
strats.provide = mergeDataOrFn
其他选项字段的合并函数是
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
总结
该篇文章仅记录了 Vue 构造函数设计和实例化的过程,从结构层面上对 Vue 有一个大概的了解,至于具体的方法是如何实现会后续会进行研究。