版本说明 本文是针对 composition-api v1.0.0-rc.6 版本的一次源码解析,主要是想探析以下两点:
Vue 在安装 composition-api 时做了些什么?
Vue 在执行每个组件的 setup
方法时做了什么?
好了,废话不多说,我们直接开始。
一、安装过程 1. 检测是否已安装 1 2 3 4 5 6 7 8 9 10 if (isVueRegistered(Vue)) { if (__DEV__) { warn( '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.' ) } return }
首先是检查是否重复安装,如果是则在开发环境中发出警告,主要是调用了 isVueRegistered
方法来进行检测,下面是它的定义:
1 2 3 4 5 6 7 const PluginInstalledFlag = '__composition_api_installed__' export function isVueRegistered (Vue: VueConstructor ) { return hasOwn(Vue, PluginInstalledFlag) }
通过检测 Vue 的 __composition_api_installed__
这个属性来 composition-api
是否已经安装。
那很明显后来真正安装 composition-api 时会设置这个属性。
2. 检测 Vue 版本 1 2 3 4 5 6 7 8 9 10 11 if (__DEV__) { if (Vue.version) { if (Vue.version[0 ] !== '2' || Vue.version[1 ] !== '.' ) { warn( `[vue-composition-api] only works with Vue 2, v${Vue.version} found.` ) } } else { warn('[vue-composition-api] no Vue version found' ) } }
然后在开发环境中判断 Vue 的版本,必须是 2.x 的版本才能使用 composition-api。
3. 添加 setup 这个 option api 1 2 3 4 5 6 7 8 9 10 11 Vue.config.optionMergeStrategies.setup = function ( parent: Function , child: Function ) { return function mergedSetupFn (props: any, context: any ) { return mergeData( typeof parent === 'function' ? parent(props, context) || {} : undefined , typeof child === 'function' ? child(props, context) || {} : undefined ) } }
接着通过 Vue 的 自定义选项合并策略 来添加 setup
这个 api。
ps:是否还有同学不知道我们可以自定义 Vue 的 options 呢?可以尝试利用这个 api 来实现一个 asyncComputed
和 multiWatch
来玩玩哦!
4. 设置已安装标记 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const PluginInstalledFlag = '__composition_api_installed__' export function setVueConstructor (Vue: VueConstructor ) { if (__DEV__ && vueConstructor && Vue.__proto__ !== vueConstructor.__proto__) { warn('[vue-composition-api] another instance of Vue installed' ) } vueConstructor = Vue Object .defineProperty(Vue, PluginInstalledFlag, { configurable : true , writable : true , value : true }) }
上面提到过,就是在这里设置一个表示已经安装的标记。
5. 设置全局混合 1 2 3 4 Vue.mixin({ beforeCreate : functionApiInit })
然后添加一个全局的 mixin
,在每个组件的 beforeCreate
生命周期执行一下 functionApiInit
方法。
以上就是安装 composition-api 做的事,关于 functionApiInit
的内容我们在下一小节中详细讲解 。
二、执行 setup 我们知道 composition-api 主要是新增了一个 setup
选项,以及一系列 hooks,而 steup
也不是简单调用一下就完事,在这之前需要做一些事,比如传入的两个参数: props
、 ctx
是怎么来的,以及 setup
的返回值为何可以在 template
中使用等等。
前面讲了 compsition-api 会在每个组件的 beforeCreate
时执行一下 functionApiInit
方法 :
1 2 3 4 Vue.mixin({ beforeCreate : functionApiInit })
下面是这个方法主要做的事。
1. 检测是否有 render 第一步是检测是否定义 render
方法,如果有 render
方法,则修改它内部。
1 2 3 4 5 6 7 8 9 10 const vm = this const $options = vm.$optionsconst { setup, render } = $optionsif (render) { $options.render = function (...args: any ): any { return activateCurrentInstance(vm, () => render.apply(this , args)) } }
activateCurrentInstance
的作用就是设置当前实例,所以我们可以在 render
中通过 getCurrentInstance
访问到当前实例。
ps:值得说明的是即便我们写的是 template
,但到了目前这个阶段这里它已经被转换成 render
函数了。
2. 检测是否有 setup 如果没有定义 setup
,说明这个组件没有使用 composition-api
,这时候则直接跳过该组件:
1 2 3 4 5 6 7 8 9 10 11 12 if (!setup) { return } if (typeof setup !== 'function' ) { if (__DEV__) { warn( 'The "setup" option should be a function that returns a object in component definitions.' , vm ) } return }
3. 在 data 方法初始化 setup 如果存在 setup
,就会修改这个组件的 data
方法,在初始化真正的 data
方法之前先初始化一下 setup
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 const { data } = $options $options.data = function wrappedData ( ) { initSetup(vm, vm.$props) return typeof data === 'function' ? (data as (this : ComponentInstance, x : ComponentInstance) => object).call( vm, vm ) : data || {} }
还记得 Vue 初始化 data
的时机是什么时候吗?答案是在 beforeCreate
和 created
之间,所以 setup
也是一样。
4. 初始化 setup initSetup
方法内部还做了挺多事的,下面是这个方法的全貌,先简单瞄一眼,我们后面会一步步拆解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 function initSetup (vm: ComponentInstance, props: Record < any, any > = {} ) { const setup = vm.$options.setup! const ctx = createSetupContext(vm) def(props, '__ob__' , createObserver()) resolveScopedSlots(vm, ctx.slots) let binding: ReturnType < SetupFunction < Data, Data >> | undefined | null activateCurrentInstance(vm, () => { binding = setup(props, ctx) }) if (!binding) return if (isFunction(binding)) { const bindingFunc = binding vm.$options.render = () => { resolveScopedSlots(vm, ctx.slots) return activateCurrentInstance(vm, () => bindingFunc()) } return } else if (isPlainObject(binding)) { if (isReactive(binding)) { binding = toRefs(binding) as Data } vmStateManager.set(vm, 'rawBindings' , binding) const bindingObj = binding Object .keys(bindingObj).forEach((name ) => { let bindingValue: any = bindingObj[name] if (!isRef(bindingValue)) { if (!isReactive(bindingValue)) { if (isFunction(bindingValue)) { bindingValue = bindingValue.bind(vm) } else if (!isObject(bindingValue)) { bindingValue = ref(bindingValue) } else if (hasReactiveArrayChild(bindingValue)) { customReactive(bindingValue) } } else if (isArray(bindingValue)) { bindingValue = ref(bindingValue) } } asVmProperty(vm, name, bindingValue) }) return } if (__DEV__) { assert( false , `"setup" must return a "Object" or a "Function", got "${Object .prototype.toString .call(binding) .slice(8 , -1 )} "` ) } }
4.1. 初始化 context 这个 ctx
是 setup
中接受的第二个参数,这个对象里面的内容是怎么生成的呢?
1 const ctx = createSetupContext(vm)
下面是 createSetupContext
所做的事,首先是定义 ctx
对象中所有的 key
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const ctx = { slots : {} } as SetupContextconst propsPlain = [ 'root' , 'parent' , 'refs' , 'listeners' , 'isServer' , 'ssrContext' , ] const propsReactiveProxy = ['attrs' ]const methodReturnVoid = ['emit' ]
接下来就是给这些属性利用 Object.defineProperty
做一层代理,当然它们都是只读的:
1 2 3 4 5 6 7 8 9 propsPlain.forEach((key ) => { let srcKey = `$${key} ` proxy(ctx, key, { get : () => vm[srcKey], set ( ) { warn(`Cannot assign to '${key} ' because it is a read-only property` , vm) } }) })
另外两个 propsReactiveProxy
和 methodReturnVoid
也差不多,这里就略过了。
4.2. 响应式 props 接着就是将 props
对象进行一遍 Observer:
1 2 3 4 5 6 def(props, '__ob__' , createObserver()) export function createObserver ( ) { return observe < any > {}.__ob__ }
首先通过 createObserver
拿到一个把空对象经过 Vue. Observer 后的 __ob__
属性,也就是当前 Observer
实例对象,如果同学们对于 Vue Observer 的原理还不太熟悉,可以看这里 数据对象的 ,本文就不赘述了。
然后给 props
新增一个 __ob_
属性,指向前面拿到的这个 __ob__
。
4.3. 解析 slots 接着就是把当前实例的 slots
给代理到前面定义的 ctx.slots
中,这时候它只是一个空对象:
1 resolveScopedSlots(vm, ctx.slots)
下面是 resolveScopedSlots
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export function resolveScopedSlots ( vm: ComponentInstance, slotsProxy: { [x: string]: Function } ): void { const parentVNode = (vm.$options as any)._parentVnode if (!parentVNode) return const prevSlots = vmStateManager.get(vm, 'slots' ) || [] const curSlots = resolveSlots(parentVNode.data.scopedSlots, vm.$slots) for (let index = 0 ; index < prevSlots.length; index++) { const key = prevSlots[index] if (!curSlots[key]) { delete slotsProxy[key] } } const slotNames = Object .keys(curSlots) for (let index = 0 ; index < slotNames.length; index++) { const key = slotNames[index] if (!slotsProxy[key]) { slotsProxy[key] = createSlotProxy(vm, key) } } vmStateManager.set(vm, 'slots' , slotNames) }
简单来说就是将父组件的 slots
数组(真正被使用的)代理到 ctx.slots
中,并且在这个 slots
数组有变化时 ctx.slots
也会相应地更新。
4.4. 执行 setup 终于到了最重要的关头,开始执行 setup
了:
1 2 3 4 activateCurrentInstance(vm, () => { binding = setup(props, ctx) })
activateCurrentInstance
之前讲过了,就是使组件的 setup
内部可以通过 getCurrentInstance
访问当前实例,相信真正使用过 composition-api
的同学们都知道这个方法的便利性了,但不知道同学们是否遇到过 getCurrentInstance
方法返回 null
值的情况呢?如果想知道为什么,可以看这篇文章:《从 Composition API 源码分析 getCurrentInstance() 为何返回 null》 。
然后将前面得到的 props
和 ctx
传进去,最后将返回值赋值给 binding
。
4.6. 处理 setup 返回值 处理返回值前需要先对它进行类型判断,有三种条件分支:
为空,直接返回
是一个函数,当成 render
方法处理
是一个普通对象,做一系列转换
如果返回值是一个函数,则把它当成 render
方法处理,当然在这之前需要重新调用一下 resolveScopedSlots
检测 slots
的更新,并且调用 activateCurrentInstance
:
1 2 3 4 5 6 7 8 9 10 11 if (isFunction(binding)) { const bindingFunc = binding vm.$options.render = () => { resolveScopedSlots(vm, ctx.slots) return activateCurrentInstance(vm, () => bindingFunc()) } return }
ps:也可以直接在 setup
中返回 JSX
哦,因为 Babel 会把它变成一个函数。
但通常我们是在 setup
返回一个对象,然后可以直接在 template
中使用这个这些值,所以我们看看返回值是一个对象的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 else if (isPlainObject(binding)) { if (isReactive(binding)) { binding = toRefs(binding) as Data } vmStateManager.set(vm, 'rawBindings' , binding) const bindingObj = binding Object .keys(bindingObj).forEach((name ) => { let bindingValue: any = bindingObj[name] if (!isRef(bindingValue)) { if (!isReactive(bindingValue)) { if (isFunction(bindingValue)) { bindingValue = bindingValue.bind(vm) } else if (!isObject(bindingValue)) { bindingValue = ref(bindingValue) } else if (hasReactiveArrayChild(bindingValue)) { customReactive(bindingValue) } } else if (isArray(bindingValue)) { bindingValue = ref(bindingValue) } } asVmProperty(vm, name, bindingValue) }) return }
首先如果返回的对象是经过 reactive
的,则要调用 toRefs
将它的子属性变成 ref
包装过的,然后调用 vmStateManager.set
将这些属性存放起来,以供别的地方使用。
然后遍历这个对象,经过一系列类型判断和处理后,将它的子属性设置为当前实例的变量,这样我们就可以在 templte
或者通过 this.xxx
去访问这些变量。
这里的类型处理简单总结一下就是:
如果属性值是一个函数,则这个函数被调用时已经 this
就是当前实例
如果属性值一个非对象非函数的值,则会自动经过 ref
包装
如果属性值是一个普通对象且有子属性值为经过 reactive
后的数组,则要将这个普通对象也要转换为经过 reactive
包装才行,所以我们在开发时要避免如下情况:
1 2 3 4 5 6 7 setup ( ) { return { obj : { arr : reactive([1 , 2 , 3 , 4 ]) } } }
最后,在开发环境下判断返回值不是对象是抛出一个错误。到此 setup
函数的执行就完了。
总结 关于 composition-api 的安装和执行过程就讲完了,下面我们来简单总结一下,composition-api 在安装时会做以下事情:
通过检查 Vue 的 __composition_api_installed__
属性来判断是否重复安装
检查 Vue 版本是否 2.x
使用合并策略添加 setup
api
标记安装
利用全局混入来对 setup
进行初始化
而在执行 setup
时会做以下事情:
检查当前组件是否使用 render
方法,如果有则在这之前标记当前实例,以便 render
方法内部可以通过 getCurrentInstance
方法访问到当前实例。
检查当前组件有 setup
api,没有则直接返回,否则在初始化 data
时先初始化一下 setup
而初始化 setup
做的事就是构造 setup
接受的两个参数:props、ctx
然后执行 setup
,根据它的返回值类型进行相应的处理
当然,compsition-api 真正的魅力在于 hooks,下次我就来讲讲 composition-api 的一系列 hooks 是如何实现的,这也能帮助我们更好地利用这些 hooks 方法来编写更优雅、可复用的代码。
本文就到此,感谢你的阅读。
🥳 加载 Disqus 评论