浅谈 Vue 框架中的响应式原理
众所周知, 这几年前端发展的非常迅速, 涌现了好几大不错的框架体系, 其中 vue.js 以入门门槛低实用性高深得广大前端喜爱. 今天我们就谈谈 vue.js 框架的核心 -- 响应式系统
说到响应式系统, 它的原理核心来自于 Object.defineProperty, 相信大多数人都认识它, 但今天也要基本的介绍介绍一下它.
我们可以看看官方的介绍:
然后我们在看看它要那些通用的属性:
知道该方法怎么用的时候, 我们就来实现就最基本的响应式系统原理:
这个图片在官方文档上就有, 仔细的观察一下, 我们不看所有的生命周期, new Vue() => init 这个阶段的时候, 数据就开始进行响应式的操作了.
我们定义一个函数, 用来表示视图更新, 调用这个函数就告诉大家视图更新啦.functioncb(val){
/* 更新视图操作 */
console.log("a=b=c=d=f=e=.....");
}
接下来我们创建一个 defineReactive , 这个方法通过 Object.defineProperty 来实现对对象的响应式操作.functiondefineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get:functiongetVal(){
returnval;
},
set:functionsetVal(newVal){
if(newVal===val)return;
cb(newVal);
}
});
}
写好这个后我们还需要定义另外一个方法 observer, 该方法我们用来遍历修改的对象, 对这些对象通过 defineReactive 函数进行相应的处理:functionobserver(value){
if(!value||(typeofvalue!=='object')){
return;
}
Object.keys(value).forEach((key)=>{
defineReactive(value,key,value[key]);
});
}
这个时候基本大功告成了, 当然真理是需要实践一下, 这个时候我们封装一个 Vue:classVue{
constructor(options){
this._data=options.data;
observer(this._data);
}
}
这里我们写一个 Vue 的构造函数, 对 options 里面的 data 进行处理, 当然, 这里面的 data 就相当于我们经常在 vue.js 模板里面的写的 data. 下面我们来 new 一个 Vue 的对象进行操作:letobj=newVue({
data:{
test:"毁灭全人类"
}
});
obj._data.test="我的心愿是: 时间和平!";
这个时候视图就会更新了, 这就是 vue.js 的响应式基本原理.
既然是基本原理, 它实现的也是最基本的功能, 接下来我们看另外一个例子:newVue({
template:
`
毁灭吧,{{a}}
毁灭吧,{{b}}
data:{
a:'全人类',
b:'地球',
c:'太阳系'
}
});
如果我们现在改变 c 的值: this.c = "全宇宙" 这个时候我们在调用 cb 是无用的, 但 vue 里面这时候发生什么? 我们不得而知. 我们留着疑问看下面一个例子:
假如现在我们有一个全局对象, 而且很多 vue 视图都调用了它:letechats={
options:'pie'
};
leto1=newVue({
template:
`
{{options}}
data:echats
});
leto2=newVue({
template:
`
{{options}}
data:echats
});
这时我们做了这样一个操作: echats.options = "line", 这时会发生什么? vue.js 又是怎么操作的呢?
根据上面的图我们可以知道, 在 getter 这个阶段会获取到相应 Watcher 实例对象, 然后 setter 被调用的时候, 会通过 Watcher 重新计算, 从而致使它关联的组件模板得以更新.
对比, 我们例 2 和例 3, 要实现这一系列的操作, 单单靠上面我们的基本原理的方法是不行的, 所以我们需要改变一下
订阅者 Dep
接下来我们来实现一个订阅者, 用来存放 Watcher 实例对象.classDep{
constructor(){
/* 存放 Watcher 对象的数组 */
this.subs=[];
}
/* 在 subs 中添加一个 Watcher 对象 */
addSub(sub){
this.subs.push(sub);
}
/* 通知所有 Watcher 对象更新视图 */
notify(){
this.subs.forEach((sub)=>{
sub.update();
})
}
}
这时我们在来实现一个观察者:
观察者 WatcherclassWatcher{
constructor(){
Dep.target=this;
}
/* 视图更新 */
update(){
console.log("毁灭全人类");
}
}
Dep.target=null;
下面我们在修改一下一开始列子的代码:functiondefineReactive(obj,key,val){
constdep=newDep();
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get:functionreactiveGetter(){
dep.addSub(Dep.target);
returnval;
},
set:functionreactiveSetter(newVal){
if(newVal===val)return;
dep.notify();
}
});
}
classVue{
constructor(options){
this._data=options.data;
observer(this._data);
newWatcher();
console.log('render...',this._data.a);
}
}
通过上面的代码, 这个时候我们在回头看例 2 和例 3, 当我 data.c 改变的时候, 这里会发生什么呢? 现在我们知道每一个 data 的属性都对应一个 dep, 而每一个 dep 就对应一个或者多个 Watcher(多个视图调用的情况), 例 2 中我们改变了 data.c, 但视图上并没有对象的 Watcher, 那么它就没法调用 addSub 方法, 所以视图不会更新. 当然例 3 也从中得以理解. 最后贴一下我整理后的所有代码:constObserver=function(data){
for(letkeyindata){
defineReactive(data,key);
}
}
constdefineReactive=function(obj,key){
constdep=newDep();
letval=obj[key];
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
console.log('in get');
dep.depend();
returnval;
},
set(newVal){
if(newVal===val){
return;
}
val=newVal;
dep.notify();
}
});
}
constobserve=function(data){
returnnewObserver(data);
}
constVue=function(options){
constself=this;
if(options&&typeofoptions.data==='function'){
this._data=options.data.apply(this);
}
this.mount=function(){
newWatcher(self,self.render);
}
this.render=()=>{
if(self){
returnthis._data.text;
}
}
observe(this._data);
}
constWatcher=function(vm,fn){
constself=this;
this.vm=vm;
Dep.target=this;
this.addDep=function(dep){
dep.addSub(self);
}
this.update=function(){
console.log('is watcher update');
}
this.value=fn();
Dep.target=null;
}
constDep=function(){
constself=this;
this.target=null;
this.subs=[];
this.depend=function(){
if(Dep.target){
Dep.target.addDep(self);
}
}
this.addSub=function(watcher){
self.subs.push(watcher);
}
this.notify=function(){
for(leti=0;i
self.subs[i].update();
}
}
}
constvue=newVue({
data(){
return{
text:'hahah'
}
}
})
vue.mount();
vue._data.text='123';
总结:
在 observer 的过程中会注册 get 方法, 该方法用来进行依赖收集. 在它的闭包中会有一个 Dep 对象, 这个对象用来存放 Watcher 对象的实例. 其实依赖收集的过程就是把 Watcher 实例存放到对应的 Dep 对象中去. get 方法可以让当前的 Watcher 对象 (Dep.target) 存放到它的 subs 中 (addSub) 方法, 在数据变化时, set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新.
后话: 参考了很多大牛的文章, 如果描述的有错, 请多多包涵~~
来源: http://www.qdfuns.com/article/16817/00a5f91f18fa5216135d6c14c81de3b9.html