上一篇:Vue2.x 源码学习准备
这篇主要看一下 initMixin 这个混入以及涉及到的 initProxy(vm)、initInjections(vm)、initProvide(vm) 方法;
准备工作
我们在使用 Vue 时是通过 new Vue() 来进行初始化的,那么这个 Vue 从哪里来的呢?
1、在 main.js 引入的 Vue 是在入口文件 src/platforms/web/entry-runtimes.js 里面暴露出来的; 2、入口文件里面的 Vue 是从 src/platforms/web/runtime/index.js 里面引入的; 3、src/platforms/web/runtime/index.js 里面的 Vue 则是从 src/core/index.js 里面引入的; 4、src/core/index.js 里面的 Vue 又是从 src/core/instance/index.js 里面引入的;
这样就找到了 Vue的本体;那么 Vue 到底是什么呢?
在 src/core/instance/index.js 文件中
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
这里 warn 信息直接告诉我们 Vue 的本质:Vue是一个构造函数,应该用“new”关键字调用 ;然后在 Vue 构造函数里面执行 this._init(options) 方法,传入初始化参数;代码最后面执行几个方法往 Vue 原型上混入一些自定义的原型方法,下面会分别说明一下。
一、初始化混入 initMixin(Vue)
在 src/core/instance/init.js 文件里面
let uid = 0
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
let startTag, endTag
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
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)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
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)
}
}
}
这一部分主要做了如下操作:
1、缓存当前的上下文到 vm 变量中; 2、为 vm 添加唯一标识; 3、打标记用来测试性能; 4、用 _isVue 值标识当前实例; 5、合并选项,区分组件实例和非组件实例; 6、初始化代理; 7、下面就是一些组件、事件、render、inject、data/props、provide 的初始化; 8、再次打标记用来测试性能; 9、el 挂载。
总结:先合并 options => 初始化代理 => 初始化组件实例相关属性,确定组件(Vue实例)的父子关系 => 初始化事件,将父组件自定义事件传递给子组件 => 绑定将 render 函数转化为 vnode 的方法 => 调用 beforeCreate 生命周期钩子 => 初始化inject, 让子组件可以访问到对应的依赖 => 将组件定义的状态(props, methods, data, computed, watch)挂载到this下 => 初始化provide 为子组件提供依赖 => 调用 created生命周期钩子 => 执行 $mount 挂载 el。
1、initInternalComponent:合并组件实例的参数
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
用Object.create 这个函数,把组件构造函数的 options 挂载到 vm.$options 的__proto__ 上,指定vm.$options 的原型;通过传入的参数 options 为 vm.$options 添加一些属性,把组件依赖父组件的 props 、listeners 等属性挂载到 vm.$options ,方便子组件调用。
2、resolveConstructorOptions:合并父级构造器参数和实例本身参数
区分 Vue 构造器和 Vue.extend 拓展器,Ctor.super 是 Vue.extend 里面定义的属性。如果是构造器则直接返回参数,如果是拓展器则执行内部代码。
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) {
Ctor.superOptions = superOptions
const modifiedOptions = resolveModifiedOptions(Ctor)
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
let modified
const latest = Ctor.options
const sealed = Ctor.sealedOptions
for (const key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
modified[key] = latest[key]
}
}
return modified
}
执行 extend 时,会出现 Vue.mixin 来对父级和子级混入一些参数,这个时候需要判断拓展器执行前后父级和子级的参数是否发生了变化,变了则更新最新的参数;最后返回合并后的的构造函数的options。
3、mergeOptions:合并实例参数和入参
在 src/core/util/options.js 文件中,这个方法的目的是合并构造函数 options 和传入的 options 这两个对象。
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
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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
}
这里合并 options 的时候会针对 props、inject、directives 分别进行合并;当传入的 options 里有 mixin 或者 extends 属性时会再次调用 mergeOptions 方法合并 mixins 和 extends 里的内容到实例的构造函数 options 上。
然后就是这个方法的核心部分,分别循环 parent 和 child ,循环 child 时额外判断当前属性不在 parent 属性里,然后调用 mergeField 方法通过 strats 和 defaultStrat 合并策略来合并 options。
defaultStrat 的逻辑是,如果 child 上该属性值存在时,就取 child 上的该属性值,如果不存在,则取 parent 上的该属性值。 strats 又细分几种策略:
1、el、propsData :直接走的 defaultStrat 策略; 2、component、directive、filter:首先缓存 parent ;如果 child 有则合并,以 child为准; 3、watch:child 属性不存在直接返回 parent;child 属性存在则判断是不是对象;parent 属性不存在则直接返回 child 属性;都存在则合并; 4、props、methods、inject、computed:如果 child 属性存在则判断是否是对象,parent 上没有该属性则直接返回 child 上属性;如果 child 和 parent 都有则合并,以child 的值为准; 5、钩子函数:child 上不存在而 parent 上存在则返回 parent 上属性;child 和 parent 上都有则返回 concat 后的属性(同名child覆盖parent);child 有而 parent 上没有则返回 child 属性(这个属性必须是数组,如果不是则转成数组); 6、data 、provide:会区分合并的是不是 Vue 实例;是Vue实例,options 有 data 属性则调用 mergeData 合并 child 和 parent,没有则走 defaultStrat 策略;不是 Vue 实例,没有 child 则返回 parent,没有 parent 则返回 child,两个都有则调用 mergeData 合并 child 和 parent;
总之,就是会以 child 属性为准。
这里就把所有业务逻辑和组件的一些特性全部都转化放到 vm.$options 里面了,后面用到的时候只需要从 vm.$options 里面取值就可以了。
二、initProxy:初始化代理
let initProxy
initProxy = function initProxy (vm) {
if (hasProxy) {
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}
export { initProxy }
当前环境 Proxy 不可用则 Vue 实例的 _renderProxy 属性指向 Vue 实例本身;Proxy 可用,如果实例的 options 上面存在 render 然后 render 上有 _withStripped 属性则调用 getHandler 否则调用 hasHandler。
getHandler :针对读取代理对象的属性时进行操作,属性不是字符串或者不存在则报错,否则返回属性值; hasHandler:开发过程中错误调用 vm 属性时,起提示作用;
注意:options.render._withStripped这个只有在严格模式下不支持with时,手动设置为true才启用,所以一般都是使用hasHandler
三、initInjections(vm)、initProvide(vm)
在 src/core/instance/inject.js 文件里面
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
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) {
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (key === '__ob__') continue
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
}
}
先遍历每一项,然后在遍历每一项的父级是否提供该项的依赖,有就返回到 result ,没有就继续找;获取到 result 之后会调用 toggleObserving 来关闭响应式绑定属性,具体实现在 defineReactive 里面,通过传参 true 和 false 来决定是否将绑定的属性设置为响应式数据;这里刻意在绑定 inject 上属性时关闭响应式绑定。
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
从 vm 实例的 options 里面取出 provide ,如果 provide 是 function 则绑定 this 给 vm._provided 私有属性,不是 function 则直接赋值给 vm._provided 私有属性;这样子组件就可以访问父组件提供的依赖了。
这里要额外的说说 inject 和 provide 这两个 API,他们俩是搭配使用的,provide 在父组件提供依赖绑定到 vm 实例的 _provided 属性上,这样可以全局访问这些绑定的依赖,inject 则在子组件通过入参在自己的父级链上获取到对应的依赖。
这里有一个问题:在初始化 inject 的时候会去父级上去找 provide ,但是 provide 的初始化在 inject 后面,这样会不会有问题呢?
答案是没有问题;这两个 API 主要是处理父子组件之间的传值,在初始化的时候,首先会初始化父组件,然后才会初始化子组件,所以这个时候子组件是可以拿到父组件里面的 provide 的;(父beforeCreate -> 父created -> 父beforeMount ->子beforeCreate -> 子created ->子beforeMount -> 子mounted-> 父mounted)
|