前言
组合式 API 是 vue3 提出的一个新的开发方式,而在 vue2 中我们可以使用新的组合式 API 进行组件开发。本篇通过一个例子,来分析这个插件是如何提供功能。
关于该插件的安装、使用,可以直接阅读文档。
安装
我们从最开始安装分析,一探究竟。
vue.use
按照文档所提到的,我们必须通过 Vue.use() 进行安装:
// vue.use 安装 import Vue from 'vue' import VueCompositionAPI from '@vue/composition-api' Vue.use(VueCompositionAPI) 复制代码
我们先看入口文件:
// index.js import type Vue from 'vue' import { Data, SetupFunction } from './component' import { Plugin } from './install' export default Plugin // auto install when using CDN if (typeof window !== 'undefined' && window.Vue) { window.Vue.use(Plugin) } 复制代码
可以知道我们 Vue.use 时,传入的就是 install 文件中的 Plugin 对象。
// install.ts 折叠源码 export function install(Vue: VueConstructor) { if (isVueRegistered(Vue)) { if (__DEV__) { warn( '[vue-composition-api] already installed. Vue.use(VueCompositionAPI) should be called only once.' ) } return } 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.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 ) } } setVueConstructor(Vue) mixin(Vue) } export const Plugin = { install: (Vue: VueConstructor) => install(Vue), } 复制代码
install
通过上面的代码和 Vue.use 可知,我们安装时其实就是调用了 install 方法,先分析一波 install。根据代码块及功能可以分成三个部分:
- 前两个大 if 的开发 check 部分
- 关于 setup 合并策略
- 通过 mixin 混入插件关于 组合式 API 的处理逻辑
第一部分中的第一个 if 是为了确保该 install 方法只被调用一次,避免浪费性能;第二个 if 则是确保vue版本为2.x。不过这里有个关于第一个if的小问题:多次注册插件时,Vue.use 自己本身会进行重复处理——安装过的插件再次注册时,不会调用 install 方法(Vue.use代码见下)。那么这个 if 的目的是啥?
// Vue.use 部分源码 Vue.use = function (plugin: Function | Object) { const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } installedPlugins.push(plugin) return this } 复制代码
根据上面代码可知 Vue.use 实际上还是传入 vue 并调用插件的 install 方法,那么如果有大神(或者是奇葩?)绕过 Vue.use 直接调用,那么这个 if 的判断就生效了。如下方代码,此时第二个 install 会判断重复后,抛出错误
// 直接调用 install import Vue from 'vue' import VueCompositionAPI from '@vue/composition-api' import App from './App.vue' Vue.config.productionTip = false VueCompositionAPI.install(Vue) VueCompositionAPI.install(Vue) 复制代码
报错:
第二部分的合并策略是“Vue.config.optionMergeStrategies”这个代码块。Vue 提供的这个能力很生僻,我们日常的开发中几乎不会主动接触到。先上文档:
这是用来定义属性的合并行为。比如例子中的 extend 在调用时,会执行 mergeOptions。
// Vue.extend Vue.extend = function (extendOptions) { const Super = this extendOptions = extendOptions || {} Sub.options = mergeOptions( Super.options, extendOptions ) } 复制代码
而 mergeOptions 里关于 _my_option的相关如下:
const strats = config.optionMergeStrategies function mergeOptions (parent, child, vm){ 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) } } 复制代码
这里的 parent 就是 Super.options 也就是 Vue.options,而 child 就是 extendOptions 也就是我们传入的 { _my_option: 1 }。在这里使用了两个 for 循环,确保父子元素种所有的 key 都会执行到 mergeField,而第二个 for 循环中的 if 判断确保不会执行两次,保证了正确性及性能。而 mergeField 则是最终执行策略的地方。从 strats 中获取到我们定义的方法,把对应参数传入并执行,在这里就是:
// demo执行 strat(undefined, 1, vm, '_my_option') // return 2 复制代码
顺便一提,Vue.mixin 的实现就是 mergeOptions,也就是说当我们使用了 mixin 且里面具有 setup 属性时,会执行到上述合并策略。
Vue.mixin = function (mixin) { this.options = mergeOptions(this.options, mixin) return this } 复制代码
而我们插件中相关的策略也很简单,获取好定义的父子 setup,然后合并成一个新的,在调用时会分别执行父子 setup,并通过 mergeData 方法合并返回:
// optionMergeStrategies.setup 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 ) } } 复制代码
第三部分则是通过调用 mixin 方法向 vue 中混入一些事件,下面是 mixin 的定义:
function mixin(Vue) { Vue.mixin({ beforeCreate: functionApiInit, mounted(this: ComponentInstance) { updateTemplateRef(this) }, updated(this: ComponentInstance) { updateTemplateRef(this) } }) function functionApiInit() {} function initSetup() {} // 省略... } 复制代码
可以看到 mixin 内部调用了 Vue.mixin 来想 beforeCreate、mounted、updated 等生命周期混入事件。这样就完成 install 的执行, Vue.use(VueCompositionAPI) 也到此结束。
初始化 — functionApiInit
functionApiInit 执行
我们知道在new Vue 时,会执行组件的 beforeCreate 生命周期。此时刚才通过 Vue.mixin 注入的函数 “functionApiInit”开始执行。
function Vue (options) { this._init(options) } Vue.prototype._init = function (options) { initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') // 触发 beforeCreate 生命周期,执行 functionApiInit initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') } 复制代码
该方法也很清晰,分别暂存了组件最开始的 render方法和 data方法(我们平常写的 data 是一个函数),然后在这基础上又扩展了一下这两个方法,达到类似钩子的目的。
function functionApiInit(this: ComponentInstance) { 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)) } } 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 } 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 || {} } } 复制代码
虽然是先扩展的 render,但在 new Vue 的实际执行中会优先执行下方扩展的方法 “wrappedData”。因为 data 的执行是在 new Vue 时发生,而 render 的执行在 $mount 中。所以我们这里就按照执行顺序来看看如何扩展我们的 wrappedData。
wrappedData 这里只是简单执行了 initSetup 方法,对原先的 data 做了判断。这里是因为 Vue 执行时拿到的 data 已经是 wrappedData 这个函数而不是用户编写的 data,所以关于原 data 的处理移交在了 wrappedData 中。可以说 99%的逻辑都在 initSetup 中。我们接下来看这个方法。
setup 调用及处理
这块是通过 initSetup 函数实现的,代码很长且仅有几行是这里不用关心的(可自行研究),整体上可以跟着注释走一遍。
function initSetup(vm: ComponentInstance, props: Record<any, any> = {}) { // 获取定义好的 setup const setup = vm.$options.setup! // 创建 setup 方法接收的第二个参数 context,主流程中使用不上,先忽略 const ctx = createSetupContext(vm) // fake reactive for `toRefs(props)` // porps 相关,主流成可先忽略(毕竟可以不写 props...) def(props, '__ob__', createObserver()) // resolve scopedSlots and slots to functions // slots 相关,同 props 先忽略 // @ts-expect-error resolveScopedSlots(vm, ctx.slots) let binding: ReturnType<SetupFunction<Data, Data>> | undefined | null // 执行 setup activateCurrentInstance(vm, () => { // make props to be fake reactive, this is for `toRefs(props)` binding = setup(props, ctx) }) // 以下都是根据 setup 返回值,进行的一些处理 if (!binding) return if (isFunction(binding)) { // setup 可以返回一个渲染函数(render) // keep typescript happy with the binding type. const bindingFunc = binding // keep currentInstance accessible for createElement // 获取到渲染函数后,手动添加再 vue 实例上 vm.$options.render = () => { // @ts-expect-error resolveScopedSlots(vm, ctx.slots) return activateCurrentInstance(vm, () => bindingFunc()) } return } else if (isPlainObject(binding)) { // setup 返回的是一个普通对象 if (isReactive(binding)) { // 如果返回的是通过 reactive 方法定义的对象,需要通过 toRefs 结构 binding = toRefs(binding) as Data } // 用于 slots 及 $refs ,先忽略 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)}"` ) } } 复制代码
我们先聚焦到 setup 的执行。setup 包裹在 activateCurrentInstance 方法中,activateCurrentInstance 目的是为了设置当前的实例。类似我们平常写的交换a、b变量的值。setup 在调用前,会先获取 currentInstance 变量并赋值给 preVm,最开始时currentInstance 为 null。接着再把 currentInstance 设置成当前的 vue 实例,于是我们变可以在 setup 通过 插件提供的 getCurrentInstance 方法获取到当前实例。在执行完毕后,又通过 setCurrentInstance(preVm) 把 currentInstance 重置为null。所以印证了文档中所说的,只能在 setup 及生命周期(不在本篇重点)中使用 getCurrentInstance 方法。
// setup执行 activateCurrentInstance(vm, () => { // make props to be fake reactive, this is for `toRefs(props)` binding = setup(props, ctx) }) function activateCurrentInstance(vm, fn, onError) { let preVm = getCurrentVue2Instance() setCurrentInstance(vm) try { return fn(vm) } catch (err) { if (onError) { onError(err) } else { throw err } } finally { setCurrentInstance(preVm) } } let currentInstance = null function setCurrentInstance(vm) { // currentInstance?.$scopedSlots currentInstance = vm } function getCurrentVue2Instance() { return currentInstance } function getCurrentInstance() { if (currentInstance) { return toVue3ComponentInstance(currentInstance) } return null } 复制代码
这里有个思考,为什么需要在最后把 currentInstance 设置为 null?我们写了一个点击事件,并在相关的事件代码里调用了getCurrentInstance 。如果在 setup 调用重置为 null ,那么在该事件里就可能导致获取到错误的 currentInstance。于是就置为null 用来避免这个问题。(个人想法,期待指正)。
setup 内部可能会执行的东西有很多,比如通过 ref 定义一个响应式变量,这块放在后续单独说。
当获取完 setup 的返回值 binding 后,会根据其类型来做处理。如果返回函数,则说明这个 setup 返回的是一个渲染函数,便把放回值赋值给 vm.$options.render 供挂载时调用。如果返回的是一个对象,则会做一些相应式处理,这块内容和响应式相关,我们后续和响应式一块看。
// setup 返回对象 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) }) 复制代码
我们这里只看重点函数 “asVmProperty”。我们知道 setup 返回的是一个对象 (赋值给了 binding / bindingObj),且里面的所有属性都能在 vue 的其他选项中使用。那么这块是如何实现的呢?
访问 setup 返回值 — asVmProperty 实现
这个函数执行后,我们就可以在 template 模版及 vue 选项中访问到 setup 的返回值,的下面是“asVmProperty” 这个函数的实现:
function asVmProperty(vm, propName, propValue) { const props = vm.$options.props if (!(propName in vm) && !(props && hasOwn(props, propName))) { if (isRef(propValue)) { proxy(vm, propName, { get: () => propValue.value, set: (val: unknown) => { propValue.value = val }, }) } else { proxy(vm, propName, { get: () => { if (isReactive(propValue)) { ;(propValue as any).__ob__.dep.depend() } return propValue }, set: (val: any) => { propValue = val }, }) } } } function proxy(target, key, { get, set }) { Object.defineProperty(target, key, { enumerable: true, configurable: true, get: get || noopFn, set: set || noopFn, }) } 复制代码
函数很短,这里有3个处理逻辑:
- 普通属性的 get 和 set 正常返回
- 如果是 ref 类型的属性(通过 ref 创建),通过 vm.xxx 访问/修改时,访问/修改 ref 的 value 属性
- 代理 reactive 类型的属性 (通过 reactive 创建),reactive 返回的是一个响应式对象。当访问这个对象时, 需要调用 响应式对象种的 depend 收集watcher(观察者),以便数据更新时通知 watcher 进行更新。
总之 asVmProperty 是拿到 setup 返回值中的一个键值对后,再通过 Object.defineProperty 劫持了 this(是vm,也就是组件实例)中访问改键值对的 get 和 set,这样我们便可以通过 this.xxx 访问到 setup 中return 出去的属性。
而模版访问也同理,因为 template 编译成 render 后,上面的变量都实际会编译成 _vm.xxx,而 _vm 就是 this ,也就是组件实例。
结语
创作不易,如果对大家有所帮助,希望大家点赞支持,有什么问题也可以在评论区里讨论😄~
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star: http://github.crmeb.net/u/defu 不胜感激 !来自 “开源独尊 ” ,链接: https://ym.baisou.ltd/post/847.html