Vue编译器中的宏定义处理:`__VUE_OPTIONS_API__`等全局标志的替换与代码消除

Vue 编译器中的宏定义处理:__VUE_OPTIONS_API__等全局标志的替换与代码消除

大家好,今天我们来深入探讨 Vue 编译器中一个重要的优化环节:宏定义处理。具体来说,我们将着重分析 __VUE_OPTIONS_API__ 等全局标志的替换与代码消除,理解它们在 Vue.js 不同构建版本和功能特性支持中的作用。

Vue.js 为了适应不同的使用场景和提供更小的包体积,采用了多种构建版本。这些构建版本通常通过不同的宏定义标志来区分,从而实现代码的按需编译和消除。宏定义本质上是在编译时进行文本替换,并根据替换结果进行条件编译,这对于优化性能和控制代码体积至关重要。

宏定义在 Vue.js 中的作用

在 Vue.js 的编译过程中,宏定义主要用于以下几个方面:

  1. 特性开关: 启用或禁用某些特性,比如 Options API 的支持、Composition API 的支持、devtools 的支持等。
  2. 环境判断: 区分开发环境和生产环境,从而在不同环境下执行不同的代码逻辑,例如添加调试信息、进行性能优化等。
  3. 代码消除: 根据宏定义的值,在编译时消除不必要的代码,减小最终的包体积。
  4. 平台适配: 针对不同的平台(如 Web、Weex、小程序)编译不同的代码。

常见的 Vue.js 宏定义标志

以下是一些常见的 Vue.js 宏定义标志,以及它们的作用:

宏定义标志 作用
__VUE_OPTIONS_API__ 决定是否支持 Options API。如果为 true,则编译包含 Options API 相关的代码;否则,移除相关代码。这使得 Vue.js 可以提供仅支持 Composition API 的构建版本,从而减小包体积。
__VUE_PROD_DEVTOOLS__ 决定是否支持生产环境下的 Devtools。通常在生产环境下,为了安全和性能考虑,会禁用 Devtools。这个标志用于控制是否包含 Devtools 相关的代码。
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ 决定是否在生产环境下提供 hydration mismatch 的详细信息。 Hydration mismatch 是指服务器端渲染的 HTML 结构与客户端渲染的 HTML 结构不一致的情况。提供详细信息有助于调试,但会增加包体积。
__VUE_SSR_COMPILE__ 决定是否启用服务器端渲染(SSR)的编译优化。如果为 true,则编译器会针对 SSR 进行优化,例如避免不必要的 DOM 操作。
__VUE_NODE_TRANSFORM_CJS_ESM__ 用于转换 CommonJS 模块为 ES 模块。在一些构建流程中,需要将 CommonJS 模块转换为 ES 模块,以便更好地进行 tree-shaking 等优化。
__VUE_RUNTIME_COMPILE__ 决定是否支持运行时编译。如果为 true,则可以在运行时将模板字符串编译成渲染函数。这会增加包体积,但允许动态生成组件。
__FEATURE_SUSPENSE 决定是否支持 Suspense 组件。Suspense 允许在组件挂起时显示一个占位符,直到异步操作完成。
__FEATURE_TELEPORT 决定是否支持 Teleport 组件。Teleport 允许将组件渲染到 DOM 树的其他位置。
__COMPAT__ 决定是否启用兼容模式。兼容模式允许 Vue.js 3 兼容 Vue.js 2 的一些 API。

宏定义的实现原理

Vue.js 的编译器通常使用 Rollup 或 Webpack 等构建工具,这些工具提供了插件机制,可以自定义编译流程。宏定义的实现通常依赖于这些工具的插件,这些插件会在编译过程中扫描代码,查找宏定义标志,并根据标志的值进行替换和条件编译。

例如,Rollup 提供了 rollup-plugin-replace 插件,可以用于替换代码中的字符串。Vue.js 的构建流程中可能会使用这个插件来替换宏定义标志。

// rollup.config.js
import replace from '@rollup/plugin-replace';

export default {
  // ...
  plugins: [
    replace({
      __VUE_OPTIONS_API__: false,
      __VUE_PROD_DEVTOOLS__: false,
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

上面的代码片段展示了如何使用 rollup-plugin-replace 插件来替换 __VUE_OPTIONS_API____VUE_PROD_DEVTOOLS__process.env.NODE_ENV 这三个宏定义标志。

代码消除的实现原理

代码消除通常通过条件编译来实现。在 Vue.js 的代码中,会使用 if 语句或三元运算符来判断宏定义标志的值,从而决定是否执行某段代码。

例如:

if (__VUE_OPTIONS_API__) {
  // Options API 相关的代码
  console.log('Using Options API');
  // ...
} else {
  // 如果不支持 Options API,则执行其他代码
  console.log('Options API is not supported');
}

在编译过程中,如果 __VUE_OPTIONS_API__ 的值为 false,那么编译器会将 if 语句中的代码块移除,只保留 else 语句中的代码块。这样就实现了代码消除的目的。

更复杂的情况,代码消除可能涉及到整个函数或者模块的移除。例如,如果 __VUE_OPTIONS_API__false,整个 Options API 相关的模块可能都会被移除。

实例分析:__VUE_OPTIONS_API__ 的代码消除

我们以 __VUE_OPTIONS_API__ 为例,详细分析一下代码消除的实现过程。

假设 Vue.js 的源代码中包含以下代码:

// src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

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

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

以及

// src/core/instance/init.js
import { initState } from './state'
import { callHook, mountComponent } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, isReservedTag, def } from '../util/index'
import config from '../config'
import { mark, measure } from '../util/perf'

let uid = 0

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      def(vm, '$el', {
        get: () => vm._vnode && vm._vnode.elm
      })
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      mark(endTag)
      measure(`vue-perf-init:${vm._uid}`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

function initInternalComponent (vm, options) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts.listeners = vnodeComponentOptions.listeners
  opts.parent = options.parent
  opts._parentVnode = parentVnode
  opts._componentTag = vnodeComponentOptions.tag
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

export function resolveConstructorOptions (Ctor) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super options have changed.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/added options.
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
    }
  }
  return options
}

function resolveModifiedOptions (Ctor) {
  let modified
  const latest = Ctor.options
  const sealed = Ctor.sealedOptions
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}
// src/core/state.js

import { isObject, def, warn } from '../util/index'
import { arrayMethods } from './array'
import {
  traverse
} from '../observer/traverse'
import Dep from '../observer/dep'

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

/**
 * By default, when a reactive property is set, the new value is
 * also converted to become reactive. However when we are mutating a
 * reactive array we do not want to trigger another deep observe on the
 * new value. This flag allows us to avoid it when necessary.
 */
export let shouldObserve = true

export function initState (vm) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initProps (vm, propsOptions) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can be non-reactive.
  // const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    // keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttr(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      def(props, key, {
        enumerable: true,
        configurable: true,
        get: () => value,
        set: () => {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Prop name: "${key}"`,
            vm
          )
        }
      })
    } else {
      def(props, key, {
        enumerable: true,
        configurable: true,
        get: () => value,
        set: noop
      })
    }
    // defineReactive(props, key, value)
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

function initMethods (vm, methods) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance property. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

function initData (vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:n' +
      'For example:n' +
      'return {n' +
      '  data: function () {n' +
      '    return { ... }n' +
      '  }n' +
      '}n',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

const computedWatcherOptions = { lazy: true }

function initComputed (vm, computed) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just normal watchers except
  // that they has no setter.

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    // create internal watcher for the computed property.
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    )

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

/**
 * Define computed property on instance.
 */
export function defineComputed (target, key, userDef) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function initWatch (vm, watch) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm,
  expOrFn,
  handler,
  options
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

export function stateMixin (Vue) {
  // flow somehow has problems with directly declared reactive objects
  // when subtype-of any is used.
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    const vm = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
// src/core/render.js
import {
  warn,
  nextTick,
  emptyObject,
  handleError,
  defineReactive
} from '../util/index'

import { createElement } from '../vdom/create-element'
import { installRenderHelpers } from './render-helpers/index'
import { resolveSlots } from './render-helpers/resolve-slots'
import { normalizeScopedSlots } from '../vdom/helpers/normalize-scoped-slots'
import VNode, { createEmptyVNode } from '../vdom/vnode'

import { isUpdatingChildComponent } from './lifecycle'

export function initRender (vm) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // cached static sub-trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // render functions written by user.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

export let currentRenderingInstance = null

// for testing only
export function setCurrentRenderingInstance (vm) {
  currentRenderingInstance = vm
}

export function renderMixin (Vue) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = nextTick

  Vue.prototype._render = function () {
    const vm = this
    const {
      render,
      _parentVnode
    } = vm.$options

    if (vm._isMounted) {
      // if the parent didn't update, the slot may be stale.
      // Force update $slots and $scopedSlots manually.
      for (const key in vm.$slots) {
        // $flow-disable-line
        vm.$slots[key] = vm.$slots[key] // force update
      }
      if (_parentVnode && _parentVnode.data.scopedSlots) {
        vm.$scopedSlots = normalizeScopedSlots(
          _parentVnode.data.scopedSlots,
          vm.$slots,
          vm.$scopedSlots
        )
      }
    }

    vm.$scopedSlots = normalizeScopedSlots(
      vm.$options._scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )

    // set parent instance to currenly rendering instance (for component-level injections)
    setCurrentRenderingInstance(vm)

    try {
      // render self
      currentRenderingInstance = vm
      vm._vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing infinite loop
      vm._vnode = vm._vnode || createEmptyVNode()
    } finally {
      currentRenderingInstance = null
    }
    // if the returned vnode is guaranteed to be a child component, we can
    // optimize it to skip unnecessary patching.
    if (vm._vnode && vm._vnode.parent) {
      vm._vnode.parent = vm.$parent._vnode
    }
    return vm._vnode
  }
}
// src/core/events.js

import {
  warn,
  invokeWithErrorHandling
} from '../util/index'
import {
  defineReactive,
  isPlainObject
} from '../observer/index'

export function initEvents (vm) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached listeners
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

let target

function add (event, fn) {
  target.$on(event, fn)
}

function remove (event, fn) {
  target.$off(event, fn)
}

function createOnceHandler (event, fn) {
  const _this = target
  return function onceHandler () {
    const res = fn.apply(null, arguments)
    if (res !== null) {
      _this.$off(event, onceHandler)
    }
  }
}

export function updateComponentListeners (
  vm,
  listeners,
  oldListeners
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

function updateListeners (
  on,
  oldOn,
  add,
  remove,
  createOnceHandler,
  vm
) {
  let name, def, cur, old, event
  for (name in on) {
    def = on[name]
    cur = oldOn[name]
    if (isUndef(def)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${name}": got ` + String(def),
        vm
      )
    } else if (isUndef(cur)) {
      if (isUndef(def.fns)) {
        event = createFnInvoker(def, vm)
        on[name] = event
        add(name, event)
      } else {
        // existing invoker, call hooks again
        on[name] = cur
        on[name].fns = def
        add(name, cur)
      }
    } else if (isDifferent(def, cur)) {
      oldOn[name] = def
      update(def, cur, createOnceHandler, vm)
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      def = oldOn[name]
      remove(name, def)
    }
  }
}

const isUndef = v => v === undefined || v === null

function isDifferent (a, b) {
  const aIsFn = isFn(a)
  const bIsFn = isFn(b)
  if (aIsFn || bIsFn) {
    return !((aIsFn && bIsFn) || (!aIsFn && !bIsFn)) || String(a) !== String(b)
  } else {
    return true
  }
}

function isFn (v) {
  return typeof v === 'function'
}

function update (def, cur, createOnceHandler, vm) {
  let i, l
  const oldFns = cur.fns
  const fns = cur.fns = Array.isArray(def)
    ? def.slice()
    : [def]
  const add = createOnceHandler(def, vm)

  for (i = 0; i < fns.length; i++) {
    const fn = fns[i]
    if (isUndef(oldFns.indexOf(fn))) {
      add(fn)
    }
  }
  for (i = 0; i < oldFns.length; i++) {
    const fn = oldFns[i]
    if (isUndef(fns.indexOf(fn))) {
      remove(fn)
    }
  }
}

function createFnInvoker (fns, vm) {
  function invoker () {
    const fns = invoker.fns
    if (Array.isArray(fns)) {
      const cloned = fns.slice()
      for (let i = 0; i < cloned.length; i++) {
        invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
      }
    } else {
      // return handler return value for single handlers
      return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
    }
  }
  invoker.fns = fns
  return invoker
}

export function eventsMixin (Vue) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event, fn) {
    const vm = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event, fn) {
    const vm = this
    function on () {
      vm.$off(event, on)
      fn.apply(this, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event, fn) {
    const vm = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event) {
    const vm = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? Array.from(cbs) : cbs
      const args = Array.from(arguments).slice(1)
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          invokeWithErrorHandling(cbs[i], vm, args, vm, `event handler for "${event}"`)
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
  }
}

// src/core/lifecycle.js
import config from '../config'
import { callHook, activateChildComponent } from '../vdom/helpers/lifecycle'
import {
  warn,
  nextTick,
  devtools,
  inBrowser,
  isIE
} from '../util/index'

import {
  patch
} from './vdom/patch'
import Watcher from './observer/watcher'
import { pushTarget, popTarget } from './observer/dep'
import { resolveSlots } from './render-helpers/resolve-slots'

import {
  mark,
  measure
} from '../util/perf'

let uid = 0

export function initLifecycle (vm) {
  const options = vm.$options

  // locate first non-abstract parent.
  let parent = options.parent
  if

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注