[Анализ исходного кода] Хуанг И, автор vue-create-api

Vue.js
[Анализ исходного кода] Хуанг И, автор vue-create-api

Для чего нужен vue-create-api?

Как указано в README.md,Плагин, который позволяет вызывать компоненты Vue через API.. ( vue-создать-апиАдрес источника )

Установите и используйте

В настоящее время доступны две установки черезnpm install vue-create-apiили импортировать файлы статических ресурсов js.

Пример использования приведен в README.md следующим образом:

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метод. Внедрить компонент Диалог, позвонитьcreateAPIпроизводственная перепискаAPIИ установленVue.prototypeа такжеDialogна объекте. Затем вы можете передать его в компонент vuethisвызова или в js-файле$createСоздайте и используйте.

содержание

имя файла иллюстрировать
creator Создание компонентов
debug Сообщение об ошибке
index главный вход
instantiate создавать экземпляр
parse настройки параметров
util Библиотека инструментов

Далее мы будемВходПриступайте к анализу, глубокому пониманию его принципа и процесса реализации.

Вход

Должен указать, является ли плагин Vue объектомinstallметод. Если плагин является функцией, функция будет использоваться какinstallметод.installКогда метод вызывается, Vue передается в качестве параметра.vue-create-apiизinstallметод вsrc/index.jsВ файле определено:

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

Метод установки обеспечиваетoptionsпараметры конфигурации,componentPrefixпрефикс имени компонента, окончательный сгенерированный API будет игнорировать этот префикс,apiPrefixДобавьте универсальный префикс к сгенерированному API, по умолчанию$create.

Метод Vue.createAPI определен в теле метода и предоставляет три параметра.ComponentКомпонент,eventsмассив событий,singleСледует ли использовать шаблон singleton для создания экземпляра компонента.eventsВы можете передать значение логического типа или типа массива. примерeventsверно, согласно логике кода, когдаeventsДля логического типа single = события, поэтомуsingleправда ,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, и оцените, определено ли имя, если оно не определено, будет выдано исключение, поэтому используйте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методы для инициализации, такие как жизненный цикл, события, рендеринг и т. д.

Назад, определить некоторые переменныеcurrentSingleComp,singleMap,beforeHooksЭти три функции будут рассмотрены позже. Давайте сначала посмотримconst apiопределяет, что он даетbeforeа такжеcreateдва метода.

beforeпредоставил параметрhook, это функция-ловушка, которая используется в теле метода, определенном в началеbeforeHooksмассив, будетhookдобавил в массив. Как мы можем догадаться из определения имени, эти функции будут определены при инициализации компонента, и этот метод может использоваться для установки каких-то ограничений.

createПредусмотрены три параметра, которыеconfigпараметры конфигурации,renderFnИспользуется для создания дочерних узлов VNode,_singleсинглтон. Следующее решениеrenderFnявляется функцией, еслиrenderFnне является функцией и_singleКогда не определено, _single = renderFn, renderFn = null, если_singleКогда не определено, _single = single.

const ownerInstance = thisКонтекст this здесь относится к вызывающей стороне. Напримерthis.$createDialog()это указывает на экземпляр vue, если вы используетеDialog.$create()метод, это указывает на объект Dialog, бывшийisInVueInstanceверно, последнее ложно.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Перехватчик событий, который очищает и уничтожает отслеживаемые свойства и экземпляры событий.

  • Примечание. Этот метод не будет работать, если он обрабатывается сервером (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 соответствующего компонента, свойство будет отслеживаться, поэтому оно поддерживает адаптивные обновления.$eventsОбратные вызовы для событий компонента. Например:

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

Второй метод передачи по значению может быть$propsПараметры размещаются непосредственно в объекте, например{ title, content }, а что, если эта структура захочет прослушивать события?

Пожалуйста, посмотрите исходный кодparseEventsметод, который проходитeventsпараметр, который находится вcreateAPIОпределенный в , он вернет объект с ключомeventsСтоимость верблюда(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Сохраните ключи данных и параметров, которые необходимо отслеживать на наличие обновлений. Переберите$propsключ , и получить значение соответствующего ключаpropKey. Далее, есть важное условие сужденияisStr(propKey) && propKey in ownerInstance,судитьpropKeyявляется строкой и находится ли свойство вownerInstanceобъекта или его цепочки прототипов. Если true, сохраните соответствующее значение экземпляра вrenderDataи сохраните ключ в массиве часов.

следующийisInVueInstanceсудить,$watchОтслеживайте изменения данных, когдаownerInstance[watchPropKeys[i]]Когда произойдет изменение, будет вызвана эта функция и будет выполнена функция обратного вызова.$updatePropsметод, который определен вsrc/instantiate.jsВнутри:

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

propsНовые обновленные данные,$forceUpdateВызывает повторный рендеринг экземпляра Vue.

  • Уведомление:

  • 1) Когда разработчики используют этот плагин для обновления данных, значение, соответствующее обновляемому свойству, должно быть строкой и соответствовать объекту данных экземпляра 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ключ , и получить значение соответствующего ключаeventHandler,судитьeventHandlerЯвляется ли это строковым типом, если это строковый тип, получите функцию, соответствующую атрибуту в экземпляре, и назначьте ее для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
  }

Частично это связано с тем, что компонент использует шаблон singleton. Определить уникальный идентификатор текущего экземпляраownerInsUid, если существует options.parent, получить уникальный идентификатор компонента Vue._uid, наоборот-1.

судитьsingleMap[ownerInsUid]Существует ли он, если он существует, получить два значения comp и ins. Затем определите, существуют ли signle, comp и ins или они истинны.

updateRenderDataФункция метода заключается в обновлении данных рендеринга и методе обратного вызова.$forceUpdateвызывает повторную визуализацию текущего экземпляра.

instantiateComponentНиже подробно описан метод создания экземпляра компонента.

Окончательное решение методаsingleпараметр, является ли он синглтоном, еслиsingleправда, с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Экземпляр компонента.

Создайте экземпляр Vuenew 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 добавляет в тело корневой элемент DOM, используемый экземпляром Vue, а метод destroy удаляет и уничтожает его.

updateRenderDataдля обновления данных рендеринга.

$mountСмонтируйте несмонтированный экземпляр вручную. То есть этот метод не вызывается, и экземпляра Vue нет.$el.

instance.$children[0]Получить экземпляр компонента, привязать$updatePropsметод, который, наконец, возвращает экземпляр компонента.

Суммировать

На этом основной код плагина vue-create-api и весь рабочий процесс завершены. Есть две важные причины поделиться анализом исходного кода этого плагина.

1. Автор - Хуан И. Я прочитал книгу г-на Хуанга "Раскрытие технологии Vue.js" и узнал много нового. Этот плагин также сделан самим г-ном Хуангом.

Во-вторых, сам код, читая исходный код, можно увидеть, что у автора есть четкая идея, процесс реализации не небрежен, а язык лаконичен и достоин неоднократного прочтения.

Наконец, блог с прикрепленной статьейУправляемая версияУдобно всем собирать и читать. Или отсканируйте код и подпишитесь на общедоступную учетную запись WeChat [Unbelievable Front-End] и ежемесячно делитесь с вами техническими галантерейными товарами, передовым опытом и интересными навыками работы с интерфейсом.