版本说明
本文是针对 composition-api v1.0.0-rc.6 版本的一次源码解析,主要是想探析以下两点:
- 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 的 自定义选项合并策略 来添加 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 的原理还不太熟悉,可以看这里 数据对象的 ,本文就不赘述了。
然后给 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》。
然后将前面得到的 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
 - 使用合并策略添加 
setupapi - 标记安装
 - 利用全局混入来对 
setup进行初始化 
而在执行 setup 时会做以下事情:
- 检查当前组件是否使用 
render方法,如果有则在这之前标记当前实例,以便render方法内部可以通过getCurrentInstance方法访问到当前实例。 - 检查当前组件有 
setupapi,没有则直接返回,否则在初始化data时先初始化一下setup - 而初始化 
setup做的事就是构造setup接受的两个参数:props、ctx - 然后执行 
setup,根据它的返回值类型进行相应的处理 
当然,compsition-api 真正的魅力在于 hooks,下次我就来讲讲 composition-api 的一系列 hooks 是如何实现的,这也能帮助我们更好地利用这些 hooks 方法来编写更优雅、可复用的代码。
本文就到此,感谢你的阅读。