You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
比如上面的代码,set内部的处理的代码就与整个数据响应化相耦合,如果下次我们想要在set中做其他的操作,就必须要修改set函数内部的内容,这是非常不友好的,不符合开闭原则(OCP: Open Close Principle)。当然Vue不会采用这种方式去设计,为了解决这个问题,Vue引入了发布-订阅模式。其实发布-订阅模式是前端工程师非常熟悉的一种模式,又叫做观察者模式,它是一种定义对象间一种一对多的依赖关系,当一个对象的状态发生改变的时候,其他观察它的对象都会得到通知。我们最常见的DOM事件就是一种发布-订阅模式。比如:
functionobservifyArray(array){vararyMethods=['push','pop','shift','unshift','splice','sort','reverse'];vararrayAugmentations=Object.create(Array.prototype);aryMethods.forEach((method)=>{letoriginal=Array.prototype[method];arrayAugmentations[method]=function(){// 调用对应的原生方法并返回结果// do everything you what do !returnoriginal.apply(this,arguments);};});array.__proto__=arrayAugmentations;}
// p === Observer.prototypep.link=function(items,index){index=index||0for(vari=0,l=items.length;i<l;i++){this.observe(i+index,items[i])}}p.observe=function(key,val){varob=Observer.create(val)if(ob){// register self as a parent of the child observer.varparents=ob.parentsif(!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.walk=function(obj){varkey,val,descriptor,prefixfor(keyinobj){prefix=key.charCodeAt(0)if(prefix===0x24||// $prefix===0x5F// _){continue}descriptor=Object.getOwnPropertyDescriptor(obj,key)// only process own non-accessor propertiesif(descriptor&&!descriptor.get){val=obj[key]this.observe(key,val)this.convert(key,val)}}}
前言
首先欢迎大家关注我的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博客算是对我鼓励,感谢大家的支持!
The text was updated successfully, but these errors were encountered: