这个库是在使用cube-ui
时注意到的,一般用于C端创建modal
/dialog
会很方便[https://didi.github.io/cube-ui/#/zh-CN/docs/create-api]
有助于理解函数式组件调用的实现思路。
从入口文件src/index.js
内进入,vue-create-api
是一个Vue
插件,因此需要提供一个install
的方法来挂载。
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
}
}
options
提供了两个参数componentPrefix
和apiPrefix
,前者用于告知组件的前缀用于后续更方便的转化api
名称,后者则用于自定义api
的前缀,如$create-dialog
最后会被转化为$createDialog
。
在install
内就做了一个操作,向Vue
实例添加了一个createAPI
方法,在文档中createAPI
支持传入两个参数的简写方法,内部会对events
进行布尔值类型判断,随后调用apiCreator
来创建组件,在通过 processComponentName
处理完组件的名称后会将创建组件的方法挂载到Vue
和组件的$create
属性,也就是调用this.$xxx
时实际就是调用api.create
。
const eventBeforeDestroy = 'hook:beforeDestroy'
export default function apiCreator(Component, events = [], single = false) {
let Vue = this
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
}
既然调用this.$xxx
时实际就是调用api.create
,那么就可以直接从api.create
开始。
进入方法内部,首先会对renderFn
和_single
做一下转化,随后声明ownerInstance
和isInVueInstance
,ownerInstance
只是为了保存当前的实例,有可能是组件,也有可能是Vue
组件实例,因为可以用this.$xxx
和Component.$create
来创建组件。
若当前为Vue
组件实例,则将其作为parent
属性保存在options
中。
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
和event
(分别是createAPI
的第一、二个参数)进行转化,将会被浅拷贝后放在props
中,event
则会被转化为{ name: onName }
的形式放在on
内,最后返回{ props, on }
在得到parseRenderData
的返回值renderData
后会调用processProps
和processEvents
来分别处理renderData.props
内可能存在的$props
和$events
属性,具体的处理规律在官方的文档内已给出。
$props
$events
随后会调用process$
函数来处理以$
作为开头的属性,这些属性最后也会被用于实例上。在属性都处理完成后调用createComponent
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()
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
}
}
return component
}
在createComponent
内,主要是对instantiateComponent
得到的组件做一些拓展和单例限制,这样可以多次调用但是只创建一次。
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()
if (this.$el && this.$el.parentNode === document.body) {
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
的render
方法来创建组件,并把$el
挂载到body
下,还支持传入renderFn
来定义组件内的插槽。
最后回到api.create
内,若为Vue
组件实例时还会在hook:beforeDestroy
内注册一个事件,在实例销毁时也会一同销毁。
如果只是单纯的想使用函数式创建组件那么只需按照其挂载的思路使用就够了。
从Vant
源码库内找到个工具函数可以基本满足这个需求
// vue3.x
export function mountComponent(RootComponent: Component) {
const app = createApp(RootComponent)
const root = document.createElement('div')
document.body.appendChild(root)
return {
instance: app.mount(root),
unmount() {
app.unmount()
document.body.removeChild(root)
}
}
}
这个库是在使用cube-ui
时注意到的,一般用于C端创建modal
/dialog
会很方便[https://didi.github.io/cube-ui/#/zh-CN/docs/create-api]
有助于理解函数式组件调用的实现思路。
从入口文件src/index.js
内进入,vue-create-api
是一个Vue
插件,因此需要提供一个install
的方法来挂载。
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
}
}
options
提供了两个参数componentPrefix
和apiPrefix
,前者用于告知组件的前缀用于后续更方便的转化api
名称,后者则用于自定义api
的前缀,如$create-dialog
最后会被转化为$createDialog
。
在install
内就做了一个操作,向Vue
实例添加了一个createAPI
方法,在文档中createAPI
支持传入两个参数的简写方法,内部会对events
进行布尔值类型判断,随后调用apiCreator
来创建组件,在通过 processComponentName
处理完组件的名称后会将创建组件的方法挂载到Vue
和组件的$create
属性,也就是调用this.$xxx
时实际就是调用api.create
。
const eventBeforeDestroy = 'hook:beforeDestroy'
export default function apiCreator(Component, events = [], single = false) {
let Vue = this
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
}
既然调用this.$xxx
时实际就是调用api.create
,那么就可以直接从api.create
开始。
进入方法内部,首先会对renderFn
和_single
做一下转化,随后声明ownerInstance
和isInVueInstance
,ownerInstance
只是为了保存当前的实例,有可能是组件,也有可能是Vue
组件实例,因为可以用this.$xxx
和Component.$create
来创建组件。
若当前为Vue
组件实例,则将其作为parent
属性保存在options
中。
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
和event
(分别是createAPI
的第一、二个参数)进行转化,将会被浅拷贝后放在props
中,event
则会被转化为{ name: onName }
的形式放在on
内,最后返回{ props, on }
在得到parseRenderData
的返回值renderData
后会调用processProps
和processEvents
来分别处理renderData.props
内可能存在的$props
和$events
属性,具体的处理规律在官方的文档内已给出。
$props
$events
随后会调用process$
函数来处理以$
作为开头的属性,这些属性最后也会被用于实例上。在属性都处理完成后调用createComponent
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()
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
}
}
return component
}
在createComponent
内,主要是对instantiateComponent
得到的组件做一些拓展和单例限制,这样可以多次调用但是只创建一次。
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()
if (this.$el && this.$el.parentNode === document.body) {
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
的render
方法来创建组件,并把$el
挂载到body
下,还支持传入renderFn
来定义组件内的插槽。
最后回到api.create
内,若为Vue
组件实例时还会在hook:beforeDestroy
内注册一个事件,在实例销毁时也会一同销毁。
如果只是单纯的想使用函数式创建组件那么只需按照其挂载的思路使用就够了。
从Vant
源码库内找到个工具函数可以基本满足这个需求
// vue3.x
export function mountComponent(RootComponent: Component) {
const app = createApp(RootComponent)
const root = document.createElement('div')
document.body.appendChild(root)
return {
instance: app.mount(root),
unmount() {
app.unmount()
document.body.removeChild(root)
}
}
}