@vue/composition-api 解析

做者:周超html

前言

組合式 API 是 vue3 提出的一個新的開發方式,而在 vue2 中咱們可使用新的組合式 API 進行組件開發。本篇經過一個例子,來分析這個插件是如何提供功能。vue

關於該插件的安裝、使用,能夠直接閱讀文檔。react

安裝

咱們從最開始安裝分析,一探究竟。git

vue.use

按照文檔所提到的,咱們必須經過 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),
}
複製代碼

install

經過上面的代碼和 Vue.use 可知,咱們安裝時其實就是調用了 install 方法,先分析一波 install。根據代碼塊及功能能夠分紅三個部分:markdown

  1. 前兩個大 if 的開發 check 部分
  2. 關於 setup 合併策略
  3. 經過 mixin 混入插件關於 組合式 API 的處理邏輯

第一部分中的第一個 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)
複製代碼

報錯:

image2021-6-23_16-38-4.png

第二部分的合併策略是「Vue.config.optionMergeStrategies」這個代碼塊。Vue 提供的這個能力很生僻,咱們平常的開發中幾乎不會主動接觸到。先上文檔

image2021-6-23_16-50-30.png

這是用來定義屬性的合併行爲。好比例子中的 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個處理邏輯:

  1. 普通屬性的 get 和 set 正常返回
  2. 若是是 ref 類型的屬性(經過 ref 建立),經過 vm.xxx 訪問/修改時,訪問/修改 ref 的 value 屬性
  3. 代理 reactive 類型的屬性 (經過 reactive 建立),reactive 返回的是一個響應式對象。當訪問這個對象時, 須要調用 響應式對象種的 depend 收集watcher(觀察者),以便數據更新時通知 watcher 進行更新。

總之 asVmProperty 是拿到 setup 返回值中的一個鍵值對後,再經過 Object.defineProperty 劫持了 this(是vm,也就是組件實例)中訪問改鍵值對的 get 和 set,這樣咱們即可以經過 this.xxx 訪問到 setup 中return 出去的屬性。

而模版訪問也同理,由於 template 編譯成 render 後,上面的變量都實際會編譯成 _vm.xxx,而 _vm 就是 this ,也就是組件實例。

相關文章
相關標籤/搜索