做者:周超html
組合式 API 是 vue3 提出的一個新的開發方式,而在 vue2 中咱們可使用新的組合式 API 進行組件開發。本篇經過一個例子,來分析這個插件是如何提供功能。vue
關於該插件的安裝、使用,能夠直接閱讀文檔。react
咱們從最開始安裝分析,一探究竟。git
按照文檔所提到的,咱們必須經過 Vue.use() 進行安裝:github
// vue.use 安裝
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
複製代碼
咱們先看入口文件:typescript
// 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 對象。api
// 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),
}
複製代碼
經過上面的代碼和 Vue.use 可知,咱們安裝時其實就是調用了 install 方法,先分析一波 install。根據代碼塊及功能能夠分紅三個部分:markdown
第一部分中的第一個 if 是爲了確保該 install 方法只被調用一次,避免浪費性能;第二個 if 則是確保vue版本爲2.x。不過這裏有個關於第一個if的小問題:屢次註冊插件時,Vue.use 本身自己會進行重複處理——安裝過的插件再次註冊時,不會調用 install 方法(Vue.use代碼見下)。那麼這個 if 的目的是啥?app
// 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 會判斷重複後,拋出錯誤ide
// 直接調用 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) 也到此結束。
咱們知道在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 中。咱們接下來看這個方法。
這塊是經過 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 的其餘選項中使用。那麼這塊是如何實現的呢?
這個函數執行後,咱們就能夠在 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個處理邏輯:
總之 asVmProperty 是拿到 setup 返回值中的一個鍵值對後,再經過 Object.defineProperty 劫持了 this(是vm,也就是組件實例)中訪問改鍵值對的 get 和 set,這樣咱們即可以經過 this.xxx 訪問到 setup 中return 出去的屬性。
而模版訪問也同理,由於 template 編譯成 render 後,上面的變量都實際會編譯成 _vm.xxx,而 _vm 就是 this ,也就是組件實例。