Skip to content

Latest commit

 

History

History
526 lines (440 loc) · 18.6 KB

createAPI2.md

File metadata and controls

526 lines (440 loc) · 18.6 KB

createAPI源码分析

简单的api式调用组件

简单调用

类似于常见的loading组件,大多数情况是在多个页面都要被调用的。这个时候最适合写成api形式,在需要的地方使用。 通常会简单的像下面这样做:

import Vue from 'vue'
export default function instantiateComponent(Component) {
  const instance = new Vue({
    render(createElement) {
      return createElement(Component)
    },
    methods: {
      init() {
        // Vue 实例使用的根 DOM 元素。
        document.body.appendChild(this.$el)
      }
    }
  })
  instance.$mount()
  instance.init()
// instance是整个vue实例,component是组件实例
  const component = instance.$children[0]
  return component
}

这样在某个组件内,只用像下面这样做

import Loading from 'components/loading.vue'
import instantiateComponent from 'common/js/instantiateComponent'
// ......省略......省略......省略......
// 然后在需要调用的地方使用
instantiateComponent(Loading)

当然这样做会出问题: instantiateComponent方法调用后出现了loading组件,之后就一直存在无法销毁

我们可以给Loading做些修改达到目的:

  1. html包裹层加一个v-if="visible"指令

  2. data属性里添加一个键值对:visible: true

  3. methods里添加两个方法:

hide() { this.visible = false }
show() { this.visible = true }

这样在使用时就可以:

// 显示时
this.instance = instantiateComponent(Loading)
this.instance.show()

// 隐藏时
this.instance && this.instance.hide()

props和events

组件显示的内容当然希望可以在外部控制,做到可配置化。同样的需要暴露一些事件。

所以我们就需要给instantiateComponent函数添加一个和配置相关的参数,让createElement使用。

先改写instantiateComponent

// ....
instantiateComponent(Component, options) {
  // ....
  const instance = new Vue({
    render(createElement) {
      return createElement(Component, { ...options })
    }
  })
  // ....
}
let options = {
  props: { txt: '请稍后..' },
  on: { click: clickHandler } // 假设clickHandler是一个函数
}
instantiateComponent(Loading, options)

这样就已经能满足我们简单的需求了,但功能不全且不够优雅

createAPI方式

createAPI的用法很简单,下面五个步骤简单的对应了下面的五行代码

  1. 引入createAPI模块
  2. 引入需api式调用的组件
  3. 注册createAPI
  4. 生成api
  5. 调用api,实例化组件
import CreateAPI from 'create-api'
import Loading from './components/loading.vue'
Vue.use(CreateAPI)
Vue.createAPI(Loading, true)

// 在某个vue文件中调用
// 假设config是配置项
this.$createDialog(config)

来让我们看一看createAPI究竟是如何工作的:

注册createAPI方法

当我们引入了createAPI后,通过Vue.use注册插件实际上就是运行了插件提供的install方法。

下面代码是install方法:

function install(Vue, options = {}) {
  // options是生成api名的配置项
  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, {
      // processComponentName是对需api调用的组件名字的处理
      componentPrefix,
      apiPrefix,
    })
    Vue.prototype[createName] = Component.$create = api.create
    return api
  }
}

根据文档和上述代码我们能得知:

  1. options是用来生成api名使用的
  2. Vue.use注册createAPI后,会在Vue构造器上添加一个叫createAPI的方法
  3. Vue.createAPI收到参数运行后会在Vue.prototype上添加一个方法

install运行完后Vue上就已经有了createAPI这个方法了,注册完成

生成api

api的生成是运行了Vue.createAPI这个方法,方法的核心是apiCreator,在apiCreator运行后,返回的api.create就是后续我们实例化组件的api

通过apiCreator方法引入的路径可以查看到src/creator.js的文件:

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) {
      //...省略
    }
  }
  return api
}

这个函数内几乎所有东西都是为api对象中的create函数服务。

因为被调用时call关键字强制绑定this,所以apiCreator内的this指向的是Vue构造函数,而变量singleMap是单例模式下的一个存储器,而beforeHooks是一个数组,存储初始化钩子被调用时传递进来的参数。

api对象的 before是一个钩子函数,参数是一个函数,用来在api调用时,组件实例化之前运行传递进来的函数。(文档没有说明)

生成的api的过程实际就是给api.create方法填入一些默认配置的过程。

下面是apiCreator的三个参数

  1. 第一个是需要api形式调用的组件Component
  2. 第二个是该组件向外暴露的事件events
  3. 第三个是否以单例来生成组件single

注意:apiCreator接受的第二个参数events是为了兼容老版本的写法,在新版本中意义不大。所以文档中只说createAPI接受两个参数,从这开始,和文档统一,接收两个参数,第二个参数表示是否单例

api名

函数名很重要(匿名函数、箭头函数: 你说啥)!

通过install方法,可以看到,[createName]processComponentName生成,代码如下

// 第二个参数是根据给Vue.use传递的第二个参数生成
// 其中apiPrefix默认为'$create-'
const createName = processComponentName(Component, {
  componentPrefix,
  apiPrefix,
})

function processComponentName(Component, options) {
  // 假设componentPrefix为'cube-',apiPrefix为'$create-'
  const {componentPrefix, apiPrefix} = options
  const name = Component.name // => 'cube-dialog'
  
  // 把componentPrefix里面的特殊字符加'/'形成正则
  const prefixReg = new RegExp(`^${escapeReg(componentPrefix)}`, 'i') // => /^cube\-/i
  
  // 把name的其实位置和prefixReg正则匹配,满足则删除
  const pureName = name.replace(prefixReg, '') // => 'dialog'
  
  // 把apiPrefix和name连接起来,变成驼峰形式
  let camelizeName = `${camelize(`${apiPrefix}${pureName}`)}` // => '$createDialog'
  return camelizeName
}

注:其中escapeReg和camelize是对正则的相关操作...这里就不仔细说了,附上代码

const camelizeRE = /-(\w)/g

function camelize(str) {
  return (str + '').replace(camelizeRE, function (m, c) {
    return c ? c.toUpperCase() : ''
  })
}

function escapeReg(str, delimiter) {
  return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&')
}

生成api名字的流程很简单,把组件名取出,从第一个字符开始判断,是否能和componentPrefix匹配,能则删除。处理后的字符再和apiPrefix连接,处理后的字符首字母大写,并加上apiPrefix前缀。

调用api,实例化组件

当我们在组件中和在某一个js文件中api形式生成组件,实际上调用的函数都是apiCreator函数返回值apicreate方法

create(config, renderFn, _single) {
  // 前置工作
  // ...省略,功能是修正参数

  // 当前调用的该方法的实例
  const ownerInstance = this
  // isInVueInstance为true:  是否在vue的实例中使用
  const isInVueInstance = !!ownerInstance.$on
  let options = {}
  if (isInVueInstance) {
    options.parent = ownerInstance
    if (!ownerInstance.__unwatchFns__) {
      ownerInstance.__unwatchFns__ = []
    }
  }
  
  // 兼容旧版本
  const renderData = parseRenderData(config, events)

  let component = null
  // 处理$props
  processProps(ownerInstance, renderData, isInVueInstance, (newProps) => {
    component && component.$updateProps(newProps)
  })
  // 处理$events
  processEvents(renderData, ownerInstance)
  // 处理$属性
  process$(renderData)
  // 实例化
  component = createComponent(renderData, renderFn, options, _single)
  // 父组件销毁时,api组件销毁
  if (isInVueInstance) {
    ownerInstance.$on(eventBeforeDestroy, beforeDestroy)
  }

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

  return component
}

create运行的条理十分的清晰,每一个模块就做某一件事,以实现下方的功能

前提:设在组件Component内调用,则Componentapi组件的父组件

  1. props可以 watch 父组件的值,做到响应式更新
  2. events可以直接写组件回调,也可以写父组件的方法名(字符串)来当作事件回调
  3. 可以支持Vue支持的所有的配置值,需以$开头
  4. 调用api式的组件销毁了,那么该组件也会自动销毁。

前置工作

上面列出的功能中,大部分是有和父组件交互的,所以create函数一开始就判断是否存在父组件

  const ownerInstance = this

如果api组件this[createName]方式实例化,那么this指向的是父组件实例。而如果是在js文件中, 通过组件自身的 $create来实例化,则this指向组件自身(未实例化)。(其中涉及到了this绑定)

组件实例化后,能在隐式原型__proto__一层一层找到属于Vue构造器的$on属性。而未实例化时,本质上只是一个复杂的、普通的对象,所以并没有Vue构造器的相关属性。

let options = {}
  if (isInVueInstance) {
    options.parent = ownerInstance
    if (!ownerInstance.__unwatchFns__) {
      ownerInstance.__unwatchFns__ = []
    }
  }

如果在组件中调用,那么就把父组件给保存在options对象中,key名叫parent。并且在父组件实例上放置一个数组__unwatchFns__,这个数组用来存储watch返回的取消观察函数,在父组件销毁时使用。

兼容旧版本及粗加工config

config是函数this[createName]调用时的第一个参数

旧版本把events写成onXxx的形式并和props放在一起,所以需要分离出来,兼容、分离就在这个parseRenderData方法里,在目录src/parse.js下。

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
}

parseRenderData又调用了parseEvents函数,现在来解释下dataevents参数

  1. data来是api调用this[createName](config, renderFn, _single)时的参数config
  2. events是生成api调用Vue.createAPI(Component, events)时的第二个参数events,一个数组,每一项时事件名

parseEvents函数把events数组中,每一项都当成对象的键名,对应的键值是首字母大写后加上on前缀。最后把这个对象的引用赋值给events。(camelize函数代码在生成api名模块)

因为我们是在实例化组件的时候,是把renderData当作配置项传递给Vue构造器,所以要符合相关规范。(Vue渲染函数data对象)

events = { click: 'onClick',  input: 'onInput' }
props = { onClick: function() {} , ...}

然后把data对象复制一份,命名为props,并且新建名叫on的对象。 遍历events自身属性,如果valuepropskey,那么就给on对象添加一个键值对。key和这个valuekey相同,值为props[key]。然后删除props中的这个属性。

on = { click: function() {} }
props = { ... }

然后把onprops组合成一个对象返回

return { props: { ... },  on: { click: function() {} } }

处理$props

在运行完兼容旧版本的parseRenderData方法后,我们得到了名叫一个renderData的对象。因为新版本中props的写发是在config中写在一个名为$props的对象中,所以还需要处理一下renderData对象,把renderData中的$props$events分开放入propson对象中。

function processProps(ownerInstance, renderData, isInVueInstance, onChange) {
  const $props = renderData.props.$props
  if ($props) {
    delete renderData.props.$props
    // 存放api组件需要响应式的变量
    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)
    }
  }
}

不解释参数就是耍流氓:

  1. ownerInstance是生成api方法时定义的变量,指向父组件或指向api组件自身
  2. renderData是经过parseRenderData粗加工后的对象
  3. isInVueInstance是否有父组件
  4. onChange提供给Vue$watch回调函数,用于响应式更新

keyPropsvalueProps$props中的键名、键值,keyvalue为父组件的键名、键值

使用Object.keys方法拿出$keyProps的集合,遍历集合。

keyProps若对应的valueProps是字符串,并且是父组件中的某一个key,那么就在renderData.props上添加keyProps,值为value

watchKeyswatchPropKeys这两个数组就是为了存储需要响应式更新的值:watchKeys每一项是api组件的key,watchPropKeys每一项是父组件的key,第n项对应第n项(n <= 0)。

响应式更新: 如果isInVueInstancetrue,那么ownerInstance是父组件。调用ownerInstance$watch方法,若父组件中在watchPropKeys属性变化了的话,那么就会更新renderData.props中相应的值,同时触发回调函数onChange。然后把$watch返回的取消观察函数保存在前置工作生成的__unwatchFns__数组里,用来停止触发回调($watch文档

(newProps) => {
  component && component.$updateProps(newProps)
}

onChange长这样,componentapi组件的实例,$updateProps是封装了的一层调用实例的$forceUpdate方法。

处理$events

$events的处理相对$props简单很多,因为没有响应式更新的地方。和处理$props一样,把props中的$events删掉,并把$events的数据挂在renderData.on

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
    })
  }
}

如果$events中键值对中某个的value是字符串,那么就在父组件中把名字和这个字符串相等方法覆盖掉这个value

处理$开头的Vue相关配置项

为了区分哪些是要给Vue的相关配置,所以在api调用时第一个参数config中需要加$,现需要找到这些变量,并去掉$后挂在renderData

function process$(renderData) {
  // props中如果存在$开头的变量,则在props中删除,并直接挂在renderData对象上
  const props = renderData.props
  Object.keys(props).forEach((prop) => {
    if (prop.charAt(0) === '$') {
      renderData[prop.slice(1)] = props[prop]
      delete props[prop]
    }
  })
}

生成实例

上面做了如此多的工作,都是为了处理要生成实例时,传递给Vue渲染函数的data对象。所谓磨刀不误砍柴工,接着是createAPI是如何实例化组件。

beforeHooks存储初始化钩子被调用时传递进来的参数,在组件实例前,先运行传递给before钩子函数。在cube-ui中,用来检验部分不推荐单例的组件是否是单例模式生成。

ownerInsUid为父组件的_uid,没有父组件则为-1。singleMap为单例模式下存储组件的对象。

(_uid为组件实例化时Vue生成的唯一id,可查看Vue代码)

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.call(this)
    instance.destroy()
  }
  const originShow = component.show
  component.show = function () {
    originShow && originShow.call(this)
    return this
  }
  const originHide = component.hide
  component.hide = function () {
    originHide && originHide.call(this)
    return this
  }
  if (single) {
    singleMap[ownerInsUid] = {
      comp: component,
      ins: instance
    }
    currentSingleComp = comp
  }
  return component
}