前言
面试的时候经常被问一些Vue源码相关的问题,通常情况下, 我会在面试前恶补掘金上的面筋来对付面试,什么双向绑定的原理呀,什么虚拟dom树呀,实际上我压根儿就没仔细研究过,其一是自己真的比较菜,其二工作上也用不上,别自己给自己添堵。但后面想一下,很多事情,为之则易,不为则难,给自己设立困难(负重)才能进步,决定每天多一点Vue的源码,在Vue的源码选择上,我选择了最老的版本(0.1)😬(真的怕自己看起来吃力), 阅读的模式为通读,从易到难一个文件一个文件的看,看完一个文件后再看它的单元测试,等完全吃透后复制粘贴代码到本地运行测试用例为代码块写一些中文注释,打上tag推到自己的仓库,开始梳理写文章总结(之前有犹豫过是否应该在掘金上写文章,因为这类Vue源码解析的文章已经很多了,而且还写的很好,我再写一遍是否还存在意义,后面想还是写吧,流水总结也不错💧)。
正文
这是我发的第二篇关于Vue源码的文章,刚开始读Vue源码时,我选择的阅读文件(模块)都是些代码量少,相对独立的功能。比如Batcher和Binding的代码加起来不足200行,同时Binding里面也引入Batcher,Batcher+Binding应该能写出一篇总结的文章来,Binding可能现在我理解的不是很透彻哦,因为在Binding的构造函数里面有(compiler, directive, value应该是一个observer,发发布者)等这些内容目前还没有阅读,我可以根据字面意思猜测的,不影响整体阅读。
简单介绍
- Batcher是异步批量处理任务队列,有三个方法push进入队列,flush批量出队列,批量执行,reset重置队列。
- Binding直译“捆绑”,通过Batcher异步更新与Binding所"捆绑"值(value)相关联的内容比如(dirs,subs),等下会画一张图来解释相关联的内容不保证准确(发现不准确的地方,可以直接指出)。
Batcher
Batcher就50多行代码,我直接贴代码了,然后关键的地方会写满注释的,那句话这么说的来着, 好像是
taking is cheap, show me the code
哈哈哈哈😄
// 构造函数
function Batcher() {
this.reset()
}
// 原型链的别名
var BatcherProto = Batcher.prototype
// reset方法
BatcherProto.reset = function () {
// utils.js里面的方法,utils.hash => Object.create(null)
// 它返回一个没有原型链的空对象,
// this.has = {}, 但是this.has.__proto__ === null
this.has = utils.hash() // job.id为key
this.queue = [] // 队列
this.waiting = false // 是否需要等待批量处理
}
// 任务进队列
// job由2部分组成,id与execute函数
BatcherProto.push = function (job) {
if (!job.id || !this.has[job.id]) {
this.queue.push(job)
this.has[job.id] = job
if(!this.waiting){
this.waiting = true
// nextTick的内部实现为 requestAnimationFrame || setTimeout(fn, 0)
// bind的实现, 返回一个闭包函数,封装了call,它不是通过bind实现的
utils.nextTick(utils.bind(this.flush, this))
}
}else if(job.override){ // 是否覆盖
var oldJob = this.has[job.id]
oldJob.cancelled = true // oldJob不执行
this.queue.push(job)
this.has[job.id] = job
}
}
// 任务批量出队列执行
BatcherProto.flush = function () {
// before flush hook
// 钩子
if (this._preFlush) this._preFlush()
// do not cache length because more jobs might be pushed
// as we execute existing jobs
// 源码的注释描述的是这样的情况,
// waiting为true,flush已经进入javascript的消息队列了,
//这时候this.queue依然可能有job,push进队列,
//或者在执行这些jobs的时候也可能,所以不要缓存length
for (var i = 0; i < this.queue.length; i++) {
var job = this.queue[i]
if (!job.cancelled) {
job.execute()
}
}
this.reset()
}
Binding
从上图可以知道当binding更新时要通知dirs和subs更新,一个binding里面可能有很多的dirs和subs,他们的更新有时可能比较耗时,所以把他们的更新放入异步队列批量处理。 下面贴出binding里面核心方法update的代码:Vue实例里的属性都有一个对应的Binding, 而DOM上面的指令以及一些计算属性依赖于Binding(我蹩脚的翻译)。下面我画一张图来解释里面的一些属性之间的关系。
BindingProto.update = function (value) {
// 如果不是计算属性 或者 是函数,直接赋值
if (!this.isComputed || this.isFn) {
this.value = value
}
// directives subs不为空
if (this.dirs.length || this.subs.length) {
var self = this
// 进入异步队列,批量处理
bindingBatcher.push({
id: this.id,
execute: function () {
if (!self.unbound) {
self._update()
}
}
})
}
}
/**
* Actually update the directives.
*/
BindingProto._update = function () {
var i = this.dirs.length,
value = this.val()
while (i--) {
this.dirs[i].$update(value) // dirs更新
}
this.pub() // 通知subs更新,
}
BindingProto.pub = function () {
var i = this.subs.length
while (i--) {
this.subs[i].update()
}
}
解释下Binding的构造函数里面的属性:
- deps(dependences: 依赖)
- dirs(DOM上的指令)
- subs翻译子类,或者可以理解依赖binding值的计算属性
通过上面的解释,你大概了解了他们的字面含义,下面通过Binding的构造函数和unbinding方法,可以知道subs, deps, binding之间的一些关系。
- Binding的构造函数:
function Binding(compiler, key, isExp, isFn) {
// ...
this.dirs = [] // directive 指令
this.subs = []
this.deps = []
// ...
}
- unbinding方法(可以理解为析构函数):
BindingProto.unbind = function () {
// ...
var subs
while (i--) {
subs = this.deps[i].subs
var j = subs.indexOf(this)
if (j > -1) subs.splice(j, 1)
}
// ...
}
根据上面的代码你可以得知
- binding.deps.subs中会包含binding
- binding.deps是一个binding实例数组
- binding.subs也是一个binding实例数组
- binding与subs,binding与deps都是一对多的关系。
总结
今天的Batcher和Binding好像没啥知识点,算是一篇流水账日记类的文章😢😢😢...
代码地址
本地运行测试用例遇到的坑: 单元测试里面有异步操作,我当时在本地运行的时候测试通过,改了一点代码,karma不是watch变化,自动运行测试用例,terminal上就显示timeout, 查了很久也不知道为啥,后面发现刷新一下karma起的浏览器窗口测试用例又通过了。
持续更新...❤️