Vue 2.x 源码解读系列《 Event 事件》

2020/3/31 11:02:20

本文主要是介绍Vue 2.x 源码解读系列《 Event 事件》,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

在 Vue 初始化的时候,同时也会调用 eventsMixin(Vue); 对事件进行初始化

这个初始化方法就是挂载一些常用的事件,比如:组件上的事件监听,组件里调用实例的 $emit() 方法,等等这些都是在 eventsMixin(Vue); 方法中完成。

在组件上监听事件有两个写法,一种是简写使用 @event 标识,一种是 v-on:event 标识。

事件方法调用入口

// 定义 Vue 构造器
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')
  }
  // 当通过 new 创建 Vue 实例时,调用 _init() 方法,对 Vue 实例进行初始
  this._init(options)
}
// 给 Vue.prototype 添加 _init() 
initMixin(Vue)
// 给 Vue.prototype 添加 $data 对象、$props 对象、$set()、$delete()、$watch()
stateMixin(Vue)
// 给 Vue.prototype 添加 $on()、$once()、$off()、$emit()、
eventsMixin(Vue)
// 给 Vue.prototype 添加 _update()、$forceUpdate()、$destroy()、
lifecycleMixin(Vue)
// 给 Vue.prototype 添加 $nextTick()、_render()
renderMixin(Vue)

export default Vue
复制代码

执行一些 XXMixin 函数,对 Vue 类进行属性和方法的挂载,比如本文的 eventsMixin(Vue) 。即完成 Vue 的组装。

事件($emit()$on()$once()$off())的几个方法是通过 src/core/instance/index.js 文件中的eventsMixin() 方法挂载到 Vue 原型上的。

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  // event 参数可以是一个字符串,也可以是一个字符串数组,fn 事件监听的回调
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    // 如果是一个字符串数组,则遍历递归数组中的每一项
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // 如果 vm._events[event] 不存在则创建对应的事件名为 key 的数组,并且把其对应的回调函数放到此数组中,也就是说一个事件名,会对应一个数组,里面存在了此事件名所对应的所有回调
      // vm._events[event] 事件对应的是一个回调函数组成的数据,但在什么时候才会出现一个事件名对象多个回调函数呢???
      // 从 $on() 方法的实现我们可以想象到一种情况下会是多个,那就是在子组件中手动调用一下 this.$on('eventName', handlerFn),但这样有必需要吗?还是有些场景我还没遇到?
      (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: string, fn: Function): Component {
    const vm: Component = this
    // 定义一个事件回调函数
    function on () {
      // 取消 event 事件的监听
      vm.$off(event, on)
      // 执行传入的函数 fn
      fn.apply(vm, arguments)
    }
    // 给 on 挂载 fn 属性
    on.fn = fn
    // 监听 event 事件,从这里可以知道,$once() 方法其内部实现最终也是通过 vm.$on() 方法实现的,但它是怎么做到多次触发,但只会执行一次的呢?
    // 关键在于上面定义的 on() 方法,这个方法对传进来的事件回调重新封装了一层,而里面的实现则是先取消实例上此事件的监听,而后再执行传入的函数。
    vm.$on(event, on)
    return vm
  }
  // 注销事件函数
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // 如果没有参数,则注销所有事件
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // 如果第一个参数为数组,则注销指定 事件
    if (Array.isArray(event)) {
      // 遍历调用(递归) vm.$off()
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // 取出已经声明的事件对应的回调函数数组
    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: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      // 转为小写字符串
      const lowerCaseEvent = event.toLowerCase()
      // 如果转换存在大小写,并且父组件有监听此事件的小写事件名,则会打印出提示
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    // vm._events 是在 Vue.prototype.$on 方法中定义的,
    // 取出对应事件中的回调函数数组
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
       // 取出参数。this.$emit() 方法的第一个参数为事件名,后面的都被视为参数,所以在使用 this.$emit() 时,我们可以传任意个参数
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      // 遍历执行数组中的每一个函数
      for (let i = 0, l = cbs.length; i < l; i++) {
        // invokeWithErrorHandling 只是对函数进行了处理处理的封装
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}
复制代码

代码品尝

1.巧用短路运算符以及变量赋值的特性。

(vm._events[event] || (vm._events[event] = [])).push(fn)
复制代码

这么写不是挺巧妙的。一般情况下我们会这么写:

if(!vm._events[event]){
    vm._events[event] = []
}
vm._events[event].push(fn)
复制代码

虽然看起来代码量没多大区别,但这种写法要求我们对 js 有比较深入的理解。不然也很难写出像源码那样的代码。这里解释下:

  1. 逻辑运算符 || 属于短路运算符,只要前面的条件为真就不会执行后面的语句了。只有当 || 运算符前面的条件结果不为真才会继续执行后面的运算。
  2. 而在 js 中对一个变量进行赋值的时候,当赋值语句执行完后,值会被作为结果返回
  3. 最后用括号把前面的语句整个括起来运行运算得出最终的结果。

2.缓存for循环变量

 for (let i = 0, l = cbs.length; i < l; i++) {
    // invokeWithErrorHandling 只是对函数进行了处理处理的封装
    invokeWithErrorHandling(cbs[i], vm, args, vm, info)
 }
复制代码

在 for 循环中除了定义 i 变量外,还额外定义了 l 变量,为什么要这么做呢?这样做的目的是缓存了数组的长度。避免每次循环都会重新计算一次数组长度。上面的代码等价于:

let l = event.length
for (let i = 0; i < l; i++) {
    vm.$off(event[i], fn)
}
复制代码

3.缓存变量

let cbs = vm._events[event]
复制代码

这样写有什么好处呢?我认为至少有两点。第一,可把全部变量缓存处局部提高查找速度,虽然在这里 vm._events[event]不算全局变量,但我们自己在写代码的时候就可以使用这种方式;第二,方便使用,比如上面的代码,后面使用到 vm._events[event] 时,直接用变量 cbs 替代,不用总是写一长串代码。

当通过 new 关键字创建 Vue 实例的时候会调用 initEvent(vm)

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 可以看到 updateChildComponent() 方法的第三个参数来自于 options.listeners,而 options 则取自 vnode.componentOptions
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

let target: any
// 添加事件监听
function add (event, fn) {
  target.$on(event, fn)
}
// 取消事件监听
function remove (event, fn) {
  target.$off(event, fn)
}

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

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}
复制代码

上面的两段代码其实都是在 events.js 文件中的。

事件实现原理

通过在子组件中调用 this.$emit() 方法与及父组件中使用 @eventName="handlerName" 的方式实现父子组件之间事件的数据交互。其实这两个方法的实现代码并不复杂。

this.$emit() 方法

当子组件中调用 this.$emit() 方法时会执行如下代码

Vue.prototype.$emit = function (event) {
  var vm = this;
  {
    var lowerCaseEvent = event.toLowerCase();
    if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
      tip(
        "Event \"" + lowerCaseEvent + "\" is emitted in component " +
        (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
        "Note that HTML attributes are case-insensitive and you cannot use " +
        "v-on to listen to camelCase events when using in-DOM templates. " +
        "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
      );
    }
  }
  var cbs = vm._events[event];
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs;
    var args = toArray(arguments, 1);
    var info = "event handler for \"" + event + "\"";
    for (var i = 0, l = cbs.length; i < l; i++) {
      invokeWithErrorHandling(cbs[i], vm, args, vm, info);
    }
  }
  return vm
};
复制代码

如果父组件中没监听 $emit() 方法发出的事件,即 var cbs = vm._events[event] 中的 cbsundefined ,则不作任何处理。

如果 cbs 有值,说明父组件有监听子组件事件。当子组件 $emit() 发出对应的事件时就会监听父组件中的监听事件的回调函数。

最后就遍历这个 cbs 函数数组对每一个回调调用 invokeWithErrorHandling() 方法进行处理。

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    // 调用 handler() 方法, context 为当前的 vue 实例,args 为 this.$emit(eventName,args) 传入的参数
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
复制代码

这个方法也没有什么特别的,只是把事件回调包装了一层。其中包括,函数执行的上下文 this 的指定,如果 handler 是一个 Promise 则对方法默认加一个 catch() 来捕获有可能的错误。并标记 res._handled = true ,意思是此 handler 已经处理过了,避免重复处理(issue #9511--- avoid catch triggering multiple times when nested calls)。

还有一个就是 apply() 的用法,这个一定要掌握。并且在很多源码都会被用到。apply() 用来改变函数的 this 指向。还有他的一个兄弟 call(),两个的区别只是在于接收参数的数量不一样,apply() 可以接收多个参数,call() 只接收一个参数。还有另外一个就是 bind() 也是可以修改函数的 this 指向,也可以传多个参数。但它跟 apply() 方法的不同在于,bind() 方法改变函数的 this 指向的同时它是返回一个新的函数,而 apply() 不是。

$emit() 方法中的 vm._events 值从哪来的呢?来自于 $on() 方法的收集,接下来就是 $on() 方法了。

this.$on() 方法

// event 参数可以是一个字符串,也可以是一个字符串数组,fn 事件监听的回调
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  // 如果是一个字符串数组,则遍历递归数组中的每一项
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    // 如果 vm._events[event] 不存在则创建对应的事件名为 key 的数组,并且把其对应的回调函数放到此数组中,也就是说一个事件名,会对应一个数组,里面存放了此事件名所对应的所有回调
    (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
}
复制代码

一般情况下都是一个事件对应一个回调函数。那什么时候才会有多个呢???有知道的可以留言交流交流。

至于其它两个方法 $once$off() 我们平时基本用不到,这两个方法主要还是源码内部自己调用。

了解了 const listeners = vm.$options._parentListeners 的由来后,接下来就是 updateComponentListeners() 方法了。这个方法接收三个参数,但在 initEvents() 方法中只传入了两个参数,原因是它是首次初始化组件,不存在 oldListeners

/**
 * updateComponentListeners() 方法
 */
function updateComponentListeners (
  vm,
  listeners,
  oldListeners
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
  target = undefined;
}
......
/**
 * updateListeners() 方法
 */
function updateListeners (
  on,
  oldOn,
  add,
  remove$$1,
  createOnceHandler,
  vm
) {
  var name, def$$1, cur, old, event;
  // 遍历所有的监听事件(on 是一个对象 { eventName: handlerFn })
  for (name in on) {
    def$$1 = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    // 如果没有设置对应的回调函数,在开发模式下就会打印出警告,isUndef() 方法判断是否全等于 undefined 或者是 null
    if (isUndef(cur)) {
      warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    // 如果旧的对象上也不存在
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm);
      }
      // 如果是只执行一次的监听回调,isTrue() 方法判断是否是真(全等于 true)
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture);
      }
      // 调用 add() 方法往 vm._events 对象里添加事件及回调
      add(event.name, cur, event.capture, event.passive, event.params);
    } else if (cur !== old) {
      old.fns = cur;
      on[name] = old;
    }
  }
  // 遍历旧的事件对象
  for (name in oldOn) {
    // 若时时存在于新的事件对象中
    if (isUndef(on[name])) {
      // 组装事件对象
      event = normalizeEvent(name);
      remove$$1(event.name, oldOn[name], event.capture);
    }
  }
}
......
/**
 * normalizeEvents() 方法
 */
var normalizeEvent = cached(function (name) {
  var passive = name.charAt(0) === '&';
  name = passive ? name.slice(1) : name;
  var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first
  name = once$$1 ? name.slice(1) : name;
  var capture = name.charAt(0) === '!';
  name = capture ? name.slice(1) : name;
  return {
    name: name,
    once: once$$1,
    capture: capture,
    passive: passive
  }
});
复制代码

上面的代码主要是给实例添加监听 add(event.name, cur, event.capture, event.passive, event.params); 这个方法其实就是调用了 target.$on(event, fn); target 为当前渲染的实例。但 add() 方法中为何传 5 个参数,而 add() 定义时,里面的逻辑只用到了前两个参数。 所以我们主要还是得关注 event 参数从何而来。

可以看到 normalizeEvent() 调用了 cached() 方法生成一个具有缓存数据能力的函数。这个函数的作用就是把一个字符串事件名处理成一个含有事件名 name 及其它基础属性的对象。这里我们可以知道 normalizeEvent() 只是一个很简单的方法,初始化一些状态值。

normalizeEvent() 方法中我们还可以知道官方文档中的 .passive.capture.once 修饰符的简写是怎么实现的了。

对应文档链接:cn.vuejs.org/v2/guide/re…

下面是几个例子

  1. & 的作用就是告诉浏览器方法代码中没有用到 preventDefault 阻止默认行为。

    <Test @&myevent="myEvent1"></Test>
    复制代码
  2. 不管子组件中调用了多少次的 $emit() 方法,如果想让一个事件只触发一次,我们可以在事件名前 @ 符号后加一个 ~ ,就像下面这样:

    <Test @~myevent="myEvent1"></Test>
    复制代码
  3. 还可以使用 ! 来改变事件的解发顺序(捕获|冒泡)

    <!-- 加感叹号 -->
    <div @!click="myclick('d1')">
        <div @!click="myclick('d2')">
            <div @!click="myclick('d3')">点我试试</div>
        </div>
    </div>
    <!-- 打印出 d1 d2 d3 -->
    
    <!-- 不加感叹号 -->
    <div @click="myclick('d1')">
        <div @click="myclick('d2')">
            <div @click="myclick('d3')">点我试试</div>
        </div>
    </div>
    <!-- 打印出 d3 d2 d1 -->
    复制代码

    这个修改符在什么场景下会使用到,反正我没有使用过。

上面的三个修饰符的简写形式我都没有使用过。毕竟看起来不是很直观。这得要求项目的所有人都对 Vue 非常熟悉才不会让彼此写的代码产生隔阂。

但话又说回来,我们是不是应该对 Vue 要有全面深入的认识,如果不是,那连一个 vue api 工程师都配不上。

比如在平时的工作中,有些代码重复性比较高,可能就只是一些变量名不同,这时我们可以使用一个循环生产代码。把共同的逻辑放到循环中。

但有人会说如果这样会不直观,同项目里的其他人可能看不懂?这...这......这.........

如果我们在同一个组件监听多个同名的事件会怎么样?

<Test @myEvent="myEvent2" @my-Event="myEvent1"></Test>
复制代码

试验的结果是:只执行最前面监听的那个事件的回调(myEvent2),至于他怎么处理的,可能等到分析模板编译的时候应该可以得出答案。也不在本遍的讨论范围之内。

事件的命名的问题

由于在我们通过 this.$emit(event) 方法发出的事件名时,它内部的处理逻辑是直接使用我们传入的事件名(不作任何处理【自动转小写】),从 vm._events[event] 对象中直接取出对应事件的回调函数。而所有监听的事件名都会被自动转成小写,这就会有对不上的场景。换句话说就是我们不能在 this.$emit() 方法中传入含有大小写的事件名,不然会找不到对应的事件。这里我们总结一下事件命名的可行性。

  1. 我们可以在父组件中书写含有大小写字母的事件名(因为事件名会默认转成小写)
  2. 我们不可以在 $emit() 方法中书写含有大小写的事件名

只要注意这两点就可以畅通无阻了。虽然这只是个小问题,但平时不注意也可让你耗不少时间去发现它。

题外话

initMixin() 方法中就又调用了各种初始化方法。在创建 Vue 实例的时候执行以初始化各种状态,其中的 initEvents()方法

/**
 * initEvents() 方法
 */
export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}
复制代码

initEvents() 方法中的 vm.$options._parentListeners 这个在哪赋值的呢?它的值又是什么呢?要想知道这个怎么来的,我们只需要打个断点,然后查看下浏览器中的执行栈就知道了。

这个值在 initInternalComponent(vm, options) 方法进行了赋值。

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

  var vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}
复制代码

可以看到 opts._parentListeners = vnodeComponentOptions.listeners;vnodeComponentOptions 的值则来自于 parentVnode.componentOptionsparentVnode 的值又来自于 options._parentVnode; ,所以最后我们需要知道这个 options 参数又是怎么来的。

Vue.prototype._init = function (options) {
  var vm = this;
  // a uid
  vm._uid = uid$3++;

  var startTag, endTag;
  /* istanbul ignore if */
  if (config.performance && mark) {
    startTag = "vue-perf-start:" + (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 */
  {
    initProxy(vm);
  }
  // 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 (config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    mark(endTag);
    measure(("vue " + (vm._name) + " init"), startTag, endTag);
  }

  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};
复制代码

_init() 方法中调用了 initInternalComponent() ,并且随后它就会执行 initEvents()方法。而 _init() 方法是在 src/core/global-api/extend.js 文件中的 Vue.extend() 方法中的定义了这么一个函数 VueComponent(),在这里可以知道,当外部使用 new Sub() 来创建一个实例的时候,就会执行 _init() 方法了。

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  // 这里的 this 指向 Vue 构造函数
  const Super = this
  const SuperId = Super.cid
  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)
  }

  const Sub = function VueComponent(options) {
    // 这里调用了 _init() 方法,初始化组件
    this._init(options)
  }
  // 把 Vue 构造函数的 prototype 挂载到 Sub.prototype.__proto__ 上
  Sub.prototype = Object.create(Super.prototype)
  // 设置 Sub.prototype.constructor 指向 Sub 构造函数
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 合并父类和传入的 options 
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }
  // allow further extension/mixin/plugin usage
  // 在新的构造函数中定义 extend、mixin、use、component、directive、filter 
  // 分别引用父类的 extend、mixin、use、component、directive、filter 为了减少查找路径
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}
复制代码

这个方法返回一个 sub 函数,在 createComponentInstanceForVnode() 方法中的最后一行代码通过 new 创建一个实例 。这个构造函数里就会执行 this._init(options)

function createComponentInstanceForVnode (
  vnode, // we know it's MountedComponentVNode but flow doesn't
  parent // activeInstance in lifecycle state
) {
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options)
}
复制代码

从上面的代码中可以看出 options._parentVnode 的值就是 vnode(待继......),到时讲到虚拟 DOM 的时候应该就可以看到它的相关代码实现。

收获

  1. 当循环中写一些业务逻辑的时候,如果代码比较多,可以把 for 循环里面的代码抽到一个方法里。比如前面的 invokeWithErrorHandlingfor 循环人人都会写,但不是所有人都有这个意识的。if 条件语句也是同样的道理。
  2. 封装函数统一处理。像这种在平时的开发当中也是经常遇到的。在看到这个之前,我也没有意识到这前自己在写代码的时候其它还可以再封装一层作统一的捕获处理。看源码真的可以让你长见识,只要你细心品尝。
  3. 如果某个函数会被频繁被调用,并且函数所处理的结果有可能会在后面被重复使用,此时我们就可以封装成一个具体缓存数据的函数,避免同样的业务逻辑被重复多次计算,特别是计算量比较大时,这个方法可以保证性能。
  4. 我们可以像上面的 Vue.prototype._init 方法一样,自身调用了很多 init 方法,进行各种初始化,把代码分别封闭到多个方法中,直接调用方法就行,代码清晰明了,更易维护,看得确实舒心。
  5. 把局变量缓存到局部变量中。
  6. for 循环可以缓存数组长度。


这篇关于Vue 2.x 源码解读系列《 Event 事件》的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程