在 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
插件,安裝插件時,能夠設置 componentPrefix
和 apiPrefix
兩個參數,這裏會在 Vue 構造器下添加一個 createAPI
方法。引入 Dialog 組件,調用 createAPI
生產對應 API
,並掛載到 Vue.prototype
和 Dialog
對象上。以後能夠在 vue 組件中經過 this
調用,或者在 js 文件中 $create
建立並使用。git
文件名稱 | 說明 |
---|---|
creator | 建立組件 |
debug | 錯誤提示 |
index | 主入口 |
instantiate | 實例化 |
parse | 參數設置 |
util | 工具庫 |
接下來咱們會從 入口 開始分析,深刻了解它的原理及實現過程。github
若是 Vue 插件是一個對象,必須提供 install
方法。若是插件是一個函數,該函數會被做爲 install
方法。 install
方法調用時,會將 Vue 做爲參數傳入。 vue-create-api
的 install
方法在 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 統一添加前綴,默認爲 $create
。api
在方法體內定義了 Vue.createAPI 方法,並提供三個參數 Component
組件、 events
事件數組、 single
是否採用單例模式實例化組件。 events
能夠傳 Boolean 類型或者 Array 類型值。 示例中 events
爲 true ,根據代碼邏輯,當 events
爲 Boolean 類型時, single = events 因此 single
爲 true ,events
賦值爲 []。數組
經過 apiCreator
方法得到 api
對象,內部有 before
和 create
兩個方法。 這裏之因此用到 call
,其做用就是要將 this
指向 Vue 類。代碼文件路徑在 src/creator.js
,這部分實現邏輯以後會細講,咱們接着往下看。服務器
經過 processComponentName
方法得到 crateName
屬性名,將 api.create
賦給 Component.$create
和 Vue.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
。
入口文件分析完了,接下來咱們看一下 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
方法,進行初始化,如 生命週期、事件、渲染等等。
講回來,定義一些變量 currentSingleComp
、 singleMap
、 beforeHooks
這三個做用以後會講到。咱們先看一下 const api
都定義了什麼,它提供了 before
和 create
兩個方法。
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
方法得到渲染數據,該方法如何實現後面介紹。
processProps
、processEvents
、process$
三個方法分別監聽參數、事件以及參數對象,這些方法如何實現後面介紹。
createComponent
方法建立了組件的實例,最後返回該示例。其中有一段代碼須要注意,以下
if (isInVueInstance) {
ownerInstance.$on(eventBeforeDestroy, beforeDestroy)
}
function beforeDestroy() {
cancelWatchProps(ownerInstance)
component.remove()
component = null
}
複製代碼
判斷組件是否在 Vue 中使用,在的話,爲其綁定一個 beforeDestroy
事件鉤子,清空並銷燬監聽的事件屬性和實例。
接下來咱們會逐步分析解析渲染數據、事件屬性監聽以及建立組件是如何實現的。
文件路徑在 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
屬性,該方法纔會繼續往下執行。
watchKeys
、watchPropKeys
存放須要監聽更新的數據 和 參數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
})
}
}
複製代碼
該方法主要的監聽用戶綁定的回調事件使其觸發。它接受兩個參數 renderData
和 ownerInstance
。
首先判斷渲染數據中是否有提供 $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
對象中,值爲一個對象,在上有說道 comp
和 ins
, comp
對應的是 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
}
複製代碼
這裏爲組件添加了三個方法,分別爲 remove
、show
、hide
。
remove
:判斷當前是否爲單例,將 singleMap
中對應的值刪除。判斷組件是否設置了 remove
方法,使用 apply
方法執行,最後將父實例銷燬。
show
和 hide
兩個方法差很少,目的是將當前組件實例返回。
接下來分析 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()
複製代碼
接下來定義了兩個方法 init
和 destory
。 init方法將 Vue 實例使用的根 DOM 元素添加到body中,destory方法將其刪除銷燬。
updateRenderData
爲更新渲染數據。
$mount
手動地掛載一個未掛載的實例。也就是說不調用該方法,Vue 實例中無 $el
。
instance.$children[0]
獲取組件實例,綁定 $updateProps
方法,最終返回該組件實例。
到這裏,vue-create-api 插件的核心代碼以及整個運轉過程都講完了。之因此分享該插件源碼分析有兩個重要緣由。
1、做者是黃軼,本人閱讀過黃老師的「Vue.js技術揭祕」學到不少知識,這個插件也是黃老師親力親爲而作,算是慕名而來。
2、代碼自己,通讀源碼能夠看到做者思路清晰,實現過程毫無拖泥帶水,語言精練值得反覆閱讀。
最後附上該文章的博客導讀版 方便你們收藏閱讀。或者掃碼關注微信公衆號【難以想象的前端】,每月會技術乾貨、最佳實踐以及有趣的前端技巧分享給你們。