阅读 1008

Emitter | 每天读一点Vue源码

前言

面试的时候经常被问一些Vue源码相关的问题,通常情况下, 我会在面试前恶补掘金上的面筋来对付面试,什么双向绑定的原理呀,什么虚拟dom树呀,实际上我压根儿就没仔细研究过,其一是自己真的比较菜,其二工作上也用不上,别自己给自己添堵。但后面想一下,很多事情,为之则易,不为则难,给自己设立困难(负重)才能进步,决定每天多一点Vue的源码,在Vue的源码选择上,我选择了最老的版本(0.1)😬(真的怕自己看起来吃力), 阅读的模式为通读,从易到难一个文件一个文件的看,看完一个文件后再看它的单元测试,等完全吃透后复制粘贴代码到本地运行测试用例为代码块写一些中文注释,打上tag推到自己的仓库,开始梳理写文章总结(之前有犹豫过是否应该在掘金上写文章,因为这类Vue源码解析的文章已经很多了,而且还写的很好,我再写一遍是否还存在意义,后面想还是写吧,流水总结也不错💧)。

正文

简单介绍

Emitter直译过来为发射器,Emitter的功能类似于dom对象事件,Emitter通过emit触发事件, dom通过dispatchEvent触发, Emitter通过on函数注册事件,dom对象通过addListener注册事件,dom和Emitter是不是可以看成你自己,自己和自己聊天,emit发,on收。

代码细节

要实现上面说到的功能实现思路大致为Emitter需要一个容器来存放通过on注册的处理函数,Emitter调用emit的时候,内部再调用on注册的处理函数,就两步是不是看起来很简单,下面的最简代码。

function Emitter () {
    this._cbs = {} // 容器
}
var EmitterProto = Emitter.prototype
EmitterProto.on = function (event, fn) {
    (this._cbs[event] = this._cbs[event] || []).push(fn) // 放入容器
}
EmitterProto.emit = function (event) {
    var callbacks = this._cbs[event], args
    if (callbacks) {
        callbacks = callbacks.slice(0)
        args = slice.call(arguments, 1)
        for (var i = 0, len = callbacks.length; i < len; i++) {
            callbacks[i].apply(this._ctx, args) // 把注册的处理函数,遍历调用
        }
    }
}
复制代码

代码优化

调用优化

场景:现在要在Emitter上注册set,get,mutate三个事件,你应该会这样写:

var __emitter__ = new Emitter()
__emitter__.on('set', function(){
    // ...
})
__emitter__.on('get', function(){
    // ...
})
__emitter__.on('mutate', function(){
    // ...
})
复制代码

能不能像jquery那样链式调用了,如果是链式调用,代码应该如下:

var __emitter__ = new Emitter()
__emitter__.on('set', function(){
    // ...
}).on('get', function(){
    // ...
}).on('mutate', function(){
    // ...
})
复制代码

要怎么实现链式调用呢?其实很简单,返回this就可以了,

// ...
EmitterProto.on = function (event, fn) {
    // ...
    return this
}
// ...
复制代码

这样的话,其他人使用你的Emitter就更加方便灵活了(服务他人🤘)。

性能优化

在emit函数里面,我们看见了这行代码callbacks[i].apply(this._ctx, args),掘金上有很多文章分析过apply/call的性能,结论是apply相对于call更耗能,Vue里面有成千上万的事件触发(Emitter会结合observer使用的),不是非常耗能吗? 其实在源码里面,上面的emit实现是applyEmit, 而emit函数接受固定的参数的,代码如下:

/**
 *  The internal, faster emit with fixed amount of arguments
 *  using Function.call
 */
 // 我们可以在observer的源码里看到: a为key, b为value, c: propagate
EmitterProto.emit = function (event, a, b, c) {}
复制代码

总结

通过对Emitter的阅读我们可以学到一个类似dom对象事件的内部实现,学习到链式调用,学习到call/apply之间的性能差异(大家可以搜掘金上相关的文章)。阅读源码的乐趣就在这里,总能通过源码学到以前没有注意到的知识,哈哈哈😄。

代码地址

详细的源码, 有中文注释

emitter无单元测试,大概是太简单了😓

持续更新...❤️