资讯专栏INFORMATION COLUMN

Vue.js源码分析-- eventsAPI--1.0.26

DoINsiSt / 2864人阅读

摘要:组件将回调函数保存在中,对同一可以绑定多个回调函数,同时,通过更新所有父组件的。这里的特殊处理暂且忽略,还得从其他源码推敲用于调用自身对绑定的回调函数。

近期开发的项目中前端使用的是Vue框架,很轻量,也很好用。不过,因为用的是别人家开发的框架,代码执行的情况是否跟我们意料的一致值得思考。调试代码或者利用测试框架测试input/ouput挺好,不过我更倾向于看源码。能够被大众所广泛使用的框架的源码非常值得一看,好处就不多说了,因人而异。

这次我看的是vue源码里的eventsAPI部分,包括$emit/$broadcast/$dispatch等。

注:由于目前看到的只是冰山一角,所以牵连到其他部分的语句会暂时忽略,所以也有可能理解起来会有断章取义的可能,如果有理解错的还望指出,互相学习。在后续的源码阅读中,一有新的认识会立即更新。

eventsAPI源码位置:src/instance/api/events.js

私有函数 modifyListenerCount
var hookRE = /^hook:/
function modifyListenerCount (vm, event, count) {
  var parent = vm.$parent
  // hooks do not get broadcasted so no need
  // to do bookkeeping for them
  if (!parent || !count || hookRE.test(event)) return
  while (parent) {
    parent._eventsCount[event] =
      (parent._eventsCount[event] || 0) + count
    parent = parent.$parent
  }
}

在events.js里边多次调用到该函数,用于向上遍历父组件,更新事件计数器。

组件的_events属性,记录着每个event绑定的回调函数(数组),比如_events[event] = [func1, func2, ...].

组件的_eventsCount属性,记录着自己以及子组件对每个event绑定的回调函数的总数目。每当子组件对event事件绑定了n个回调,那父组件(一直向上遍历到根)的_eventsCount[event]会+n。目前发现,_eventsCount在$broadcast会使用到。

Vue.prototype.$on
Vue.prototype.$on = function (event, fn) {
  (this._events[event] || (this._events[event] = []))
    .push(fn)
  modifyListenerCount(this, event, 1)
  return this
}

基础函数,事件监听绑定。组件将回调函数fn保存在_events[event]中,对同一event可以绑定多个回调函数,同时,通过modifyListenerCount更新所有父组件的_eventsCount[event]。

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

$once:当event事件发生时,fn只会被调用一次,调用完成后通过$off解除绑定。

Vue.prototype.$off
Vue.prototype.$off = function (event, fn) {
  var cbs
  // all
  if (!arguments.length) {
    if (this.$parent) {
      for (event in this._events) {
        cbs = this._events[event]
        if (cbs) {
          modifyListenerCount(this, event, -cbs.length)
        }
      }
    }
    this._events = {}
    return this
  }
  // specific event
  cbs = this._events[event]
  if (!cbs) {
    return this
  }
  if (arguments.length === 1) {
    modifyListenerCount(this, event, -cbs.length)
    this._events[event] = null
    return this
  }
  // specific handler
  var cb
  var i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      modifyListenerCount(this, event, -1)
      cbs.splice(i, 1)
      break
    }
  }
  return this
}

$off:解除事件绑定,源码可以看出它的三个调用方式:

vm.$off()
不带参数:将删除组件所有绑定的事件(this._events = {}),在此之前,会遍历更新父组件的计数器。

vm.$off(event)
只带参数event:将删除组件对event绑定的所有事件,同样会遍历更新父组件的计数器。

vm.$off(event, fn)
带齐参数event和fn:将删除组件对event事件绑定的fn回调,同样会遍历更新父组件的计数器。

Vue.prototype.$emit
Vue.prototype.$emit = function (event) {
  var isSource = typeof event === "string"
  event = isSource
    ? event
    : event.name
  var cbs = this._events[event]
  var shouldPropagate = isSource || !cbs
  if (cbs) {
    cbs = cbs.length > 1
      ? toArray(cbs)
      : cbs
    // 这里的特殊处理暂且忽略,还得从其他源码推敲
    // this is a somewhat hacky solution to the question raised
    // in #2102: for an inline component listener like ,
    // the propagation handling is somewhat broken. Therefore we
    // need to treat these inline callbacks differently.
    var hasParentCbs = isSource && cbs.some(function (cb) {
      return cb._fromParent
    })
    if (hasParentCbs) {
      shouldPropagate = false
    }
    var args = toArray(arguments, 1)
    for (var i = 0, l = cbs.length; i < l; i++) {
      var cb = cbs[i]
      var res = cb.apply(this, args)
      if (res === true && (!hasParentCbs || cb._fromParent)) {
        shouldPropagate = true
      }
    }
  }
  return shouldPropagate
}

$emit:用于调用自身对event绑定的回调函数。该函数会被$broadcast和$dispatch调用,所以对参数的event进行了适配。部分变量备注:

isSource:是否是源组件发出的$emit事件。也就是说,只有直接调用vm.$emit事件或者$dispatch率先触发自己绑定的回调($dispatch源码第一行)的时候,参数是event字符串,此时isScource才为true。其他情况,如$broadcast内部调用$emit,其参数会是一个非字符串,在下面的$broadcast和$dispatch可以看到,此时的参数会是{ name: event, source: this }。

event:由isSource可以得到:event即事件(字符串)。

shouldPropagate:是否需要继续传播事件触发。源码中,遍历了event绑定的事件,除开(!hasParentCbs || cb._fromParent)这个不说,只要执行的绑定事件明确return true,shouldPropagate才会置为true。对于$progress,如果shouldPropagate为true,会触发继续向下传播事件。

Vue.prototype.$broadcast
Vue.prototype.$broadcast = function (event) {
  var isSource = typeof event === "string"
  event = isSource
    ? event
    : event.name
  // if no child has registered for this event,
  // then there"s no need to broadcast.
  if (!this._eventsCount[event]) return
  var children = this.$children
  var args = toArray(arguments)
  if (isSource) {
    // use object event to indicate non-source emit
    // on children
    args[0] = { name: event, source: this }
  }
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i]
    var shouldPropagate = child.$emit.apply(child, args)
    if (shouldPropagate) {
      child.$broadcast.apply(child, args)
    }
  }
  return this
}

此处isSource的理解跟$emit的理解差不多,指代是否最开始调用$broadcast。

这里vm._eventsCount[event]起到作用了,如果该计数为0,说明其所有子组件包括递归下去的子组件都没有对event绑定回调。

从for循环的写法可以看出,这里何时停止事件传播使用的方法类似于深度优先搜索(DFS)如下图

A组件发出$broadcast,自身不会调用监听event的事件,而是传递给子组件,子组件B1率先执行监听event的事件,其中有一个绑定事件return true,那么该B1继续传播事件,C1率先执行,C1所有监听event的回调事件都没有return true,所以C1不会往它的子组件传播事件。

到此,只是遍历完最左侧的线,接下来轮到C2执行,C2执行后再决定是否需要传递给其子组件,接下来C3....执行完B1的子组件,接下来就B2,然后...

从这里可以看出,如果某一层一个组件return true,那么会继续遍历新一层子组件,有点雪崩式的爆发,return true或许会导致性能下降,这种事件通知的机制或许需要改善改善,因为假设我只要通知B1和C1,结果还是会遍历B层其他组件还有C层其他组件,这样会消耗多余的资源,且注意,这里是同步。

Vue.prototype.$dispatch
Vue.prototype.$dispatch = function (event) {
  var shouldPropagate = this.$emit.apply(this, arguments)
  if (!shouldPropagate) return
  var parent = this.$parent
  var args = toArray(arguments)
  // use object event to indicate non-source emit
  // on parents
  args[0] = { name: event, source: this }
  while (parent) {
    shouldPropagate = parent.$emit.apply(parent, args)
    parent = shouldPropagate
      ? parent.$parent
      : null
  }
  return this
}

$dispatch相对简单,先触发自身对event绑定的回调,如果自己没有监听event的回调,则会继续调用父组件触发相应绑定的事件。如果有回调,还需要判断_fromParent这个属性,这个不知何物,待发掘。

假设A->B->C三层,B发出$dispatch("e"),想要B和A执行,那么B需要return true; C发出$dispatch("e"),想要C和B执行,那么C需要return true。但此时B也return true了,所以A也会触发。所以如果遇到这种情况,可以修改dispatch的事件名字,比如C换成$dispatch("f");或者通过传递其他参数来判断是否需要return true。(推荐前者,比较干净)

总结

Vue的eventsAPI是比较好理解的模块,在看源码以前,原以为$broadcast和$dispatch是在$nextTick实现,现在才意识到是一调用便执行。所以如果有多个地方会return true,还是需要考虑下用其他方法,不然会阻塞挺久的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90970.html

相关文章

  • vue源码分析系列之入debug环境搭建

    摘要:目标是为了可以调试版本的,也就是下的源码,所以主要是的开启。结语至此就可以开心的研究源码啦。文章链接源码分析系列源码分析系列之入口文件分析源码分析系列之响应式数据一源码分析系列之响应式数据二 概述 为了探究vue的本质,所以想debug一下源码,但是怎么开始是个问题,于是有了这样一篇记录。目标是为了可以调试es6版本的,也就是src下的源码,所以主要是sourceMap的开启。原文来自...

    nihao 评论0 收藏0
  • Vue2 transition源码分析

    摘要:至此算是找到了源码位置。至此进入过渡的部分完毕。在动画结束后,调用了由组件生命周期传入的方法,把这个元素的副本移出了文档流。这篇并没有去分析相关的内容,推荐一篇讲非常不错的文章,对构造函数如何来的感兴趣的同学可以看这里 Vue transition源码分析 本来打算自己造一个transition的轮子,所以决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,...

    Genng 评论0 收藏0
  • 你想要的——vue源码分析1

    摘要:本次分析的版本是。持续更新中。。。目录的引入的实例化的引入这一章将会分析用户在引入后,框架做的初始化工作创建这个类,并往类上添加类属性类方法和实例属性实例方法。 背景 Vue.js是现在国内比较火的前端框架,希望通过接下来的一系列文章,能够帮助大家更好的了解Vue.js的实现原理。本次分析的版本是Vue.js2.5.16。(持续更新中。。。) 目录 Vue.js的引入 Vue的实例化...

    jifei 评论0 收藏0
  • 深入Vue.js源码开始(三)

    摘要:数据驱动一个核心思想是数据驱动。发生了什么从入口代码开始分析,我们先来分析背后发生了哪些事情。函数最后判断为根节点的时候设置为,表示这个实例已经挂载了,同时执行钩子函数。这里注意表示实例的父虚拟,所以它为则表示当前是根的实例。 数据驱动 Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传...

    lentrue 评论0 收藏0
  • Vue2 源码分析

    摘要:应用启动一般是通过,所以,先从该构造函数着手。构造函数文件该文件只是构造函数,原型对象的声明分散在当前目录的多个文件中构造函数接收参数,然后调用。 源码版本:v2.1.10 分析目标 通过阅读源码,对 Vue2 的基础运行机制有所了解,主要是: Vue2 中数据绑定的实现方式 Vue2 中对 Virtual DOM 机制的使用方式 源码初见 项目构建配置文件为 build/conf...

    alin 评论0 收藏0

发表评论

0条评论

DoINsiSt

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<