composition-api 源码解析
source link: https://4ark.me/post/composition-api-score-code/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
# 版本说明
本文是针对 composition-api v1.0.0-rc.6 (opens new window) 版本的一次源码解析,主要是想探析以下两点:
- Vue 在安装 composition-api 时做了些什么?
- Vue 在执行每个组件的
setup
方法时做了什么?
好了,废话不多说,我们直接开始。
# 一、安装过程
# 1. 检测是否已安装
// src/install.ts
if (isVueRegistered(Vue)) {
if (__DEV__) {
warn(
'[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.'
)
}
return
}
首先是检查是否重复安装,如果是则在开发环境中发出警告,主要是调用了 isVueRegistered
方法来进行检测,下面是它的定义:
// src/runtimeContext.ts
const PluginInstalledFlag = '__composition_api_installed__'
export function isVueRegistered(Vue: VueConstructor) {
return hasOwn(Vue, PluginInstalledFlag)
}
通过检测 Vue 的 __composition_api_installed__
这个属性来 composition-api
是否已经安装。
那很明显后来真正安装 composition-api 时会设置这个属性。
# 2. 检测 Vue 版本
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
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 的 自定义选项合并策略 (opens new window) 来添加 setup
这个 api。
ps:是否还有同学不知道我们可以自定义 Vue 的 options 呢?可以尝试利用这个 api 来实现一个 asyncComputed
和 multiWatch
来玩玩哦!
# 4. 设置已安装标记
// src/runtimeContext.ts
const PluginInstalledFlag = '__composition_api_installed__'
export function setVueConstructor(Vue: VueConstructor) {
// @ts-ignore
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. 设置全局混合
Vue.mixin({
beforeCreate: functionApiInit
// ... other
})
然后添加一个全局的 mixin
,在每个组件的 beforeCreate
生命周期执行一下 functionApiInit
方法。
以上就是安装 composition-api 做的事,关于 functionApiInit
的内容我们在下一小节中详细讲解 。
# 二、执行 setup
我们知道 composition-api 主要是新增了一个 setup
选项,以及一系列 hooks,而 steup
也不是简单调用一下就完事,在这之前需要做一些事,比如传入的两个参数:props
、ctx
是怎么来的,以及 setup
的返回值为何可以在 template
中使用等等。
前面讲了 compsition-api 会在每个组件的 beforeCreate
时执行一下 functionApiInit
方法 :
Vue.mixin({
beforeCreate: functionApiInit
// ... other
})
下面是这个方法主要做的事。
# 1. 检测是否有 render
第一步是检测是否定义 render
方法,如果有 render
方法,则修改它内部。
const vm = this
const $options = vm.$options
const { setup, render } = $options
if (render) {
// keep currentInstance accessible for createElement
$options.render = function(...args: any): any {
return activateCurrentInstance(vm, () => render.apply(this, args))
}
}
activateCurrentInstance
的作用就是设置当前实例,所以我们可以在 render
中通过 getCurrentInstance
访问到当前实例。
ps:值得说明的是即便我们写的是 template
,但到了目前这个阶段这里它已经被转换成 render
函数了。
# 2. 检测是否有 setup
如果没有定义 setup
,说明这个组件没有使用 composition-api
,这时候则直接跳过该组件:
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
方法:
const { data } = $options
// wrapper the data option, so we can invoke setup before data get resolved
$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
方法内部还做了挺多事的,下面是这个方法的全貌,先简单瞄一眼,我们后面会一步步拆解:
function initSetup(vm: ComponentInstance, props: Record<any, any> = {}) {
const setup = vm.$options.setup!
const ctx = createSetupContext(vm)
// fake reactive for `toRefs(props)`
def(props, '__ob__', createObserver())
// resolve scopedSlots and slots to functions
// @ts-expect-error
resolveScopedSlots(vm, ctx.slots)
let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null
activateCurrentInstance(vm, () => {
// make props to be fake reactive, this is for `toRefs(props)`
binding = setup(props, ctx)
})
if (!binding) return
if (isFunction(binding)) {
// keep typescript happy with the binding type.
const bindingFunc = binding
// keep currentInstance accessible for createElement
vm.$options.render = () => {
// @ts-expect-error
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)) {
// creates a custom reactive properties without make the object explicitly reactive
// NOTE we should try to avoid this, better implementation needed
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
中接受的第二个参数,这个对象里面的内容是怎么生成的呢?
const ctx = createSetupContext(vm)
下面是 createSetupContext
所做的事,首先是定义 ctx
对象中所有的 key
:
const ctx = { slots: {} } as SetupContext
const propsPlain = [
'root',
'parent',
'refs',
'listeners',
'isServer',
'ssrContext',
]
const propsReactiveProxy = ['attrs']
const methodReturnVoid = ['emit']
接下来就是给这些属性利用 Object.defineProperty
做一层代理,当然它们都是只读的:
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:
def(props, '__ob__', createObserver())
// src/reactivity/reactive.ts
export function createObserver() {
return observe < any > {}.__ob__
}
首先通过 createObserver
拿到一个把空对象经过 Vue.Observer 后的 __ob__
属性,也就是当前 Observer
实例对象,如果同学们对于 Vue Observer 的原理还不太熟悉,可以看这里 数据对象的 (opens new window),本文就不赘述了。
然后给 props
新增一个 __ob_
属性,指向前面拿到的这个 __ob__
。
# 4.3. 解析 slots
接着就是把当前实例的 slots
给代理到前面定义的 ctx.slots
中,这时候它只是一个空对象:
resolveScopedSlots(vm, ctx.slots)
下面是 resolveScopedSlots
的实现:
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)
// remove staled slots
for (let index = 0; index < prevSlots.length; index++) {
const key = prevSlots[index]
if (!curSlots[key]) {
delete slotsProxy[key]
}
}
// proxy fresh slots
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
了:
activateCurrentInstance(vm, () => {
// make props to be fake reactive, this is for `toRefs(props)`
binding = setup(props, ctx)
})
activateCurrentInstance
之前讲过了,就是使组件的 setup
内部可以通过 getCurrentInstance
访问当前实例,相信真正使用过 composition-api
的同学们都知道这个方法的便利性了,但不知道同学们是否遇到过 getCurrentInstance
方法返回 null
值的情况呢?如果想知道为什么,可以看这篇文章:《从 Composition API 源码分析 getCurrentInstance() 为何返回 null》 (opens new window)。
然后将前面得到的 props
和 ctx
传进去,最后将返回值赋值给 binding
。
# 4.6. 处理 setup 返回值
处理返回值前需要先对它进行类型判断,有三种条件分支:
- 为空,直接返回
- 是一个函数,当成
render
方法处理 - 是一个普通对象,做一系列转换
如果返回值是一个函数,则把它当成 render
方法处理,当然在这之前需要重新调用一下 resolveScopedSlots
检测 slots
的更新,并且调用 activateCurrentInstance
:
if (isFunction(binding)) {
// keep typescript happy with the binding type.
const bindingFunc = binding
// keep currentInstance accessible for createElement
vm.$options.render = () => {
// @ts-expect-error
resolveScopedSlots(vm, ctx.slots)
return activateCurrentInstance(vm, () => bindingFunc())
}
return
}
ps:也可以直接在 setup
中返回 JSX
哦,因为 Babel 会把它变成一个函数。
但通常我们是在 setup
返回一个对象,然后可以直接在 template
中使用这个这些值,所以我们看看返回值是一个对象的情况:
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)) {
// creates a custom reactive properties without make the object explicitly reactive
// NOTE we should try to avoid this, better implementation needed
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
包装才行,所以我们在开发时要避免如下情况:
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 方法来编写更优雅、可复用的代码。
本文就到此,感谢你的阅读。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK