比如上面的代码,set内部的处理的代码就与整个数据响应化相耦合,如果下次我们想要在set中做其他的操作,就必须要修改set函数内部的内容,这是非常不友好的,不符合开闭原则(OCP: Open Close Principle)。当然Vue不会采用这种方式去设计,为了解决这个问题,Vue引入了发布-订阅模式。其实发布-订阅模式是前端工程师非常熟悉的一种模式,又叫做观察者模式,它是一种定义对象间一种一对多的依赖关系,当一个对象的状态发生改变的时候,其他观察它的对象都会得到通知。我们最常见的DOM事件就是一种发布-订阅模式。比如:
function observifyArray(array){
var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayAugmentations = Object.create(Array.prototype);
aryMethods.forEach((method)=> {
let original = Array.prototype[method];
arrayAugmentations[method] = function () {
// 调用对应的原生方法并返回结果
// do everything you what do !
return original.apply(this, arguments);
};
});
array.__proto__ = arrayAugmentations;
}
// p === Observer.prototype
p.link = function (items, index) {
index = index || 0
for (var i = 0, l = items.length; i < l; i++) {
this.observe(i + index, items[i])
}
}
p.observe = function (key, val) {
var ob = Observer.create(val)
if (ob) {
// register self as a parent of the child observer.
var parents = ob.parents
if (!parents) {
ob.parents = parents = Object.create(null)
}
if (parents[this.id]) {
_.warn('Observing duplicate key: ' + key)
return
}
parents[this.id] = {
ob: this,
key: key
}
}
}
p.updateIndices = function () {
var arr = this.value
var i = arr.length
var ob
while (i--) {
ob = arr[i] && arr[i].$observer
if (ob) {
ob.parents[this.id].key = i
}
}
}
接着看函数propagate:
p.propagate = function (event, path, val, mutation) {
this.emit(event, path, val, mutation)
if (!this.parents) return
for (var id in this.parents) {
var parent = this.parents[id]
if (!parent) continue
var key = parent.key
var parentPath = path
? key + Observer.pathDelimiter + path
: key
parent.ob.propagate(event, parentPath, val, mutation)
}
}
前言
首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。接下来的日子我应该会着力写一系列关于Vue与React内部原理的文章,感兴趣的同学点个关注或者Star。 之前的两篇文章响应式数据与数据依赖基本原理和从Vue数组响应化所引发的思考我们介绍了响应式数据相关的内容,没有看的同学可以点击上面的链接了解一下。如果大家都阅读过上面两篇文章的话,肯定对这方面内容有了足够的知识储备,想来是时候来看看Vue内部是如何实现数据响应化。目前Vue的代码非常庞大,但其中包含了例如:服务器渲染等我们不关心的内容,为了能集中于我们想学习的部分,我们这次阅读的是Vue的早期代码,大家可以
checkout
到这里查看对应的代码。 之前零零碎碎的看过React的部分源码,当我看到Vue的源码,觉得真的是非常优秀,各个模块之间解耦的非常好,可读性也很高。Vue响应式数据是在Observer
模块中实现的,我们可以看看Observer
是如何实现的。发布-订阅模式
如果看过上两篇文章的同学应该会发现一个问题:数据响应化的代码与其他的代码耦合太强了,比如说:
比如上面的代码,
set
内部的处理的代码就与整个数据响应化相耦合,如果下次我们想要在set
中做其他的操作,就必须要修改set
函数内部的内容,这是非常不友好的,不符合开闭原则(OCP: Open Close Principle)。当然Vue不会采用这种方式去设计,为了解决这个问题,Vue引入了发布-订阅模式。其实发布-订阅模式是前端工程师非常熟悉的一种模式,又叫做观察者模式,它是一种定义对象间一种一对多的依赖关系,当一个对象的状态发生改变的时候,其他观察它的对象都会得到通知。我们最常见的DOM事件就是一种发布-订阅模式。比如:在上面的代码中我们监听了
body
的click
事件,虽然我们不知道click
事件什么时候会发生,但是我们一定能保证,如果发生了body
的click
事件,我们一定能得到通知,即回调函数被调用。在JavaScript中因为函数是一等公民,我们很少使用传统的发布-订阅模式,多采用的是事件模型的方式实现。在Vue中也实现了一个事件模型,我们可以看一下。因为Vue的模块之间解耦的非常好,因此在看代码之前,其实我们可以先来看看对应的单元测试文件,你就知道这个模块要实现什么功能,甚至如果你愿意的话,也可以自己实现一个类似的模块放进Vue的源码中运行。Vue早期代码使用是
jasmine
进行单元测试,emitter_spec.js
是事件模型的单元测试文件。首先简单介绍一下jasmine
用到的函数,可以对照下面的代码了解具体的功能:describe
是一个测试单元集合it
是一个测试用例beforeEach
会在每一个测试用例it
执行前执行expect
期望函数,用作对期望值和实际值之间执行逻辑比较createSpy
用来创建spy,而spy的作用是监测函数的调用相关信息和函数执行参数可以看出
Emitter
对象实例对外提供以下接口:on
: 注册监听接口,参数分别是事件名和监听函数emit
: 触发事件函数,参数是事件名off
: 取消对应事件的注册函数,参数分别是事件名和监听函数once
: 与on
类似,仅会在第一次时通知监听函数,随后监听函数会被移除。看完了上面的单元测试代码,我们现在已经基本了解了这个模块要干什么,现在让我们看看对应的代码:
我们可以看到上面的代码采用了原型模式创建了一个
Emitter
类。配合Karma跑一下这个模块 ,测试用例全部通过,到现在我们已经阅读完Emitter
了,这算是一个小小的热身吧,接下来让我们正式看一下Observer
模块。Observer
对外功能
按照上面的思路我们先看看
Observer
对应的测试用例observer_spec.js
,由于Observer
的测试用例非常长,我会在代码注释中做解释,并尽量精简测试用例,能让我们了解模块对应功能即可,希望你能有耐心阅读下来。源码实现
数组
能坚持看到这里,我们的长征路就走过了一半了,我们已经知道了
Oberver
对外提供的功能了,现在我们就来了解一下Oberver
内部的实现原理。Oberver
模块实际上采用采用组合继承(借用构造函数+原型继承)方式继承了Emitter
,其目的就是继承Emitter
的on
,off
,emit
等方法。我们在上面的测试用例发现,我们并没有用new
方法直接创建一个Oberver
的对象实例,而是采用一个工厂方法Oberver.create
方法来创建的,我们接下来看源码,由于代码比较多我会尽量去拆分成一个个小块来讲:我们首先从
Observer.create
看起,如果value
值没有响应化过(通过是否含有$observer
属性去判断),则使用new操作符创建Obsever实例(区分对象OBJECT与数组ARRAY)。接下来我们看Observer
的构造函数是怎么定义的,首先借用Emitter
构造函数:配合原型继承
从而实现了组合继承
Emitter
,因此Observer
继承了Emitter
的属性(ctx
)和方法(on
,emit
等)。我们可以看到Observer
有以下属性:id
: 响应式数据的唯一标识value
: 原始数据type
: 标识是数组还是对象parents
: 标识响应式数据的父级,可能存在多个,比如var obj = { a : { b: 1}}
,在处理{b: 1}
的响应化过程中parents
中某个属性指向的就是obj
的$observer
。我们接着看首先给该数据赋值
$observer
属性,指向的是实例对象本身。_.define
内部是通过defineProperty
实现的:下面我们首先看看是怎么处理数组类型的数据的
如果看过我前两篇文章的同学,其实还记得我们对数组响应化当时还做了一个着重的原理讲解,大概原理就是我们通过给数组对象设置新的原型对象,从而遮蔽掉原生数组的变异方法,大概的原理可以是:
回到Vue的源码,虽然我们知道基本原理肯定是相同的,但是我们仍然需要看看
arrayAugmentations
是什么?下面arrayAugmentations
代码比较长。我们会在注释里面解释基本原理:上面的代码相对比较长,具体的解释我们在代码中已经注释。到这里我们已经了解完
arrayAugmentations
了,我们接着看看_.augment
做了什么。我们在文章从Vue数组响应化所引发的思考中讲过Vue是通过__proto__
来实现数组响应化的,但是由于__proto__
是个非标准属性,虽然广泛的浏览器厂商基本都实现了这个属性,但是还是存在部分的安卓版本并不支持该属性,Vue必须对此做相关的处理,_.augment
就负责这个部分:我们看到如果浏览器不支持
__proto__
话调用deepMixin
函数。而deepMixin
的实现也是非常的简单,就是使用Object.defineProperty
将原对象的属性描述符赋值给目标对象。接着调用了函数:关于
link
函数在上面的备注中我们已经见过了:当时我们的解释是将新插入的数据响应化,知道了功能我们看看代码的实现:
其实代码逻辑非常简单,
link
函数会对给定数组index(默认为0)之后的元素调用this.observe
, 而observe
其实也就是对给定的val
值递归调用Observer.create
,将数据响应化,并建立父级的Observer与当前实例的对应关系。前面其实我们发现Vue不仅仅会对插入的数据响应化,并且也会对删除的元素调用unlink
,具体的调用代码是:之前我们大致讲过其用作就是对删除的数据解除响应化,我们来看看具体的实现:
代码非常简单,就是对数据调用
unobserve
,而unobserve
函数的主要目的就是解除父级observer
与当前数据的关系并且不再保留引用,让浏览器内核必要的时候能够回收内存空间。在
arrayAugmentations
中其实还调用过Observer
的两个原型方法,一个是:另一个是:
首先看看
updateIndices
函数,当时的函数的作用是更新子元素在parents的key,来看看具体实现:接着看函数
propagate
:我们之前说过
propagate
函数的作用的就是触发自身及其递归触发父级的事件,首先调用emit
函数对外触发时间,其参数分别是:事件名、路径、值、mutatin
对象。然后接着递归调用父级的事件,并且对应改变触发的path
参数。parentPath
等于parents[id].key
+Observer.pathDelimiter
+path
到此为止我们已经学习完了Vue是如何处理数组的响应化的,现在需要来看看是如何处理对象的响应化的。
对象
在
Observer
的构造函数中关于对象处理的代码是:和数组一样,我们首先要了解一下
objectAugmentations
的内部实现:相比于
arrayAugmentations
,objectAgumentations
内部实现则简单的多,objectAgumentations
添加了两个方法:$add
与$delete
。$add
用于给对象添加新的属性,如果该对象之前就存在键值为key
的属性则不做任何操作,否则首先使用_.define
赋值该属性,然后调用ob.observe
目的是递归调用使得val
值响应化。而convert
函数的作用是将该属性转换成访问器属性getter/setter
使得属性被访问或者被改变的时候我们能够监听到,具体我可以看一下convert
函数的内部实现:convert
函数的内部实现也不复杂,在get
函数中,如果开启了全局的Observer.emitGet
开关,在该属性被访问的时候,会对调用propagate
触发本身以及父级的对应get
事件。在set
函数中,首先调用unobserve
对之间的值接触响应化,接着调用ob.observe
使得新赋值的数据响应化。最后首先触发本身的set:self
事件,接着调用propagate
触发本身以及父级的对应set
事件。$delete
用于给删除对象的属性,如果不存在该属性则直接退出,否则先用delete
操作符删除对象的属性,然后对外触发本身的delete:self
事件,接着调用delete
触发本身以及父级对应的delete
事件。看完了
objectAgumentations
之后,我们在Observer
构造函数中知道,如果传入的参数中存在op.doNotAlterProto
意味着不要改变对象的原型,则采用deepMixin
函数将$add
和$delete
函数添加到对象中,否则采用函数arguments
函数将$add
和$delete
添加到对象的原型中。最后调用了walk
函数,让我们看看walk
是内部是怎么实现的:首先遍历
obj
中的各个属性,如果是以$
或者_
开头的属性名,则不做处理。接着获取该属性的描述符,如果不存在get
函数,则对该属性值调用observe
函数,使得数据响应化,然后调用convert
函数将该属性转换成访问器属性getter/setter
使得属性被访问或者被改变的时候能被够监听。总结
到此为止,我们已经看完了整个
Observer
模块的所有代码,其实基本原理和我们之前设想都是差不多的,只不过Vue代码中各个函数分解粒度非常小,使得代码逻辑非常清晰。看到这里,我推荐你也clone一份Vue源码,checkout到对应的版本号,自己阅读一遍,跑跑测试用例,打个断点试着调试一下,应该会对你理解这个模块有所帮助。最后如果对这个系列的文章感兴趣欢迎大家关注我的Github博客算是对我鼓励,感谢大家的支持!