写在前面
之前写博客是为了记录自己的开发过程,没指望有人看,所以写的非常凌乱。没想到的是在这些日子里,竟然会有其他人看我的博客并给我评论留言,我感到受宠若惊。所以我要求自己写博客要写的更容易让别人看懂,把自己的所想所得分享出去,如果有人看了我的博客,我希望的文字能对得起别人花费的时间。
前端框架是什么
其实前端框架做的事情都一样,无非是解决了如何用数据来渲染页面的问题:在我看来,无论react还是vue还是angular,其实都是controller+view。
拿我最熟悉的angular来说,数据与页面的绑定就是scope与compile的绑定。
之前写表达式解析部分写的头晕,决定换换脑子,写写scope。
scope的本质
scope其实就是一个普通对象,只不过上面封装了很多方法而已。
scope的数据监测机制
在angular的scope中,有两个部分是数据监测的关键,$watch和$digest。我目前不清楚vue的数据变化监测是什么机制,但是angular的是脏值监测,本质是给scope对象上增加watcher。
watcher是什么
watcher监控scope的某一个属性,并且当属性变化的时候执行回调函数。
$digest()是什么
digest负责把每一个watcher都检查并且运行一次。
今天先决定写这个部分。
在scope的对象上有很多watcher,所以要有一个地方存放这些watcher。
function Scope(){
this.$$watchers=[]
}
scope有$watch函数,这个函数接受两个参数,第一个参数用于指定该watcher监听的属性,第二个是属性变化的时候执行的回调函数。
Scope.prototype.$watch=function(watchFn,listenFn){
var watcher = {
watchFn:watchFn,
listenFn:listenFn
}
this.$$watchers.push(watcher);
}
$digest是用来执行所有watcher的listen方法,也很简单,遍历一下,执行就可以
Scope.prototype.$digest=function(){
for(var i=0;i<this.$$watchers.length;i++){
this.$$watchers[i].listenFn();
}
}
写一个测试案例试试看:
describe('scope', function() {
var scope;
beforeEach(function(){
scope=new Scope()
})
it('scope可以赋值', function() {
scope.name='wangji'
expect(scope.name).toBe('wangji');
});
it('scope的watch和digest方法执行正常', function() {
var watchFn=function(){
return 'name'
}
var listenFn=jasmine.createSpy();
scope.$watch(watchFn,listenFn);
scope.$digest();
expect(listenFn).toHaveBeenCalled();
});
});
watcher的实现就是典型的一种观察者模式
watcher有两个方法,一个是watchFn,一个是listenFn。watchFn用于获取scope上某一个属性的值:
watchFn=function(scope){
return scope.id//这个watcher用于监听scope.id的值
}
listenFn是一个回调函数,这样一来就是很典型的观察者模式:一个方法用来监听某个对象的值,另一个方法是当监听到相应动作时候执行。
脏值检测的实现
每一个watcher既然能获取scope上的一个属性值,那么应该也可以保存上次的值。然后运行一次digest,把每一个watcher跑一次,如果这次拿到的值和上次保存的值不同,说明值是脏的,就可以运行listenFn回调函数,典型的观察者模式。
function Scope(){
this.$$watchers=[]
}
Scope.prototype.$watch=function(watchFn,listenFn){
var watcher = {
watchFn:watchFn,
listenFn:listenFn,
last:''
}
this.$$watchers.push(watcher);
}
Scope.prototype.$digest=function(){
var self = this;
var oldValue,newValue;
for(var i=0;i<this.$$watchers.length;i++){
oldValue = this.$$watchers[i].last;
newValue = this.$$watchers[i].watchFn(self)
if(oldValue!=newValue){
this.$$watchers[i].last = newValue;
this.$$watchers[i].listenFn();
}
}
}
执行以下案例看看效果
it('脏值检测',function() {
scope.id=2;
var watchFn=function(scope){
return scope.id;
}
var listenFn=function(){
console.log('listen!')
}
scope.$watch(watchFn,listenFn);
scope.$digest();
})
运行$digest方法时,期望中是这样的:
watcher的last值最初是空的,调用watchFn后拿到id属性的值,是2,进行对比,不相等,然后执行了listenFn,打印出listen!这句话。同时,watcher的last被设置为新值,也就是2.
来运行一下试试:
没问题!那么再运行一次$digest的话,应该不会再打印listen了,因为经过第一次digest以后,watcher的last属性被赋值为最新值,所以值不再脏了,也就不再运行listenFn了。
完成,这就是脏值检测的核心。