引言
一直想要好好学一下关于框架原理的东西,奈何以前的课程任务太重,现在终于得空可以学习学习,跟着一个牛逼的博客做下去,相信自己能够得到很大的提升的哈哈哈哈哈哈,不想看我的可以直接去他那里seesee,传送门
前期知识
先了解一下MVC和MVVM地工作原理:
MVC可谓是十分经典:
- 视图(View):用户界面。
- 控制器(Controller):业务逻辑
- 模型(Model):数据保存
各部分之间的通信方式如下: - View 传送指令到 Controller
- Controller 完成业务逻辑后,要求 Model 改变状态
- Model 将新的数据发送到 View,用户得到反馈
- 所有通信都是单向的。
MVVM现今也是十分的流行:
- 所有的通信都是双向的
- view和model层不发生关系,所有的都通过ViewModel传递~~
- view层其实比较薄,而ViewModel比较厚,所有的逻辑代码什么的都在这里面
- view和ViewModel采用的是双向绑定,view的变动自动反映在viewModel中,反之亦然~
先尝试一个小demo
在vue中其实比较核心的是Object.defineProperty(obj, prop, descriptor)
这个API。
我们需要注意的是,使用的时候不能同时设置访问器(getter和setter)和writeable或者value,否则会会报这样的错误:
TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
,
step1:defineProperty
let callback={
target: null
}
let defineReactive=function(object,key,value){
let array=[]
Object.defineProperty(object,key,{
configurable: true,
enumerable:true,
get: function(){
if(callback.target){
array.push(callback.target)
}
return value
},
set: function(newValue){
if(newValue!=value){
array.forEach(fun => {
fun(newValue,value)
});
}
value=newValue;
}
})
}
let object={};
defineReactive(object,'test','test');
callback.target=function(newValue,oldValue){
console.log("我被添加进去了,新的值是",newValue);
}
object.test;
callback.target=null;
object.test='test2';
callback.target=function(newValue,oldValue){
console.log("添加第二个函数,新的value",newValue);
}
object.test
callback.target=null;
object.test='test3';
当我们取值的时候,函数自动帮我们添加针对当前值的依赖,当我们的值发生变化的时候,会自动处理这些依赖,比如我们的dom变化的时候。
这其实也是vue
的核心代码,只是它的实现要远比我们的复杂许多!
step2 Dep
上一步我们实现了VUE的依赖收集和触发,但是我们只是将它放在了一个内置的数组里面,虽然容易理解但是十分不好维护,为了更加好地维护,需要事先一个维护依赖的类!
类下属性:target 函数,用于存放需要添加的依赖
实例下属性:
subs/array:存放依赖
addsub/function: 添加依赖
removesub/function:删除依赖
notify/function:执行依赖
let Dep=function(){
//实例属性
this.subs=[]
//实例方法
this.addSub=function(sub){
this.subs.push(sub);
}
this.removeSub=function(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.sub.splice(index,1);
}
}
this.notify=function(newValue,oldValue){
this.subs.forEach(func => {
func(newValue,oldValue)
});
}
}
//类属性
Dep.target=null;
let defineReactive=function(object,key,value){
let dep=new Dep();
Object.defineProperty(object,key,{
configurable:true,
enumerable:true,
get: function(){
if(Dep.target){
dep.addSub(Dep.target)
}
return value
},
set:function(newValue){
if(newValue!=value){
dep.notify(newValue,value);
}
value=newValue;
}
})
}
let object={};
defineReactive(object,'test','test');
Dep.target=function(newValue,oldValue){
console.log('我被添加进去了,新的值',newValue);
}
object.test;
Dep.target=null;
object.test='test2';
Dep.target=function(newValue,oldValue){
console.log('添加第二个函数,新的值是',newValue);
}
object.test;
Dep.target=null
object.test='test3';
- dep类将监听属性和处理依赖进行了一定的解耦,但是解耦不是很完全,在我们触发依赖的时候还是得传递新旧值。
- 删除依赖肯定在外部环境中,但是我们的dep实例是在
defineReactive
作用域里面,外部没有办法直接进行调用!
vue中的Watcher
类处理了以上两个问题,后面说!
刚刚的Dep类可以在es6的语法下:
class Dep{
constructor(){
this.subs=[];
}
addSub(sub){
this.subs.push(sub);
}
removeSub(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.subs.splice(index,1);
}
}
notify(){
this.subs.forEach(func=>func(newValue,oldValue))
}
}
//类属性
Dep.target=null;
step3 Watcher
- 解耦不完全,需要传递参数
- 没有地方可以移除依赖
//其中wacher是存入subs数组里面的一个个元素
let Watcher= function(object,key,callback){
this.obj=object;
this.getter=key;
this.cb=callback;
this.dep=null;
this.value=undefined;
this.get=function(){
Dep.target=this;
let value=this.obj[this.getter];//有个object、get操作
Dep.target=null;
return value;
}
this.update=function(){
const value = this.obj[this.getter];
const oldValue=this.value;
this.value=value;
this.cb.call(this.obj,value,oldValue);
}
this.addDep=function(dep){
this.dep=dep;
}
this.value=this.get();
}
let Dep=function(){
this.subs=[];
this.addSub=function(sub){
this.subs.push(sub);
}
this.removeSub=function(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.subs.splice(index,1)
}
}
this.notify=function(){
this.subs.forEach(watcher => {
watcher.update();
});
}
}
Dep.target=null;
let defineReactive=function(object,key,value){
let dep=new Dep();
Object.defineProperty(object,key,{
configurable:true,
enumerable:true,
get:function(){
if(Dep.target){
dep.addSub(Dep.target)
// 添加 watcher 对 dep 的引用
Dep.target.addDep(dep)
}
return value;
},
set: function(newValue){
if(newValue!=value){
value=newValue;
dep.notify();
}
}
})
}
let object = {}
defineReactive(object, 'test', 'test')
let watcher = new Watcher(object, 'test', function(newValue, oldValue){
console.log('作为 watcher 添加的第一个函数,很自豪。新值:' + newValue)
})
object.test = 'test2'
// 作为 watcher 添加的第一个函数,很自豪。新值:test2
let watcher2 = new Watcher(object, 'test', function(newValue, oldValue){
console.log('作为 watcher 添加的第二个函数,也很自豪。新值:' + newValue)
})
object.test = 'test3'
// 作为 watcher 添加的第一个函数,很自豪。新值:test3
// 作为 watcher 添加的第二个函数,也很自豪。新值:test3
// 接着我们来试一下删除依赖,把 watcher2 给删除
watcher2.dep.removeSub(watcher2)
object.test = 'test4'
// 作为 watcher 添加的第一个函数,很自豪。新值:test4
上面的代码实现了一个Watcher,实例里面保存了:需要监听的对象(object)、取值方法(key)、对应的回调(callback)、需要监听的值(value)、以及取值函数(get)和出触发函数(update),这样我们就把依赖相关的所有内容都保存在了这个实例里面。
为了保存对Dep的引用,在Watcher中设置了dep,用于保存这个监听被那个Dep引用了。
编写代码的时候我们不用特地的触发get来添加依赖因为我们将之前的get值的代码放在了watcher里面:
修改前:
Dep.target = function(newValue, oldValue){console.log('我被添加进去了,新的值是:' + newValue)}
object.test
Dep.target = null
修改后:
this.get = function(){
Dep.target = this
let vaule = this.obj[this.getter]
Dep.target = null
return value
}
this.value = this.get()
最终,通过以上的代码我们解决了之前的问题。
问题:这时候呢我们又出现了一个问题:那就当前的Watcher还是需要优化的,比如如果被多个Dep引用的话,就得存一个数组嘤嘤嘤
setp4 优化Watcher
目前为止我们做了三个东西
- 通过
defineReactive
函数,实现了对数据取值和设置的监听!!Object.defineProperty - 通过
Dep
类,实现了依赖的管理 - 通过
Watcher
类,实现了解耦等等。
问题:
- 一个
watcher
对应多个属性。 - 多个
Dep
依赖于同一个Watcher
的情况。
//其中wacher是存入subs数组里面的一个个元素
let Watcher= function(object,getter,callback){
this.obj=object;
this.getter=getter;
this.cb=callback;
this.deps=[];
this.value=undefined;
this.get=function(){
Dep.target=this;
let value=this.getter.call(object)//有个object、get操作
Dep.target=null;
return value;
}
this.update=function(){
const value=this.getter.call(object);
const oldValue=this.value;
this.value=value;
this.cb.call(this.obj,value,oldValue);
}
this.addDep=function(dep){
this.deps.push(dep);
}
//新调节的取消依赖的方法
this.teardown=function(){
let i=this.deps.length;
while(i--){
this.deps[i].removeSub(this)
}
this.deps=[];
}
this.value=this.get();
}
let Dep=function(){
this.subs=[];
this.addSub=function(sub){
this.subs.push(sub);
}
this.removeSub=function(sub){
const index=this.subs.indexOf(sub);
if(index>-1){
this.subs.splice(index,1)
}
}
this.notify=function(){
this.subs.forEach(watcher => {
watcher.update();
});
}
}
Dep.target=null;
let defineReactive=function(object,key,value){
let dep=new Dep();
Object.defineProperty(object,key,{
configurable:true,
enumerable:true,
get:function(){
if(Dep.target){
dep.addSub(Dep.target)
// 添加 watcher 对 dep 的引用
Dep.target.addDep(dep)
}
return value;
},
set: function(newValue){
if(newValue!=value){
value=newValue;
dep.notify();
}
}
})
}
let object={};
defineReactive(object,'num1',2);
defineReactive(object,'num2',4);
let watcher=new Watcher(object,function(){
return this.num1+this.num2
},function(newValue,oldValue){
console.log(`这是一个监听函数,${object.num1} + ${object.num2} = ${newValue}`)
});
let watcher2=new Watcher(object,function(){
return this.num1*this.num2
},function(newValue,oldValue){
console.log(`这是一个监听函数,${object.num1} * ${object.num2} = ${newValue}`);
});
object.num1=4;
object.num2=11;
//测试取消
watcher2.teardown();
object.num1=5;
object.num2=12;
其实最终优化就是两点:
- 传入
Watcher
的不再是个key值,而是一个函数,这个函数里面呢,我们触发了多个属性的get
。 - 需要保存多个dep的话,将它用一个数组保存起来就可以了。
- 当然,为了方便取消这个watcher,我们需要的是添加函数为了取消所有dep对watcher的依赖。
自己小结一下:
可能有点乱,之前我甚至没有把watcher和dep之间的关系弄懂,于是我花了个小图图,其实就是要实现一个一对多的关系,但是同时为了watcher能够保存对dep的引用,我们需要将依赖于此的dep保存到watcher里面(作用域啥的知识),以便于 引用。
step5 Observe
我们已经有了:
defineReactive
控制了对象属性,变为可监听结构Dep
收集管理依赖Watcher
一个抽象的依赖
defineReactive
和Dep
改造了对象下面的某一个属性,将一个目标变成了观察者模式中的目标,目标发生变化的时候会调度观察者;
Watcher
就是一个观察者,会注册到目标里面。
需要优化的地方:
4. 如果想要对象的属性都得以响应,必须对对象下的所有属性进行遍历,依次调用 defineReactive
不是很方便。
5. 代码都在一个文件里面十分不便与管理
最后实现看 https://github.com/CoCoManYY/MyWheel/tree/master/myMVVM/demo/step5
其中的配置采坑笔记
step6 Array
之前的几个step实现了对象属性的监听,但是有关于数组的行为一直没有进行处理。
- 调用方法:
arr.splice(1,2,"something","something2")
- 直接赋值:
arr[1]='something'
总结对原数组造成影响的方法:push、pop、shift、unshift、splice、sort、reverse。数组长度发生变化,数组内元素顺序发生变化。