【源碼解析】vue-create-api做者黃軼

vue-create-api 是幹嗎的?

在 README.md 中這樣介紹的,一個可以讓 Vue 組件經過 API 方式調用的插件。( vue-create-api 源碼地址 )html

安裝使用

目前提供兩種安裝,經過 npm install vue-create-api, 或者引入js靜態資源文件。前端

在 README.md 中提供了使用示例,以下:vue

import CreateAPI from 'vue-create-api'

Vue.use(CreateAPI)

Vue.use(CreateAPI, {
  componentPrefix: 'cube-'
  apiPrefix: '$create-'
})

import Dialog from './components/dialog.vue'

Vue.createAPI(Dialog, true)

Dialog.$create({
  $props: {
    title: 'Hello',
    content: 'I am from pure JS'
  }
}).show()

this.$createDialog({
  $props: {
    title: 'Hello',
    content: 'I am from a vue component'
  }
}).show()
複製代碼

引入 vue-create-api 插件,安裝插件時,能夠設置 componentPrefixapiPrefix 兩個參數,這裏會在 Vue 構造器下添加一個 createAPI 方法。引入 Dialog 組件,調用 createAPI 生產對應 API,並掛載到 Vue.prototypeDialog 對象上。以後能夠在 vue 組件中經過 this 調用,或者在 js 文件中 $create 建立並使用。git

目錄

文件名稱 說明
creator 建立組件
debug 錯誤提示
index 主入口
instantiate 實例化
parse 參數設置
util 工具庫

接下來咱們會從 入口 開始分析,深刻了解它的原理及實現過程。github

入口

若是 Vue 插件是一個對象,必須提供 install 方法。若是插件是一個函數,該函數會被做爲 install 方法。 install 方法調用時,會將 Vue 做爲參數傳入。 vue-create-apiinstall 方法在 src/index.js 文件中定義:npm

import { camelize, escapeReg, isBoolean } from './util'
import { assert, warn } from './debug'
import apiCreator from './creator'
import instantiateComponent from './instantiate'

function install(Vue, options = {}) {
  const {componentPrefix = '', apiPrefix = '$create-'} = options

  Vue.createAPI = function (Component, events, single) {
    if (isBoolean(events)) {
      single = events
      events = []
    }
    const api = apiCreator.call(this, Component, events, single)
    const createName = processComponentName(Component, {
      componentPrefix,
      apiPrefix,
    })
    Vue.prototype[createName] = Component.$create = api.create
    return api
  }
}
複製代碼

install 方法提供 options 配置參數, componentPrefix 爲組件名前綴,最終生成的 API 會忽略該前綴, apiPrefix 爲生成的 API 統一添加前綴,默認爲 $createapi

在方法體內定義了 Vue.createAPI 方法,並提供三個參數 Component 組件、 events 事件數組、 single 是否採用單例模式實例化組件。 events 能夠傳 Boolean 類型或者 Array 類型值。 示例中 events 爲 true ,根據代碼邏輯,當 events 爲 Boolean 類型時, single = events 因此 single 爲 true ,events 賦值爲 []。數組

經過 apiCreator 方法得到 api 對象,內部有 beforecreate 兩個方法。 這裏之因此用到 call,其做用就是要將 this 指向 Vue 類。代碼文件路徑在 src/creator.js ,這部分實現邏輯以後會細講,咱們接着往下看。服務器

經過 processComponentName 方法得到 crateName 屬性名,將 api.create 賦給 Component.$createVue.prototype[createName],最後返回 api。這裏也就是上面示例中 this.$createDialog()Dialog.$create() 的實現過程。微信

processComponentName 方法很是簡單,代碼以下:

function processComponentName(Component, options) {
  const {componentPrefix, apiPrefix} = options
  const name = Component.name
  assert(name, 'Component must have name while using create-api!')
  const prefixReg = new RegExp(`^${escapeReg(componentPrefix)}`, 'i')
  const pureName = name.replace(prefixReg, '')
  let camelizeName = `${camelize(`${apiPrefix}${pureName}`)}`
  return camelizeName
}
複製代碼

這段代碼目的就是匹配剪切拼接字符串,最終返回處理好的 camelizeName 值,須要注意一下這裏有用到 Component.name,而且判斷 name 是否認義,未定義則拋出異常,因此用 vue-create-api 插件的話,組件必定要定義 name

建立API

入口文件分析完了,接下來咱們看一下 apiCreator 作了什麼操做,文件路徑爲 src/creator.js,代碼比較多,爲了閱讀方便,我按照主要邏輯分段講解:

import instantiateComponent from './instantiate'
import parseRenderData from './parse'
import { isFunction, isUndef, isStr } from './util'

const eventBeforeDestroy = 'hook:beforeDestroy'

export default function apiCreator(Component, events = [], single = false) {
  let Vue = this
  let currentSingleComp
  let singleMap = {}
  const beforeHooks = []

  ...

  const api = {
    before(hook) {
      beforeHooks.push(hook)
    },
    create(config, renderFn, _single) {
      if (!isFunction(renderFn) && isUndef(_single)) {
        _single = renderFn
        renderFn = null
      }

      if (isUndef(_single)) {
        _single = single
      }

      const ownerInstance = this
      const isInVueInstance = !!ownerInstance.$on
      let options = {}

      if (isInVueInstance) {
        // Set parent to store router i18n ...
        options.parent = ownerInstance
        if (!ownerInstance.__unwatchFns__) {
          ownerInstance.__unwatchFns__ = []
        }
      }

      const renderData = parseRenderData(config, events)

      let component = null

      processProps(ownerInstance, renderData, isInVueInstance, (newProps) => {
        component && component.$updateProps(newProps)
      })
      processEvents(renderData, ownerInstance)
      process$(renderData)

      component = createComponent(renderData, renderFn, options, _single)

      if (isInVueInstance) {
        ownerInstance.$on(eventBeforeDestroy, beforeDestroy)
      }

      function beforeDestroy() {
        cancelWatchProps(ownerInstance)
        component.remove()
        component = null
      }

      return component
    }
  }

  return api
}
複製代碼

這個js文件是 vue-create-api 的核心文件,這裏麪包含着解析渲染數據、事件屬性監聽和建立組件等操做,這些我會一一分析給你們。

apiCreator 函數有三個參數,分別爲 Component,events,single。這同 createAPI 一致。首先 Vue = this,這裏的 this 指向是 Vue 這個類,vue 源碼在 src/core/instance/index.js 中,以下所示:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
複製代碼

咱們平時開發中 new Vue 操做,就是實例化這個對象方法。在方法體內,執行 this._init 方法,進行初始化,如 生命週期、事件、渲染等等。

講回來,定義一些變量 currentSingleCompsingleMapbeforeHooks 這三個做用以後會講到。咱們先看一下 const api 都定義了什麼,它提供了 beforecreate 兩個方法。

before 提供了一個參數 hook ,它就是一個鉤子函數,在方法體內用到了一開始定義的 beforeHooks 數組,將 hook 添加到該數組。根據名稱定義咱們能夠猜到,這些函數會在組件初始化的時候就定義好,該方法能夠用於某種限制設定。

create 提供了三個參數,分別爲 config 配置參數、 renderFn 用於生成子 VNode 節點, _single 單例。接下來判斷 renderFn 是否爲函數,若是 renderFn 不爲函數而且 _single 爲 undefined 時,_single = renderFn,renderFn = null,若是 _single 爲 undefined 時,_single = single。

const ownerInstance = this 這裏的 this 上下文指向的是調用者。舉個例子 this.$createDialog() this 指向的就是 vue 實例,若使用 Dialog.$create() 方法時,this 指向的就是 Dialog 對象,前者 isInVueInstance 爲 true,後者爲 false。 ownerInstance.__unwatchFns__ 用做監聽 Prop 變化。因此這裏當用 Dialog.$create() 這樣的形式建立組件的實例並使用時,沒法讓 Prop 響應式更新。

經過 parseRenderData 方法得到渲染數據,該方法如何實現後面介紹。

processPropsprocessEventsprocess$ 三個方法分別監聽參數、事件以及參數對象,這些方法如何實現後面介紹。

createComponent 方法建立了組件的實例,最後返回該示例。其中有一段代碼須要注意,以下

if (isInVueInstance) {
  ownerInstance.$on(eventBeforeDestroy, beforeDestroy)
}

function beforeDestroy() {
  cancelWatchProps(ownerInstance)
  component.remove()
  component = null
}
複製代碼

判斷組件是否在 Vue 中使用,在的話,爲其綁定一個 beforeDestroy 事件鉤子,清空並銷燬監聽的事件屬性和實例。

  • 注意:若是是服務器渲染(SSR)的話,該方法會無效。

接下來咱們會逐步分析解析渲染數據事件屬性監聽以及建立組件是如何實現的。

解析渲染數據

文件路徑在 src/parse.js,代碼以下:

import { camelize } from './util'

export default function parseRenderData(data = {}, events = {}) {
  events = parseEvents(events)
  const props = {...data}
  const on = {}
  for (const name in events) {
    if (events.hasOwnProperty(name)) {
      const handlerName = events[name]
      if (props[handlerName]) {
        on[name] = props[handlerName]
        delete props[handlerName]
      }
    }
  }
  return {
    props,
    on
  }
}

function parseEvents(events) {
  const parsedEvents = {}
  events.forEach((name) => {
    parsedEvents[name] = camelize(`on-${name}`)
  })
  return parsedEvents
}
複製代碼

該方法提供兩個參數,第一個參數 data 在建立組件時傳遞。第二個參數爲 events 在調用 createAPI 時定義。

說一下 data 這個參數有兩種形式。

第一種傳值方式爲 { $props, $events }$props 對應的組件的 prop 參數,該屬性會被 watch,因此支持響應更新。$events 爲組件的事件回調。舉個實例:

this.$createDialog({
  $props: {
    title: 'Hello',
    content: 'I am from a vue component'
  },
  $event: {
    change: () => {}
  }
}).show()
複製代碼

第二種傳值方式能夠將 $props 裏的參數直接放在對象裏,如 { title, content },若這種結構想要監聽事件怎麼辦?

請看源碼中有 parseEvents 方法,該方法傳 events 參數,該參數在 createAPI 中定義,會返回一個對象,key 爲 events 的值,value 爲 camelize(on-${name})。循環 events 判斷是否在 data 中有定義 on* 開頭的參數,若是匹配成功,賦值到 on 對象,並與 props 一同返回。

因此若是想要用第二種方式監聽事件,就以下定義:

Vue.createAPI(Dialog, ['change'])

this.$createDialog({
  title: 'Hello',
  content: 'I am from a vue component',
  onChange: () => {}
}).show()
複製代碼
  • 注意:這段代碼大部分是爲了支持配置 on* 事件監聽。若是使用者沒有這樣的需求的話,能夠優化掉這裏。

事件屬性監聽

文件路徑依然在 src/creator.js,先講 processProps 方法,代碼以下:

function processProps(ownerInstance, renderData, isInVueInstance, onChange) {
    const $props = renderData.props.$props
    if ($props) {
      delete renderData.props.$props

      const watchKeys = []
      const watchPropKeys = []
      Object.keys($props).forEach((key) => {
        const propKey = $props[key]
        if (isStr(propKey) && propKey in ownerInstance) {
          // get instance value
          renderData.props[key] = ownerInstance[propKey]
          watchKeys.push(key)
          watchPropKeys.push(propKey)
        } else {
          renderData.props[key] = propKey
        }
      })
      if (isInVueInstance) {
        const unwatchFn = ownerInstance.$watch(function () {
          const props = {}
          watchKeys.forEach((key, i) => {
            props[key] = ownerInstance[watchPropKeys[i]]
          })
          return props
        }, onChange)
        ownerInstance.__unwatchFns__.push(unwatchFn)
      }
    }
  }
複製代碼

該方法主要目的作數據響應及存儲,它接收四個參數,ownerInstance 建立者實例對象,renderData 渲染的數據對象,isInVueInstance 判斷是否在 vue 組件內被建立, 以及 onChange 一個回調函數。

首先判斷渲染數據中是否有提供 $props,因此當使用者設置了 $props 屬性,該方法纔會繼續往下執行。

watchKeyswatchPropKeys 存放須要監聽更新的數據 和 參數key。循環遍歷 $props 的 key,並獲取對應 key 的 value 值爲 propKey。接下來有一個重要的判斷條件 isStr(propKey) && propKey in ownerInstance,判斷 propKey 是否爲字符串和該屬性是否在 ownerInstance 對象或其原型鏈中。若是成立,將實例的對應的值存入 renderData 中,而且將 key 存入 watch 數組內。

接下來 isInVueInstance 判斷,$watch 監聽數據變化,當 ownerInstance[watchPropKeys[i]] 發生變化時,該函數都會被調用,執行回調函數 $updateProps 方法,該方法定義在 src/instantiate.js 內:

component.$updateProps = function (props) {
    Object.assign(renderData.props, props)
    instance.$forceUpdate()
  }
複製代碼

props 爲更新後的新數據,$forceUpdate 使 Vue 實例從新渲染。

  • 注意:

  • 1)開發者在使用該插件進行數據更新時,須要更新的屬性對應的 value 要爲字符串,並對應着 Vue 實例的數據對象。

  • 2)根據源碼分析,未在 Vue 建立的實例沒法數據更新,這一點在 README 中也有說明。分析源碼後,讓咱們瞭解真正的緣由。

接下來咱們分析 processEvents 方法,代碼以下:

function processEvents(renderData, ownerInstance) {
    const $events = renderData.props.$events
    if ($events) {
      delete renderData.props.$events

      Object.keys($events).forEach((event) => {
        let eventHandler = $events[event]
        if (typeof eventHandler === 'string') {
          eventHandler = ownerInstance[eventHandler]
        }
        renderData.on[event] = eventHandler
      })
    }
  }
複製代碼

該方法主要的監聽用戶綁定的回調事件使其觸發。它接受兩個參數 renderDataownerInstance

首先判斷渲染數據中是否有提供 $events,因此當使用者設置了 $events 屬性,該方法纔會繼續往下執行。

循環遍歷 $events 的 key,並獲取對應 key 的 value 值爲 eventHandler,判斷 eventHandler 是否爲 string 類型,若是爲 string 類型,在實例中獲取該屬性對應的函數並賦給 eventHandler,最後將該函數賦給 renderData

接下來咱們分析 process$ 方法,代碼以下:

function process$(renderData) {
    const props = renderData.props
    Object.keys(props).forEach((prop) => {
      if (prop.charAt(0) === '$') {
        renderData[prop.slice(1)] = props[prop]
        delete props[prop]
      }
    })
  }
複製代碼

該方法提供使用者能夠設置 $xxx 配置,使用起來更靈活,例如想要給組件多設置一個 className 的話,能夠配置爲 $class: 'my-class',方法體內會遍歷參數首位是否爲 $,而後將數據保存在 renderData 中,在以後進行數據處理渲染。

建立組件

文件路徑依然在 src/creator.js,代碼以下:

function createComponent(renderData, renderFn, options, single) {
    beforeHooks.forEach((before) => {
      before(renderData, renderFn, single)
    })
    const ownerInsUid = options.parent ? options.parent._uid : -1
    const {comp, ins} = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {}
    if (single && comp && ins) {
      ins.updateRenderData(renderData, renderFn)
      ins.$forceUpdate()
      currentSingleComp = comp
      return comp
    }
    const component = instantiateComponent(Vue, Component, renderData, renderFn, options)
    const instance = component.$parent
    const originRemove = component.remove

    component.remove = function () {
      if (single) {
        if (!singleMap[ownerInsUid]) {
          return
        }
        singleMap[ownerInsUid] = null
      }
      originRemove && originRemove.apply(this, arguments)
      instance.destroy()
    }

    const originShow = component.show
    component.show = function () {
      originShow && originShow.apply(this, arguments)
      return this
    }

    const originHide = component.hide
    component.hide = function () {
      originHide && originHide.apply(this, arguments)
      return this
    }

    if (single) {
      singleMap[ownerInsUid] = {
        comp: component,
        ins: instance
      }
      currentSingleComp = comp
    }
    return component
  }

複製代碼

該方法接收四個參數,renderData 以前已經處理好須要渲染的數據,renderFn 用於生成子 VNode 節點,options 組件實例,single 是否單例。

beforeHooks.forEach((before) => {
    before(renderData, renderFn, single)
  })
複製代碼

首先循環 beforeHooks 獲取在調用 Vue.createAPI 時綁定的方法,若是設置了 before,那麼每次調用都會先執行這個方法。

const ownerInsUid = options.parent ? options.parent._uid : -1
  const {comp, ins} = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {}
  if (single && comp && ins) {
    ins.updateRenderData(renderData, renderFn)
    ins.$forceUpdate()
    currentSingleComp = comp
    return comp
  }
  const component = instantiateComponent(Vue, Component, renderData, renderFn, options)
  const instance = component.$parent

  ...

  if (single) {
    singleMap[ownerInsUid] = {
      comp: component,
      ins: instance
    }
    currentSingleComp = comp
  }
複製代碼

這部分做用是組件使用單例模式。定義當前實例惟一標識 ownerInsUid,若是 options.parent 存在,獲取 Vue 組件的惟一標識 _uid,反之爲 -1

判斷 singleMap[ownerInsUid] 是否存在,若是存在獲取 comp 和 ins 兩個值。 接下來分別判斷 signle、comp、ins 是否存在或爲 true。

updateRenderData 方法做用是更新渲染數據及回調方法。$forceUpdate 方法使當前實例從新渲染。

instantiateComponent 爲建立一個組件實例的方法,這裏以後細說。

該方法的最後判斷 single 參數,是否爲單例,若是 single 爲 true,以 ownerInsUid 爲鍵存儲到 singleMap 對象中,值爲一個對象,在上有說道 compinscomp 對應的是 component,也就是當前組件的實例,ins 對應的是父實例 component.$parent

const originRemove = component.remove
  component.remove = function () {
    if (single) {
      if (!singleMap[ownerInsUid]) {
        return
      }
      singleMap[ownerInsUid] = null
    }
    originRemove && originRemove.apply(this, arguments)
    instance.destroy()
  }

  const originShow = component.show
  component.show = function () {
    originShow && originShow.apply(this, arguments)
    return this
  }

  const originHide = component.hide
  component.hide = function () {
    originHide && originHide.apply(this, arguments)
    return this
  }
複製代碼

這裏爲組件添加了三個方法,分別爲 removeshowhide

remove:判斷當前是否爲單例,將 singleMap 中對應的值刪除。判斷組件是否設置了 remove 方法,使用 apply 方法執行,最後將父實例銷燬。

showhide 兩個方法差很少,目的是將當前組件實例返回。

接下來分析 instantiateComponent 方法,文件路徑在 src/instantiate.js,代碼以下:

export default function instantiateComponent(Vue, Component, data, renderFn, options) {
  let renderData
  let childrenRenderFn

  const instance = new Vue({
    ...options,
    render(createElement) {
      let children = childrenRenderFn && childrenRenderFn(createElement)
      if (children && !Array.isArray(children)) {
        children = [children]
      }

      return createElement(Component, {...renderData}, children || [])
    },
    methods: {
      init() {
        document.body.appendChild(this.$el)
      },
      destroy() {
        this.$destroy()
        document.body.removeChild(this.$el)
      }
    }
  })
  instance.updateRenderData = function (data, render) {
    renderData = data
    childrenRenderFn = render
  }
  instance.updateRenderData(data, renderFn)
  instance.$mount()
  instance.init()
  const component = instance.$children[0]
  component.$updateProps = function (props) {
    Object.assign(renderData.props, props)
    instance.$forceUpdate()
  }
  return component
}

複製代碼

該方法包含五個參數,Vue 類,Component 組件,data 組件參數及回調事件,renderFn 用於生成子 VNode 節點,options 組件實例。

建立一個 Vue 實例 new Vue。經過解構 options 爲其添加父組件實例。

render 方法爲字符串模板的代替方案,參數 createElement 的做用是建立 VNode。首先判斷 childrenRenderFn 值,它是值爲 renderFn 用於生成子 VNode 節點。若是存在就將 createElement 傳入。最終返回 createElement 方法,若是你對該方法不瞭解的話能夠以後翻閱一下 vue官方文檔。說到 childrenRenderFn 方法,纔可讓該插件有以下配置:

this.$createDialog({
  $props: {
    title: 'Hello',
    content: 'I am from a vue component'
  }
}, createElement => {
  return [
    createElement('p', 'other content')
  ]
}).show()
複製代碼

接下來定義了兩個方法 initdestory。 init方法將 Vue 實例使用的根 DOM 元素添加到body中,destory方法將其刪除銷燬。

updateRenderData 爲更新渲染數據。

$mount 手動地掛載一個未掛載的實例。也就是說不調用該方法,Vue 實例中無 $el

instance.$children[0] 獲取組件實例,綁定 $updateProps 方法,最終返回該組件實例。

總結

到這裏,vue-create-api 插件的核心代碼以及整個運轉過程都講完了。之因此分享該插件源碼分析有兩個重要緣由。

1、做者是黃軼,本人閱讀過黃老師的「Vue.js技術揭祕」學到不少知識,這個插件也是黃老師親力親爲而作,算是慕名而來。

2、代碼自己,通讀源碼能夠看到做者思路清晰,實現過程毫無拖泥帶水,語言精練值得反覆閱讀。

最後附上該文章的博客導讀版 方便你們收藏閱讀。或者掃碼關注微信公衆號【難以想象的前端】,每月會技術乾貨、最佳實踐以及有趣的前端技巧分享給你們。

相關文章
相關標籤/搜索