exports._updateAdd = function (path) {
var index = path.lastIndexOf(Observer.pathDelimiter)
if (index > -1) path = path.slice(0, index)
this._updateBindingAt(path)
}
var _ = require('./util')
function Batcher () {
this._preFlush = null
this.reset()
}
var p = Batcher.prototype
p.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(this.flush, this)
}
} else if (job.override) {
var oldJob = this.has[job.id]
oldJob.cancelled = true
this.queue.push(job)
this.has[job.id] = job
}
}
p.flush = function () {
// before flush hook
if (this._preFlush) {
this._preFlush()
}
// do not cache length because more jobs might be pushed
// as we run existing jobs
for (var i = 0; i < this.queue.length; i++) {
var job = this.queue[i]
if (!job.cancelled) {
job.run()
}
}
this.reset()
}
p.reset = function () {
this.has = {}
this.queue = []
this.waiting = false
}
module.exports = Batcher
exports.applyFilters = function (value, filters) {
if (!filters) {
return value
}
for (var i = 0, l = filters.length; i < l; i++) {
value = filters[i](value)
}
return value
}
var p = Watcher.prototype
p.initDeps = function (paths) {
var i = paths.length
while (i--) {
this.addDep(paths[i])
}
this.value = this.get()
}
p.addDep = function (path) {
var vm = this.vm
var newDeps = this.newDeps
var oldDeps = this.deps
if (!newDeps[path]) {
newDeps[path] = true
if (!oldDeps[path]) {
var binding =
vm._getBindingAt(path) ||
vm._createBindingAt(path)
binding._addSub(this)
}
}
}
p.update = function () {
batcher.push(this)
}
p.run = function () {
if (this.active) {
var value = this.get()
if (
(typeof value === 'object' && value !== null) ||
value !== this.value
) {
var oldValue = this.value
this.value = value
this.cb.call(this.ctx, value, oldValue)
}
}
}
p.teardown = function () {
if (this.active) {
this.active = false
var vm = this.vm
for (var path in this.deps) {
vm._getBindingAt(path)._removeSub(this)
}
}
}
前言
首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。接下来的日子我应该会着力写一系列关于Vue与React内部原理的文章,感兴趣的同学点个关注或者Star。
回顾
上一篇文章Vue响应式数据: Observer模块实现我们介绍Vue早期代码中的
Observer
模块,Observer
模块实现了数据的响应化并且用监听者模式对外提供功能。任何模块想要感知到数据变化,只要监听Observer
模块对应的事件,从而将整个数据响应化的过程与相应的处理逻辑相独立。其实我们可以思考一下,在Vue中一个响应式数据发生改变可能会触发那些逻辑呢?可能是一个对应的DOM改变,也可能是一个
watch
侦听器的回调函数的调用,或者是导致一个computed
计算属性函数的调用。其实在之前的文章响应式数据与数据依赖基本原理我们就引入了一个Dep
和Watcher
的概念。同时还附上了一个概念图:我们当时为了解耦响应式数据和对应的数据改变后处理逻辑,我们增加了
Dep
和Watcher
的概念,每一个响应式数据都有一个Dep
用于集中管理和维护该数据改变时对应执行回调函数。当数据改变时,我们不需要直接触发所有的回调函数,而是去通知对应的数据的Dep
,然后由Dep
去执行相应的逻辑。将这个概念抽象出现出来,其基本逻辑就是上图所示。了解设计模式的同学,应该很快就能意识到这是一个代理模式。引入
Dep
的目的其实也就是代理模式的优点,分离调用者和被调用者的逻辑,降低耦合性。可见设计模式在软件开发中作用是非常广泛的,甚至有时候你都没有意识到它的存在。我们前面说过,响应式数据改变后可能对应的是DOM修改的处理逻辑或者是
watch
函数对应的处理逻辑。为了弱化不同类型的处理逻辑,我们引入了Watcher
类。Dep
并不会关心每一个不同的注册者的逻辑,Dep
只知道每一个注册者都是一个Watcher
的实例,每次发生改变时只需要调用对应的update
方法,具体的逻辑被隐藏在update
函数之后。Vue的早期实现思路
Vue的内部实现逻辑基本上和我们的逻辑是一样的。由
bindings
模块负责上面所讲的Dep
的功能。bindings模块
在Vue组件的初始化函数
_init
中调用了:目的就是创建组件对应的binding Tree,在研究
_initBindings
函数之前,我们先看看Binding
:Binding
类的定义非常简单,内部属性_subs
数组用来存储对应的订阅者(subscription),也就是我们后面将要说的Watcher
,原型方法分别是:_addSub
: 用来增加对应的订阅者_removeSub
: 用来删除对应的订阅者_notify
: 通知所有的订阅者,其实就是遍历整个订阅者数据,并调用对应的update
方法。_addChild
: 用来增加一个属性名为key
值的子Binding
,其实也就用来构建Binding
Tree。看完
Binding
类我们接着看_initBindings
函数的定义:_initBindings
是在初始化Vue组件实例中调用的,因此this
也就是指向的是当前的Vue实例对象。 首先我们看到给Vue的实例对象中创建了私有属性_rootBinding
,作为Bindings Tree的根节点,并且_rootBinding
的$data
属性指向就是根节点本身。如果当前的Vue实例中存在父节点($parent
),则通过给给_rootBinding
添加$parent
属性来构建起与父级Bindings
Tree的关联。我们知道Bindings
的主要作用就是在响应式数据改变时触发对应的逻辑,因此_initBindings
函数监听了实例属性_observer
的各个事件。set
: 监听响应式数据对象属性值修改mutate
: 监听响应式数据数组修改delete
: 监听响应式数据对象属性删除add
: 监听响应式数据对象属性增加get
: 监听响应式数据某个属性被调用我们看到对于
set
、mutate
、delete
事件我们都调用了内部的_updateBindingAt
函数,接着看_updateBindingAt
函数定义:假如说数据是下面格式:
当设置
vm.a.b = 2
时,我们调用_updateBindingAt
的path
为ab
。_updateBindingAt
函数首先会任何数据变化的时候都通知调用根级_rootBinding
中的所有订阅者,然后调用_getBindingAt
函数去获得当前路径ab
的binding
,如果存在,则通知调用所有的订阅者(下面箭头指向的就是对应路径ab
的Binding
)。关于Path
模块我们这里不做过多的介绍,我们只要知道Path.getFromObserver
函数能遍历Binding Tree
找到对应路径的Binding
。接下来我们当响应式数据触发
add
事件时就会触发_updateAdd
函数:假设是下列的数据格式:
当我们执行
vm.a.$add("b", 1)
时,此时函数_updateAdd
的参数path
为ab
,但是对应的binding
还未创建,因此对应的Watcher
还没有依赖到该Binding
。对于这种不存在Binding
的Watcher
,会暂时依赖于父级的Binding
,因此函数_updateAdd
也就是找到了对应父级的Binding
,然后通知调用所有的订阅者。 接下来触发响应式数据的get
事件时,对应调用函数:函数
_collectDep
的主要目的就是收集依赖,当get
事件触发的时候,会将_activeWatcher
添加到路径path
的Binding
中。关于_activeWatcher
与addDep
函数,马上我们会在Watcher
模块中介绍到。Watcher模块
我们前面已经讲过,
Binding
中的订阅者都是Watcher
实例,Binding
并不关心数据更改后的操作,对于Binding
而言只需要调用订阅者的update
方法,具体的处理逻辑都隐藏在Watcher
的背后。对于Watcher
,其逻辑可能是一个指令directive
(用于连接DOM与响应式数据)或者是一个watch
侦听器的回调函数,这样就能符合单一职责原则,解除模块之间的耦合度,让程序更易维护。在介绍
Watcher
之前,我们先介绍一下Batcher
模块,顾名思义,主要就是批处理任务,看过React源码的同学应该也在其中看到过相似的概念。在这些框架中,有可能是因为某个操作过于昂贵(比如DOM操作),我们如果数据一改变就触发相应的操作其实是不合适的,比如:其实两次操作下来,我们的完全可以不需要进行操作,因为前后数据并没有发生改变,这时如果我们进行批量处理,将两次操作统一起来,就能在一定程度提升效率。
Batcher
内部有四个属性并对外提供三个方法:属性:
has
: 用来记录某个任务(job)是否已经在队列中queue
: 用来存储当前的任务队列waiting
: 用来表示当前的任务队列处于等待执行状态_preFlush
: 用来在执行任务队列前调用的钩子函数方法:
reset
:重置参数属性push
: 将任务放入批处理队列flush
: 执行批处理队列中的所有任务上面的代码逻辑非常简单,不用逐一介绍,值得注意的是,每一个任务
job
都含有id
属性,用来唯一标识任务,如果当前任务队列中已经存在并且任务的override
属性为false
就不会重复放入。override
属性就是用来表示是否需要覆盖已经存在的任务。任务的cancelled
属性用来表示该任务是否需要被取消执行。所有的任务job
中的run
方法就是任务所需要执行的内容。关于Vue.nextTick
之后的文章我们会介绍,现在你可以就可以简单理解成setTimeOut
。接下来我们来看一下
Watcher
模块的实现:Watcher
模块主要做的就是解析表达式,从中收集依赖并且在数据改变的时候调用注册的回调函数。vm
: 就是对应的响应式数据所在的Vue实例expression
: 待解析的表达式cb
: 注册的回调函数,在数据改变时会调用ctx
: 回调函数执行的上下文id
:Watcher
的标识,用在Batcher
对应的job.id
,每一个Watcher
其实就是一个job
value
: 表达式expression
对应的计算值active
: 该watcher
是否是激活的deps
: 用来存储当前Watcher
依赖的数据路径在整个
Watcher
构造函数中我们需要注意的是两个部分:和
第一部分对应的就是过滤器的处理,比如存在:
那么
Watcher
在解析表达式a+b|abs
,得到对应的结果值就是1
。_.resolveFilters
函数将filters
解析成readFilters
和writeFilters
,其实本人也是从Vue2.0才开始入手的,之前并没有听过还存在什么writeFilter
,于是翻看了Vue1.0的文档,找了已经被废弃的Vue1.0的双向过滤器。比如:currency
过滤器就是双边过滤器,当在输入框中输入例如:$12
的时候,我们发现vm.price
已经被赋值为12
。这就是write
过滤器生效的结果。我们来看一下工具类
utils
中filter
模块所提供的两个方法resolveFilters
与applyFilters
:resolveFilters
在被Watcher
调用的时候,vm
参数对应的就是Vue的实例,而target
则是Watcher
实例本身,传入的filters
就比较特殊了,比如我们上面的例子:a+b|abs
,对应的filters
就是我们看到
filters
是一个数组,其实每个元素的name
对应的就是应用的过滤器函数名,而args
则是传入的预定的其他参数。代码的逻辑非常的简单,遍历filters
数组,将其中的每一个使用到的过滤器从vm.$options.filters
取出,将对应的read
和write
包装成新的函数,并分别放入res.read
与res.write
,并将res
返回。然后配合下面的模块提供的applyFilters
函数,我们就可以一个值经过给定的一系列过滤器处理,得到最终的数值了。第二部分代码:
涉及到的就是表达式的处理,我们之前讲过,每个
Watcher
其实都是从一个表达式中收集依赖,并且在相应的数据发生改变的时候调用对应的回调函数,expParser
模块不是我们本次文章的重点内容,我们不需要知道它是怎么实现的,我们只要只要它是做什么的,可以看下面的代码:其实从上面两个测试用例中我们已经能看出
expParser.parse
的功能了,expParser.parse
能转化一个表达式,返回值res
中的paths
表示表达式依赖数据的根路径,get
函数用于从一个值域scope
中取得表达式对应的计算值。而set
函数用于给值域scope
中设置表达式的值。我们接着看
this.initDeps(res.paths)
initDeps
函数的首先就是将表达式依赖根路径中的每一个值调用函数addDep
,将Watcher
实例添加进入对应的Binding
中,addDep
内部实现也是非常的简洁,调用_getBindingAt
函数(已经存在对应的Binding
)或者_createBindingAt
(创建新的Binding
)获取到对应Binding
并将自身添加进入。newDeps
用来记录此次addDep
过程中之前不存在的依赖项。之后initDeps
函数调用了this.get()
获取当前表达式对应的值。get
函数内部实质就是调用表达式对应的get
函数获取表达式当前对应的结果,然后通过applyFilters
得到当前表达式对应过滤器处理后的值。值得注意的是,在调用之前执行了钩子函数beforeGet
,其目的就是开启Observer
的emitGet
使得我们可以接受响应式数据的get
事件,然后将当前Vue实例的_activeWatcher
赋值成当前的Watcher
并置空newDeps
准备存储本次新增的依赖数据项。我们在Binding
提到过:beforeGet
所作的就是为了收集依赖所做的准备。afterGet
所做的就是清除为依赖收集所做准备,逻辑和beforeGet
正好相反。我们知道
Watcher
会在相应的响应式数据改变的时候被对应Binding
所调用,因此Watcher
一定包含方法update
:update
并没有理解调用对应回调函数,而且将Watcher
放入Batcher
队列,Batcher
会在恰当的时间调用Watcher
的run
函数。run
内部会调用this.get()
得到表达式当前的计算值,并且触发回调函数。Watcher
还有一个函数用于从所有的依赖的Binding
中移除自身:teardown
内部逻辑非常简单,不再赘述。总结
讲到这里大家可能被我粗糙的文笔搞的混乱了,我们举个例子来看看,理顺一下思路,假设存在下面的例子:
对应于上面的数据,相应的构造好的Binding Tree如下的:
我们在调用
this.get
去收集表达式a.b.c+d.c.e
的对应值时,我们会被Observer
模块的get
事件触发六次,分别对应的值为:a
a.b
a.b.c
d
d.e
d.e.f
因此此时的
Watcher
中的dep
存储的就是对应的依赖路径:而此时的
Watcher
实例在Binding Tree
的注册情况如下:到此为止,我们已经了解响应式数据是如何与Watcher对应的和响应式数据改变时触发相应的操作。逻辑虽说不算特别难,但是还是有一定的复杂度的,建议可以对应看看源码,调试一下疑惑的地方,相信会有更多的收获。
如果文章有不正确的地方欢迎指正。最后还是希望大家能给我的Github博客点个Star。愿共同学习,一同进步。