Интерпретация исходного кода Vue (5) — глобальный API

JavaScript Vue.js
Интерпретация исходного кода Vue (5) — глобальный API

Цель

Глубокое понимание принципов реализации следующих глобальных API.

  • Vue.use

  • Vue.mixin

  • Vue.component

  • Vue.filter

  • Vue.directive

  • VUe.extend

  • Vue.set

  • Vue.delete

  • Vue.nextTick

Интерпретация исходного кода

Из первой статьи циклаИнтерпретация исходного кода Vue (1) — Предисловиесередина源码目录结构Как видно из введения, большинство реализаций многих глобальных API-интерфейсов Vue размещены в/src/core/global-apiПод содержанием. Эти глобальные записи для чтения исходного кода API находятся в/src/core/global-api/index.jsв файле.

Вход

/src/core/global-api/index.js

/**
 * 初始化 Vue 的众多全局 API,比如:
 *   默认配置:Vue.config
 *   工具方法:Vue.util.xx
 *   Vue.set、Vue.delete、Vue.nextTick、Vue.observable
 *   Vue.options.components、Vue.options.directives、Vue.options.filters、Vue.options._base
 *   Vue.use、Vue.extend、Vue.mixin、Vue.component、Vue.directive、Vue.filter
 *   
 */
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  // Vue 的众多默认配置项
  configDef.get = () => config

  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }

  // Vue.config
  Object.defineProperty(Vue, 'config', configDef)

  /**
   * 暴露一些工具方法,轻易不要使用这些工具方法,处理你很清楚这些工具方法,以及知道使用的风险
   */
  Vue.util = {
    // 警告日志
    warn,
    // 类似选项合并
    extend,
    // 合并选项
    mergeOptions,
    // 设置响应式
    defineReactive
  }

  // Vue.set / delete / nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 响应式方法
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  // Vue.options.compoents/directives/filter
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // 将 Vue 构造函数挂载到 Vue.options._base 上
  Vue.options._base = Vue

  // 在 Vue.options.components 中添加内置组件,比如 keep-alive
  extend(Vue.options.components, builtInComponents)

  // Vue.use
  initUse(Vue)
  // Vue.mixin
  initMixin(Vue)
  // Vue.extend
  initExtend(Vue)
  // Vue.component/directive/filter
  initAssetRegisters(Vue)
}

Vue.use

/src/core/global-api/use.js

/**
 * 定义 Vue.use,负责为 Vue 安装插件,做了以下两件事:
 *   1、判断插件是否已经被安装,如果安装则直接结束
 *   2、安装插件,执行插件的 install 方法
 * @param {*} plugin install 方法 或者 包含 install 方法的对象
 * @returns Vue 实例
 */
Vue.use = function (plugin: Function | Object) {
  // 已经安装过的插件列表
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  // 判断 plugin 是否已经安装,保证不重复安装
  if (installedPlugins.indexOf(plugin) > -1) {
    return this
  }

  // 将 Vue 构造函数放到第一个参数位置,然后将这些参数传递给 install 方法
  const args = toArray(arguments, 1)
  args.unshift(this)

  if (typeof plugin.install === 'function') {
    // plugin 是一个对象,则执行其 install 方法安装插件
    plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
    // 执行直接 plugin 方法安装插件
    plugin.apply(null, args)
  }
  // 在 插件列表中 添加新安装的插件
  installedPlugins.push(plugin)
  return this
}

Vue.mixin

/src/core/global-api/mixin.js

/**
 * 定义 Vue.mixin,负责全局混入选项,影响之后所有创建的 Vue 实例,这些实例会合并全局混入的选项
 * @param {*} mixin Vue 配置对象
 * @returns 返回 Vue 实例
 */
Vue.mixin = function (mixin: Object) {
  // 在 Vue 的默认配置项上合并 mixin 对象
  this.options = mergeOptions(this.options, mixin)
  return this
}

mergeOptions

src/core/util/options.js

/**
 * 合并两个选项,出现相同配置项时,子选项会覆盖父选项的配置
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // 标准化 props、inject、directive 选项,方便后续程序的处理
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 处理原始 child 对象上的 extends 和 mixins,分别执行 mergeOptions,将这些继承而来的选项合并到 parent
  // mergeOptions 处理过的对象会含有 _base 属性
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  // 遍历 父选项
  for (key in parent) {
    mergeField(key)
  }

  // 遍历 子选项,如果父选项不存在该配置,则合并,否则跳过,因为父子拥有同一个属性的情况在上面处理父选项时已经处理过了,用的子选项的值
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  // 合并选项,childVal 优先级高于 parentVal
  function mergeField (key) {
    // strat 是合并策略函数,如何 key 冲突,则 childVal 会 覆盖 parentVal
    const strat = strats[key] || defaultStrat
    // 值为如果 childVal 存在则优先使用 childVal,否则使用 parentVal
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

Vue.component, Vue.filter, Vue.directive

/src/core/global-api/assets.js

Реализация этих трех API особенная, но принципы очень похожи, поэтому они реализуются вместе.

const ASSET_TYPES = ['component', 'directive', 'filter']

/**
 * 定义 Vue.component、Vue.filter、Vue.directive 这三个方法
 * 这三个方法所做的事情是类似的,就是在 this.options.xx 上存放对应的配置
 * 比如 Vue.component(compName, {xx}) 结果是 this.options.components.compName = 组件构造函数
 * ASSET_TYPES = ['component', 'directive', 'filter']
 */
ASSET_TYPES.forEach(type => {
  /**
   * 比如:Vue.component(name, definition)
   * @param {*} id name
   * @param {*} definition 组件构造函数或者配置对象 
   * @returns 返回组件构造函数
   */
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      if (type === 'component' && isPlainObject(definition)) {
        // 如果组件配置中存在 name,则使用,否则直接使用 id
        definition.name = definition.name || id
        // extend 就是 Vue.extend,所以这时的 definition 就变成了 组件构造函数,使用时可直接 new Definition()
        definition = this.options._base.extend(definition)
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      // this.options.components[id] = definition
      // 在实例化时通过 mergeOptions 将全局注册的组件合并到每个组件的配置对象的 components 中
      this.options[type + 's'][id] = definition
      return definition
    }
  }
})

Vue.extend

/src/core/global-api/extend.js

/**
 * Each instance constructor, including Vue, has a unique
 * cid. This enables us to create wrapped "child
 * constructors" for prototypal inheritance and cache them.
 */
Vue.cid = 0
let cid = 1

/**
 * 基于 Vue 去扩展子类,该子类同样支持进一步的扩展
 * 扩展时可以传递一些默认配置,就像 Vue 也会有一些默认配置
 * 默认配置如果和基类有冲突则会进行选项合并(mergeOptions)
 */
Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid

  /**
   * 利用缓存,如果存在则直接返回缓存中的构造函数
   * 什么情况下可以利用到这个缓存?
   *   如果你在多次调用 Vue.extend 时使用了同一个配置项(extendOptions),这时就会启用该缓存
   */
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  // 定义 Sub 构造函数,和 Vue 构造函数一样
  const Sub = function VueComponent(options) {
    // 初始化
    this._init(options)
  }
  // 通过原型继承的方式继承 Vue
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 选项合并,合并 Vue 的配置项到 自己的配置项上来
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  // 记录自己的基类
  Sub['super'] = Super

  // 初始化 props,将 props 配置代理到 Sub.prototype._props 对象上
  // 在组件内通过 this._props 方式可以访问
  if (Sub.options.props) {
    initProps(Sub)
  }

  // 初始化 computed,将 computed 配置代理到 Sub.prototype 对象上
  // 在组件内可以通过 this.computedKey 的方式访问
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // 定义 extend、mixin、use 这三个静态方法,允许在 Sub 基础上再进一步构造子类
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // 定义 component、filter、directive 三个静态方法
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })

  // 递归组件的原理,如果组件设置了 name 属性,则将自己注册到自己的 components 选项中
  if (name) {
    Sub.options.components[name] = Sub
  }

  // 在扩展时保留对基类选项的引用。
  // 稍后在实例化时,我们可以检查 Super 的选项是否具有更新
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // 缓存
  cachedCtors[SuperId] = Sub
  return Sub
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

Vue.set

/src/core/global-api/index.js

Vue.set = set

set

/src/core/observer/index.js

/**
 * 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
 * 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 更新数组指定下标的元素,Vue.set(array, idx, val),通过 splice 方法实现响应式更新
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 更新对象已有属性,Vue.set(obj, key, val),执行更新即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  // 不能向 Vue 实例或者 $data 添加动态添加响应式属性,vmCount 的用处之一,
  // this.$data 的 ob.vmCount = 1,表示根组件,其它子组件的 vm.vmCount 都是 0
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // target 不是响应式对象,新属性会被设置,但是不会做响应式处理
  if (!ob) {
    target[key] = val
    return val
  }
  // 给对象定义新属性,通过 defineReactive 方法设置响应式,并触发依赖更新
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

Vue.delete

/src/core/global-api/index.js

Vue.delete = del

del

/src/core/observer/index.js

/**
 * 通过 Vue.delete 或者 vm.$delete 删除 target 对象的指定 key
 * 数组通过 splice 方法实现,对象则通过 delete 运算符删除指定 key,并执行依赖通知
 */
export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }

  // target 为数组,则通过 splice 方法删除指定下标的元素
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__

  // 避免删除 Vue 实例的属性或者 $data 的数据
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // 如果属性不存在直接结束
  if (!hasOwn(target, key)) {
    return
  }
  // 通过 delete 运算符删除对象的属性
  delete target[key]
  if (!ob) {
    return
  }
  // 执行依赖通知
  ob.dep.notify()
}

Vue.nextTick

/src/core/global-api/index.js

Vue.nextTick = nextTick

nextTick

/src/core/util/next-tick.js

Для более подробного анализа метода nextTick вы можете просмотреть предыдущую статьюИнтерпретация исходного кода Vue (4) — асинхронное обновление.

const callbacks = []
/**
 * 完成两件事:
 *   1、用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
 *   2、如果 pending 为 false,表示现在浏览器的任务队列中没有 flushCallbacks 函数
 *     如果 pending 为 true,则表示浏览器的任务队列中已经被放入了 flushCallbacks 函数,
 *     待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
 *     浏览器的任务队列了
 * pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
 * @param {*} cb 接收一个回调函数 => flushSchedulerQueue
 * @param {*} ctx 上下文
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用 callbacks 数组存储经过包装的 cb 函数
  callbacks.push(() => {
    if (cb) {
      // 用 try catch 包装回调函数,便于错误捕获
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Суммировать

  • интервьюер спросил: что делает Vue.use (плагин)?

    отвечать:

    Фактически, за установку плагина отвечает плагин, который выполняет метод установки, предоставляемый плагином.

    • Сначала проверьте, установлен ли плагин

    • Если нет, выполните метод установки, предоставляемый подключаемым модулем, чтобы установить подключаемый модуль, и подключаемый модуль решает, что делать.


  • интервьюер спросил: Что делает Vue.mixin(options)?

    отвечать:

    Отвечает за слияние настроек параметров в глобальной конфигурации Vue. Затем глобальная конфигурация объединяется с собственной конфигурацией компонента, когда каждый компонент генерирует vnode.

    • Стандартизируйте формат свойств, инъекций, параметров директивы в объекте параметров.

    • Обрабатывайте расширения и примеси в параметрах и объединяйте их в глобальную конфигурацию соответственно.

    • Затем объедините конфигурацию параметров и глобальную конфигурацию.Если параметры конфликтуют, конфигурация параметров переопределяет глобальную конфигурацию.


  • интервьюер спросил: Что делает Vue.component(compName, Comp)?

    отвечать:

    Отвечает за регистрацию глобальных компонентов. На самом деле конфигурация компонента регистрируется в опции компонентов глобальной конфигурации (options.components), а затем каждый подкомпонент будет объединять опцию глобальных компонентов с элементом конфигурации локальных компонентов при создании vnode.

    • Если второй параметр пустой, значит получить конструктор компонента compName

    • Если Comp является объектом конфигурации компонента, используйте метод Vue.extend для получения конструктора компонента, в противном случае сразу переходите к следующему шагу.

    • Установите информацию о компоненте в глобальной конфигурации,this.options.components.compName = CompConstructor


  • интервьюер спросил: что делает Vue.directive('my-directive', {xx})?

    отвечать:

    Зарегистрируйте мою директиву глобально, тогда каждый дочерний компонент будет объединять параметр глобальных директив с параметром локальных директив при создании vnode. Принцип тот же, что и у метода Vue.component:

    • Если второй параметр пуст, получить объект конфигурации указанной директивы

    • Если не пусто, если второй параметр является функцией, будет сгенерирован объект конфигурации { bind: второй параметр, обновление: второй параметр}

    • Затем установите объект конфигурации инструкции в глобальную конфигурацию,this.options.directives['my-directive'] = {xx}


  • интервьюер спросил: что делает Vue.filter('my-filter', function(val) {xx})?

    отвечать:

    Отвечает за глобальную регистрацию фильтра my-filter, а затем каждый подкомпонент объединяет параметр глобальных фильтров с параметром локальных фильтров при создании vnode. Принцип таков:

    • Получает функцию обратного вызова для фильтра my-filter, если второй аргумент не указан

    • Если указан второй параметр, это настройкаthis.options.filters['my-filter'] = function(val) {xx}.


  • интервьюер спросил: что делает Vue.extend(options)?

    отвечать:

    Vue.extend создает подкласс на основе Vue, и параметры параметров будут использоваться в качестве глобальной конфигурации подкласса по умолчанию, как и глобальная конфигурация Vue по умолчанию. Таким образом, расширение подкласса через Vue.extend имеет большое значение для создания некоторой общедоступной конфигурации для использования подклассом подкласса.

    • Определите конструктор подкласса, который, как и Vue, также вызывает _init(options)

    • Комбинированная конфигурация и параметры Vue, если параметр конфликтует, параметры параметра переопределяют элементы конфигурации Vue.

    • Определите глобальный API для подклассов, значением является глобальный API Vue, напримерSub.extend = Super.extend, так что подклассы могут также расширять другие подклассы

    • вернуть подкласс


  • интервьюер спросил: что делает Vue.set(target, key, val)

    отвечать:

    Поскольку Vue не может обнаруживать общие новые свойства (такие как this.myObject.newProperty = 'hi'), добавление свойства к отзывчивому объекту через Vue.set может гарантировать, что новое свойство также будет реагировать и запускает обновление представления.

    • Обновите элемент указанного индекса массива: Vue.set(array, idx, val), который внутренне реализует адаптивное обновление с помощью метода splice.

    • Обновите существующие свойства объекта: Vue.set(obj, key, val), просто обновите его напрямую =>obj[key] = val

    • Невозможно динамически добавлять реактивные данные корневого уровня в экземпляр Vue или $data.

    • Vue.set(obj, key, val), если obj не является реактивным объектом, он будет выполненobj[key] = val, но не будет выполнять адаптивную обработку

    • Vue.set (Obj, Key, VAL), добавляет новый ключ к объекту ответа OBJ, устанавливает ответ с помощью метода DefineReactive и запускает обновление выкупа.


  • интервьюер спросил: Что делает Vue.delete(target, key)?

    отвечать:

    Удалить свойство объекта. Если объект является реактивным, убедитесь, что удаление запускает обновление представления. Этот метод в основном используется, чтобы обойти ограничение, заключающееся в том, что Vue не может обнаружить удаление свойства, но его следует использовать редко. Конечно, также невозможно удалить реактивные свойства на корневом уровне.

    • Vue.delete(array, idx), удалить элемент с указанным индексом, который реализуется внутри через метод splice

    • Удалить свойство отзывчивого объекта: Vue.delete(obj, key), который выполняется внутриdelete obj.key, а затем выполните обновление зависимостей


  • интервьюер спросил: что делает Vue.nextTick(cb)?

    отвечать:

    Функция метода Vue.nextTick(cb) заключается в задержке выполнения функции обратного вызова cb, которая обычно используется дляthis.key = newValПосле изменения данных я хочу немедленно получить измененные данные DOM:

    this.key = 'new val'
    
    Vue.nextTick(function() {
      // DOM 更新了
    })
    

    Его внутренний процесс выполнения:

    • this.key = 'new val, запустить обновление уведомления о зависимости и поместить наблюдателя, ответственного за обновление, в очередь наблюдателя.

    • Поместите функцию, которая очищает очередь наблюдателя, в массив обратных вызовов.

    • Поместите функцию, которая обновляет массив обратных вызовов, в очередь асинхронных задач браузера.

    • Vue.nextTick(cb)чтобы сократить очередь, поместите функцию cb в массив callbacks

    • Выполнить функцию, которая обновляет массив обратных вызовов в какой-то момент в будущем.

    • Затем выполните множество функций в массиве обратных вызовов, инициируйте выполнение watcher.run и обновите DOM.

    • Поскольку функция cb размещается позже в массиве callbacks, это гарантирует, что сначала завершится обновление DOM, а затем будет выполнена функция cb.

Сопутствующее видео

Интерпретация исходного кода Vue (5) — глобальный API

Поиск внимания

Приветствую всех, чтобы следовать за мнойСчет наггетса такжеСтанция Б, если контент полезен для вас, ставьте лайк, добавляйте в избранное + подписывайтесь

Связь

группа по обмену знаниями

Связь