@vue/composition-api 與 Vue3 的前生今世
通過本文你將會(huì) GET
compositions-api 的誕生背景 @vue/composition-api 和 vue3 的‘姻緣’ @vue/composition-api 實(shí)現(xiàn)原理 @vue/composition-api 的優(yōu)勢與劣勢
Why @vue/compositions-api?
首先,來區(qū)分一下 compositions-api 和 @vue/compositions-api 這兩個(gè)東東。
compositions-api(組合式 API) 是 Vue3 提出的一個(gè)新的 Vue 概念(語法)。
@vue/compositions-api 是 Vue2 的一個(gè)插件,需通過 Vue.use() 進(jìn)行調(diào)用。
為什么會(huì)有 compositions-api
根據(jù)官方文檔描述:
?composition-api-rfc[1]組合式 API: 一組低侵入式的、函數(shù)式的 API,使得我們能夠更靈活地「組合」組件的邏輯。
?
好處是:
更好的邏輯復(fù)用與代碼組織 更好的類型推導(dǎo)
相同組件邏輯下,原來的 options 形式實(shí)現(xiàn)與新的 composition-api 實(shí)現(xiàn)代碼結(jié)構(gòu)對(duì)比:

為什么會(huì)有 @vue/compositions-api
為了抹平 compositions-api 語法和 Vue2 的 gap,或者說為了讓 Vue2 項(xiàng)目也能體驗(yàn)到 compositions-api 帶來的便利和快感, Vue團(tuán)隊(duì)提供了 @vue/compositions-api 插件的解決方案進(jìn)行處理。
因此在 Vue2 項(xiàng)目中你也可以歡快的使用 compositions-api 語法(當(dāng)然了由于實(shí)現(xiàn)原理的差異,某些語法功能支持并不友好)。
@vue/composition-api 和 vue3 的‘姻緣’
@vue/composition-api 插件與 Vue3 一樣,都是誕生于 2019 年,也就是 在 Vue3 提出來的基于 Proxy 實(shí)現(xiàn)的時(shí)候,Vue團(tuán)隊(duì)就已經(jīng)考慮到利用 @vue/composition-api 插件,來抹平瀏覽器的兼容性問題了。
并且上篇文章也已經(jīng)提到,為什么會(huì)有 vue2 + @vue/composition-api 這種產(chǎn)物,直接用 Vue3 不香嗎,主要的原因還是 Vue3 的兼容性問題(各大瀏覽器廠商對(duì)Proxy的支持還沒普及)。
那么 vue2 + @vue/composition-api 到底是個(gè)什么東東呢,怎么用呢?
簡單用法如下:
在 vue2 項(xiàng)目中安裝
npm install @vue/composition-api
在使用 @vue/composition-api前,必須先通過 Vue.use() 進(jìn)行安裝。之后才可使用新的組合式 API進(jìn)行組件開發(fā)。
import?Vue?from?'vue'
import?VueCompositionAPI?from?'@vue/composition-api'
Vue.use(VueCompositionAPI)
//?使用?API
import?{?ref,?reactive?}?from?'@vue/composition-api'
//?而在?vue3?中?
//?直接?import?{?ref,?reactive?}?from?'vue'?即可,?
//?不需要引入插件,并單獨(dú)從?'@vue/composition-api'?解構(gòu) api
??? 當(dāng)遷移到 Vue 3 時(shí),只需簡單的將 @vue/composition-api 替換成 vue 即可。現(xiàn)有的代碼幾乎無需進(jìn)行額外的改動(dòng)。
?
你可以盡情的享受 composition-api 帶來的快感了

@vue/composition-api 部分實(shí)現(xiàn)原理
這里我們主要介紹,基于 Vue2 @vue/composition-api 的一些實(shí)現(xiàn)原理(基于 Vue3 composition-api實(shí)現(xiàn)后面單獨(dú)篇幅進(jìn)行討論)。
源碼整體結(jié)構(gòu)如下圖(index 入口文件)

可以看出來,默認(rèn)導(dǎo)出是 install 函數(shù),用于 Vue.use 進(jìn)行插件安裝, 其他的都是一些具體的 composition-api 的功能函數(shù)。
那么,為了有側(cè)重點(diǎn),下面我們主要圍繞幾個(gè)問題進(jìn)行重點(diǎn)討論
來一看 install 主要干了什么? setup 中為什么可以隨意使用 composition-api,并脫離了 this? 基于 vue2 的 reactive / ref 是怎么實(shí)現(xiàn)的?
首先,一起來剖析一下 install 函數(shù)
//?install(Vue,?mixin)
export?function?install(
??Vue:?VueConstructor,
??_install:?(Vue:?VueConstructor)?=>?void
)?{
??//?這里去掉了?dev?調(diào)試模式的邏輯
??if?(currentVue?&&?currentVue?===?Vue)?{
????return
??}
??//?你可能會(huì)困惑 Vue.config.optionMergeStrategies 這個(gè)是什么東東?
??//?vue2.6?源碼中你可以找到答案?
??//?vue/src/core/util/options.js
??//?Option?overwriting?strategies?are?functions?that?handle
??//?how?to?merge?a?parent?option?value?and?a?child?option
??//?value?into?the?final?value.
??//?
??Vue.config.optionMergeStrategies.setup?=?function?(
????parent:?Function,
????child:?Function
??)?{
????//?mergeData?函數(shù)在?vue2.6?源碼中同樣存在
????//?mergeData?-?recursively?merges?two?data?objects?together.
????//?
????return?function?mergedSetupFn(props:?any,?context:?any)?{
??????return?mergeData(
????????typeof?parent?===?'function'???parent(props,?context)?||?{}?:?undefined,
????????typeof?child?===?'function'???child(props,?context)?||?{}?:?undefined
??????)
????}
??}
??//?設(shè)置全劇唯一?currentVue?實(shí)例
??setCurrentVue(Vue)
??//?注冊(cè)安裝到?Vue,@vue/composition-api?最核心邏輯
??_install(Vue)
}
下面來看看 ?_install(Vue) 到底干了什么, 也就是 mixin 函數(shù)
export?function?mixin(Vue:?VueConstructor)?{
??//?可以看出核心邏輯?就是通過?Vue.mixin?并結(jié)合?hooks?
??//?混入一些初始化?composition-api?的功能邏輯
??//?functionApiInit??updateTemplateRef?主要這兩個(gè)核心函數(shù)的插入
??//?可以看出來,結(jié)合?hooks?機(jī)制,侵入性并不強(qiáng),不會(huì)影響到原有的?Vue2?功能的正常使用
??Vue.mixin({
????beforeCreate:?functionApiInit,
????mounted(this:?ComponentInstance)?{
??????updateTemplateRef(this)
????},
????updated(this:?ComponentInstance)?{
??????updateTemplateRef(this)
????},
??})
??//?...
??
??//?其實(shí)?functionApiInit?做的事情很簡單,
??//?如果?vm.$options?中存在?setup,?render?就復(fù)寫?setup,?render?做一些處理
??function?functionApiInit(this:?ComponentInstance)?{
????const?vm?=?this
????const?$options?=?vm.$options
????const?{?setup,?render?}?=?$options
????//?如果存在?render?函數(shù),復(fù)寫?$options.render
????if?(render)?{
??????//?keep?currentInstance?accessible?for?createElement
??????$options.render?=?function?(...args:?any):?any?{
????????//?activateCurrentInstance?維護(hù)當(dāng)前?vm,?并執(zhí)行?render-fn
????????return?activateCurrentInstance(vm,?()?=>?render.apply(this,?args))
????????//?這里列出來?activateCurrentInstance?函數(shù)的具體邏輯
??????????/*?
??????????//?維護(hù)全局的?currentInstance?對(duì)象,?
??????????//?讓?setup、render?的執(zhí)行始終是在正確的?vm?對(duì)象(必須要維護(hù)當(dāng)前執(zhí)行的組件實(shí)例,因?yàn)闆]有了?this)
??????????function?activateCurrentInstance(vm,?fn)?{
????????????let?preVm?=?getCurrentInstance()
????????????setCurrentVM(vm)
????????????try?{
??????????????return?fn(vm)
????????????}?catch?(err)?{}?finally?{
??????????????setCurrentVM(preVm)
????????????}
??????????}
??????????*/
??????}
????}
????if?(!setup)?{
??????return
????}
????if?(typeof?setup?!==?'function')?{
??????return
????}
????const?{?data?}?=?$options
????//?wrapper?the?data?option,?so?we?can?invoke?setup?before?data?get?resolved
????//?把?this.data?復(fù)寫,?引入?initSetup()
????$options.data?=?function?wrappedData()?{
??????//?核心功能函數(shù),?初始化注冊(cè)?setup?
??????initSetup(vm,?vm.$props)
??????return?typeof?data?===?'function'
??????????data.call(vm,?vm)
????????:?data?||?{}
????}
??}
??//?最最核心的邏輯之一
??function?initSetup(vm:?ComponentInstance,?props:?Record<any,?any>?=?{})?{
????const?setup?=?vm.$options.setup!
????//?創(chuàng)建?setup?上下文對(duì)象?,因?yàn)?setup?本身也可以接受一些?vm?實(shí)例的參數(shù)
????const?ctx?=?createSetupContext(vm)
????//?mark?props?as?reactive
????markReactive(props)
????//?resolve?scopedSlots?and?slots?to?functions
????resolveScopedSlots(vm,?ctx.slots)
????let?binding
????//?同樣的,涉及到?setup的執(zhí)行,需要維護(hù)全局的?currentInstance?對(duì)象
????activateCurrentInstance(vm,?()?=>?{
??????//?setup?函數(shù)執(zhí)行后,如果有返回,并且是響應(yīng)式對(duì)象,是需要在?view?層?template?中處理
??????binding?=?setup(props,?ctx)
????})
????if?(!binding)?return
????//?如果?binding?是?對(duì)象則進(jìn)行處理
????if?(isPlainObject(binding))?{
??????const?bindingObj?=?binding
??????//?vm.__secret_vfa_state__[rawBindings]?=?binding
??????vmStateManager.set(vm,?'rawBindings',?binding)
??????//?遍歷?binding?對(duì)象?keys
??????Object.keys(binding).forEach((name)?=>?{
????????let?bindingValue?=?bindingObj[name]
????????//?如果?binding[key]?不是響應(yīng)式的,?需要進(jìn)一步響應(yīng)式處理,
????????//?因?yàn)樾枰S護(hù)?view?層變更,?也就是響應(yīng)式系統(tǒng)的雙向綁定關(guān)系
????????//?only?make?primitive?value?reactive
????????if?(!isRef(bindingValue))?{
??????????//?...
??????????//?ref?這不是?vue3?提出來的嗎,怎么vue2?也能用
??????????bindingValue?=?ref(bindingValue)
??????????//?...
????????}
????????//?如果?name?不存在?vm?中,?并且也沒有?vm.$options.props[name]
????????//?則進(jìn)行代理處理?proxy(vm,?name,?{get,?set}),proxy?即?Object.defineProperty
????????asVmProperty(vm,?name,?bindingValue)
??????})
??????return
????}
??}
??//?這里不詳細(xì)介紹,不是本篇重點(diǎn)
??function?updateTemplateRef()?{
????//?...
??}
}
下面來看看 ref / reactive 這些 vue3 的新語法功能 為什么 vue2 中也能進(jìn)行使用
?預(yù)備知識(shí): Object.seal(obj)方法封閉一個(gè)對(duì)象, 阻止添加新屬性并將所有現(xiàn)有屬性標(biāo)記為不可配置。當(dāng)前屬性的值只要原來是可寫的就可以改變。obj 是將要被密封的對(duì)象,返回一個(gè) 被密封的對(duì)象。
?
//?來看看?ref?干了什么
export?function?ref(raw?:?unknown)?{
??if?(isRef(raw))?{
????return?raw
??}
??//?利用?reactive?函數(shù)生成響應(yīng)式對(duì)象
??const?value?=?reactive({?[RefKey]:?raw?})
??//?利用?createRef?返回?ref?對(duì)象
??return?createRef({
????get:?()?=>?value[RefKey]?as?any,
????set:?(v)?=>?((value[RefKey]?as?any)?=?v),
??})
}
//?createRef?函數(shù)
export?function?createRef<T>(options:?RefOption )?{
??//?seal?the?ref,?this?could?prevent?ref?from?being?observed
??//?It's?safe?to?seal?the?ref,?since?we?really?shouldn't?extend?it.
??return?Object.seal(new?RefImpl(options))
??//?RefImpl?類具體內(nèi)容如下,會(huì)初始化?value?屬性,并在構(gòu)造函數(shù)中進(jìn)行?proxy?處理,
??//?上面也提到了?proxy?就是?Object.defineProperty
??//?當(dāng)然了,?在?vue3?中是基于?Proxy?api?實(shí)現(xiàn)的,在?vue2?中則是基于?Object.defineProperty?實(shí)現(xiàn)
????/*
????class?RefImpl?implements?Ref?{
??????readonly?[_refBrand]!:?true
??????public?value!:?T
??????constructor({?get,?set?}:?RefOption)?{
????????proxy(this,?'value',?{
??????????get,
??????????set,
????????})
??????}
????}
????*/
}
//?reactivity?函數(shù)
//?Make?obj?reactivity
export?function?reactive<T?extends?object>(obj:?T):?UnwrapRef<T>?{
??if?(
????!isPlainObject(obj)?||
????isReactive(obj)?||
????isRaw(obj)?||
????!Object.isExtensible(obj)
??)?{
????return?obj
??}
??//?observe?函數(shù)?即?Vue.observable(obj)?用于初始化構(gòu)建響應(yīng)式對(duì)象,vue2.6?源碼中的?api
??//?具體細(xì)節(jié)見?vue/src/core/global-api/index.js
??const?observed?=?observe(obj)
??//?Object.defineProperty(obj,?ReactiveIdentifierKey,?ReactiveIdentifier);
??//?markReactive(obj)
??//?setupAccessControl(observed)
??return?observed?
}
看到這里, 再回頭想一想剛剛提到的三個(gè)問題:
install 主要干了什么? setup 中為什么可以隨意使用 composition-api,并脫離了 this? 基于 vue2 的 reactive / ref 是怎么實(shí)現(xiàn)的?
現(xiàn)在是不是已經(jīng)知道答案了呢。
其實(shí)這些問題本身并不難,難的是能不能花心思和精力去進(jìn)行專研,思考。
@vue/composition-api 的優(yōu)勢與劣勢
最后,來看看 基于 Vue2 的 composition-api 有哪些優(yōu)缺點(diǎn)。優(yōu)點(diǎn)其實(shí)上面也已經(jīng)提到了,這里主要看一下缺點(diǎn)。
composition-api 的使用限制[2] 不能在數(shù)組中使用含有 ref 的普通對(duì)象。在數(shù)組中,應(yīng)該總是將 ref 存放到 reactive 對(duì)象中 reactive() 會(huì)返回一個(gè)修改過的原始的對(duì)象。此行為與 Vue 2 中的 Vue.observable 一致。在 Vue 3 中,reactive() 會(huì)返回一個(gè)新的的代理對(duì)象 watch 中不支持 ?onTrack 和 onTrigger 選項(xiàng) Vue 3 新引入的 API ,在本插件中暫不適用: onRenderTrackedonRenderTriggeredisProxy在 data() 中使用 ref, reactive 或其他組合式 API 將不會(huì)生效 emit 選項(xiàng), emit 僅因在類型定義中對(duì)齊 Vue3 的選項(xiàng)而提供,不會(huì)有任何效果。 性能影響 由于 Vue 2 的公共 API 的限制,@vue/composition-api 不可避免地引入了額外的性能開銷
至此,對(duì)于 @vue/composition-api 先介紹到這里,如果還有什么疑問或者想討論的,后臺(tái)回復(fù) 好友 即可加筆者微信。
Reference
1: https://github.com/vuejs/composition-api-rfc/blob/master/index.md
[2]2: https://github.com/vuejs/composition-api/blob/main/README.md
