初学vue,你得知道我们是从new Vue
开始的:
new Vue({
el: '#app',
data: obj,
...
})
那你觉得是不是很有意思,咱们new Vue
之后,就可以使用他那么多的功能,可见Vue是暴出来的一个一个功能类函数,我们进入源码一探究竟:
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'
/** * 添加全局的API */
initGlobalAPI(Vue)
/** * 服务端渲染需要 */
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
/** * 服务端渲染需要 */
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
/** * 服务端渲染需要 */
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
/** * vue版本号 这里的'__VERSION__'为占位符,发布版本时将被自动替换 */
Vue.version = '__VERSION__'
export default Vue
那么我们看到咱们的核心Vue
来自'./instance/index'
那我们再去一探究竟,可想而知里面必定有一个Vue函数类
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'
// Vue构造函数必须使用new关键字实例化, 否则会抛出一个警告, 实例化Vue的时候会调用_init方法初始化
// 这里options也是.vue文件中暴露出的对象
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
可以看到里面有一个function Vue
功能类,而且里面加载了initMixin
,stateMixin
等,这几个方法分别传入了Vue来初始化一些功能。
另外我们可以在入口文件出看到initGlobalAPI
这个方法,那么我们打开initGlobalAPI
所在的位置:./global-api/index
......
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.
// 注册全局工具API, 只对Vue生效。
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
//定义全局属性
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 初始化options
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.
// 用_base属性来挂载Vue构造器
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
//定义全局方法
initUse(Vue) // Vue.use
initMixin(Vue) // Vue.mixin
initExtend(Vue) // Vue.extend
initAssetRegisters(Vue)
}
可见暴露出多个方法给全局,而Vue.util是一些工具方法:
import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index'
那么我们回到Vue功能函数类上:
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'
// Vue构造函数必须使用new关键字实例化, 否则会抛出一个警告, 实例化Vue的时候会调用_init方法初始化
// 这里options也是.vue文件中暴露出的对象
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
我们可以看到initMixin(Vue)
执行了,那么我们去读一下init的源码:
import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'
let uid = 0
// 向Vue.prototype添加_init方法, 用于new Vue时 初始化一些配置项
// 主要的功能是合并配置项, 初始化生命周期, 事件, 组件, 指令等等
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
// 使用uid来区分不同的vue实例, 以便后面的缓存功能使用
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)
}
// 如果是vue实例,则不需要被observe
vm._isVue = true
// 1、处理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
)
}
//2、renderProxy
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
// 依次初始化配置项, 并调用生命周期钩子
vm._self = vm
initLifecycle(vm)
// 事件监听初始化
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
//初始化vm状态 prop/data/computed/watch完成初始化
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)
}
// 配置项里有el属性, 则会挂载到真实DOM上, 完成视图的渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
参考 前端进阶面试题详细解答
阅读init.js源码之后,我们可以看到其实就是这样一个顺序:
- option参数
- renderProxy代理
- vm的生命周期相关变量初始化
- vm的事件监听
- 初始化vm的状态
- render & $mount
在Vue的原型上挂了一个_init方法,也就是说,我们执行new Vue(options)
的时候就会执行这个方法,并且我们传过去的options可以通过vm.$options访问到,那么mergeOptions()
内部原理又是啥呢,而且我们看到他接收到两个部分resolveConstructorOptions()
,options
并且融合在一起,那么我们先研究resolveConstructorOptions()
:
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
那么问题又来了,let options = Ctor.options
中的Ctor
又是什么东西呢?我们可以看到下面extend(Ctor.extendOptions, modifiedOptions)
那我们去找extend
:
import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'
export function initExtend (Vue: GlobalAPI) {
/** * Each instance constructor, including Vue, has a unique * cid. This enables us to create wrapped "child * constructors" for prototypal inheritance and cache them. */
// 为每个构造函数分配一个唯一的cid, 以便用于缓存
Vue.cid = 0
let cid = 1
/** * Class inheritance */
// 生成Vue构造器的子类
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// 如果cid已存在, 则应用缓存, 直接返回缓存的构造器
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)
}
// 创建一个包含_init方法的子类, 并赋予唯一的cid
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.
// 将props和computed都代理到vm实例上
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
// 添加extend,mixin,use这些API
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.extend
,可以看出来这不就是实现了一个继承嘛。Sub
继承自super
,然后return
出去。
所以resolveConstructorOptions
就做两件事
Ctor.super
来判断该类是否是Vue的子类if (superOptions !== cachedSuperOptions)
来判断父类中的options
有没有发生变化
那么我们知道了resolveConstructorOptions
与new Vue(options)
,我们的mergeOptions函数就是把他们合并的:
// 合并组件的配置项
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 序列化props, inject, directive
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
// child的mixins加入parent中
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
// 使用strat中的合并方法去依次合并配置对象
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
}
那么我们写的一些组件和特性全部放在vm.$options
里面了,那么下一步也就是renderProxy
咱们有兴趣的童鞋,可以依据引用路径
去看看实现原理。
那么我们initMixin
函数执行完前两步之后需要执行的几个函数分别为
// 依次初始化配置项, 并调用生命周期钩子
vm._self = vm
initLifecycle(vm)
// 事件监听初始化
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
//初始化vm状态 prop/data/computed/watch完成初始化
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
那我们先来了解一下这几个函数分别干了啥:
1、initLifecycle
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
2、initEvents
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
3、initRender
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 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)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
4、callback
export function callHook (vm: Component, hook: string) {
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
}
那我们在initRender
中可以发现初始化了一些功能,例如$lintener
,$createElement
,而在callback
里面却直接调用了beforeCreate钩子函数。
后面还有
5、initInjections
,resolveInject
,initProvide
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject).filter(key => {
/* istanbul ignore next */
return Object.getOwnPropertyDescriptor(inject, key).enumerable
})
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
那么关于这三个函数是干嘛的?我们先来了解一下:
provide/inject:
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
那就是相当于初始化依赖的关系,而initProvide
基本没有什么内容,就是将$options
里的provide
赋值到当前实例上
写到这里那么激动人心的时刻到了--initState
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
// 如果选项中不存在data,调用observe来观测一个空对象
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// 这里需要额外判断一下opts.watch !== nativeWatch
// 是因为在Firefox下, Object实例上会有一个自带的watch属性, 需要判断这个watch必须是vue提供的watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
可以看出initState
里面初始化了props
,data
,computed
,watch
,methods
那么我们主要看一下initData
,initProps
的代码:
/** * data初始化, 和props基本一致 */
function initData (vm: Component) {
let data = vm.$options.data
/** * data字段有两种可选类型 * 当data是function时, 直接执行这个function 并将返回值作为data * data也可以直接就是一个object, 但仅应该在root组件这样做, 因为直接使用data对象会导致多个相同的组件持有同一个data对象的引用 * 而使用一个返回新对象的function就可以避免这个问题 * 详见 https://cn.vuejs.org/v2/style-guide/#%E7%BB%84%E4%BB%B6%E6%95%B0%E6%8D%AE-%E5%BF%85%E8%A6%81 * 其实在initData之前的mergeOptions操作中已经将data格式化为一个function * 但是在initData和mergeOptions中间还有一个生命周期钩子beforeCreate被调用 * 这里使用typeof再次判断data的类型是为了防止在beforeCreate中改变了vm.$options.data */
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 防止开发者在写data选项时不按规矩返回一个对象
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
// 将data的每个字段都代理到vm实例上, 并判断是否与methods, props冲突, 重复key会报错
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 检查是否有重复的key, 这里可以看出props优先级最高, 其次是data, 最后是methods
// 真的出现重名时,会按照优先级覆盖
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
// isReserved用于判断一个key是否以$或者_开头,vue不会在vm上面代理这些key
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
// 使data变为响应式
observe(data, true /* asRootData */)
}
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
// 使用toggleObserving可以设置shouldObserve为参数的值, 这里设为false是阻止观察者响应更新, 直到props初始化结束
if (!isRoot) {
toggleObserving(false)
}
// 遍历所有props选项, 并校验props
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
// 如果使用了保留属性, 这里会报错, 保留属性包括key,ref,slot,slot-scope,is
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// isUpdatingChildComponent会在组件开始更新时置为true, 完成更新时置为false
// 如果子组件中更改一个props时会导致父组件也重新渲染, 这里就会抛出警告
defineReactive(props, key, value, () => {
if (vm.$parent && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
// 使props变为响应式属性
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
// 将props都代理到vm实例上, 以便代码中可以通过this.propName去访问到对应的props
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
// 初始化完毕, 开启响应式更新
toggleObserving(true)
}
上述代码实现了把props
,data
变为响应式,后面的computed
,watch
暂时不讲,后续专门有一篇文章来写他们。
那么initState
之后我们还会执行一个callback
函数,传入的是created
参数,调用钩子函数created
这个时候也就是页面已经创建了,并且下一个生命周期为beforMount
,在讲挂载之前,一定要讲响应式系统原理