我是卡卡卡颂


  • 首页

  • 归档

  • 标签

Angular源码解析4——作用域上代码的执行

发表于 2016-09-30

在作用域中执行代码

  1. $eval

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    *$eval接收2个参数
    *fn需要在作用中执行的函数
    *(可选)传入的参数
    **/
    Scope.prototype.$eval=function (fn,locals) {
    return fn(this,locals);
    }
  2. $apply
    有时候我们需要执行与Angular无关的方法(比如window.setInerval),但是这些方法改变了作用域中的数据,我们希望监听到这些变化,这时候我们需要通过$apply传入方法。

    1
    2
    3
    4
    5
    6
    7
    8
    Scope.prototype.$apply=functin (fn) {
    try {
    return this.$eval(fn);
    } finally {
    //$digest的执行放在finally中,以确保即使报错也会执行
    this.$digest();
    }
    }
  3. 延迟执行的函数
    在angular中我们可以用$apply传递一个计时器(setInterval)来实现延迟执行函数,也可以调用内部的$interval和$timeout。Angular中还有一种延迟执行的方法$evalAsync。

$evalAsync会将一个函数推入计划中。这个函数会在这次$digest或者下次$digest之前执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//在Scope中定义任务队列
function Scope () {
this.$$watchers=[];
this.$$asyncQueue=[];
}
//定义$evalAsync,传入需要延迟执行的函数
Scope.prototype.$evalAsync=function (fn) {
this.$$asyncQueue.push({scope:this,expression:fn});
}
Scope.prototype.$digest=function () {
var time=10;
var dirty;
do {
while (this.$$asyncQueue.length) {
var task=this.$$asyncQueue.shift();
this.$eval(task.expression);
}
dirty=this.$$digestOnce();
if (dirty &&!(time--)) {
throw '已到10次脏值查询'
}
} while (dirty);
}

Angular源码解析3——深度比较、深度克隆

发表于 2016-09-27

为什么需要深度比较、深度克隆

详细内容见上一篇。简单的说是为了比较引用类型值(对象、数组)内部数据的变化以触发脏值查询。

##深度比较 源码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function equals(o1, o2) {
//对基本类型值的比较
if (o1 === o2) return true;
if (o1 === null || o2 === null) return false;
//对NaN特殊处理,使得NaN === NaN
if (o1 !== o1 && o2 !== o2) return true;
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
//如果比较的值都是对象
if (t1 == t2 && t1 == 'object') {
if (isArray(o1)) {
if (!isArray(o2)) return false;
//都是数组,遍历数组每一项
if ((length = o1.length) == o2.length) {
for (key = 0; key < length; key++) {
if (!equals(o1[key], o2[key])) return false;
}
return true;
}
} else if (isDate(o1)) {
if (!isDate(o2)) return false;
return equals(o1.getTime(), o2.getTime());
} else if (isRegExp(o1)) {
if (!isRegExp(o2)) return false;
return o1.toString() == o2.toString();
} else {
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
keySet = createMap();
for (key in o1) {
if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
if (!equals(o1[key], o2[key])) return false;
keySet[key] = true;
}
for (key in o2) {
if (!(key in keySet) &&
key.charAt(0) !== '$' &&
isDefined(o2[key]) &&
!isFunction(o2[key])) return false;
}
return true;
}
}
return false;
}

深度克隆 源码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function clone (obj) {
var buf;
if (obj instanceof Array) {
buf=[];
var i=obj.length;
while (i--) {
buf[i]=clone(obj[i]);
}
return buf;
}
else if (obj instanceof Object) {
buf={};
for (var k in obj) {
buf[k]=clone(obj[k]);
}
return buf;
}
else {
return obj;
}
}

Angular源码解析2--——脏值查询完善

发表于 2016-09-27

上一篇提到的问题

上一篇提到了由于监听函数第三个传参是作用域本身,所以我们可以在监听函数内部改变作用域的值。但是这么做的话,会有另一个监控器来监控值的变化。但是在一次$digest中就无法检测值的变化。所以我们需要实现一种机制——$digest会持续遍历所有监听器,知道他们停止变化。

实现原理

我们将$digest改名为$$digestOnce。意思是每调用他一次就会遍历所有监听器一次。他会返回一个状态,表示这次遍历该数据是否发生变化。我们再用一个外层函数包裹$$digestOnce,一旦发现这个状态为true,就再执行一次$$$digestOnce。直到状态不再为true。这表示数据不再发生变化。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Scope.prototype.$$$digestOnce=function () {
var that=this;
var dirty;
this.$$watchers.forEach(function (watch){
var oldVal=watch.last;
var newVal=watch.watchFn(that);
if (oldVal!=newVal) {
watch.listenerFn(newVal,oldVal,that);
dirty=true;
}
watch.last=newVal;
})
return dirty;
}
Scope.prototype.$digest=function () {
var dirty;
do {
dirty=this.$$digestOnce();
} while (dirty)
}

重要提示

  • 其实Angular的源码中并不存在$$digestOnce,而是将其直接封装在$digest中,这里只是为了结构的清晰。
  • 通过这里我们可以看出,在一次$digest中,监听器可能会执行多次。

漏洞

假设我们有两个监听器,监听器a监听Scope.a的值并且他的监听函数会改变Scope.b的值。监听器b监听Scope.b的值并且他的监听函数会改变Scope.a的值。这样互相监听,最终会导致双方的值永远无法稳定,$digest会无限调用,这肯定是需要被杜绝的。

应对

对此,我们需要设定,当$digest执行一定次数以后,如果状态还是无法稳定,我们就宣布他的状态永远无法稳定,并抛出错误。为了性能考虑,Angular对这个次数的设定是10。

代码实现

1
2
3
4
5
6
7
8
9
10
Scope.prototype.$digest=function () {
var ttl=10;
var dirty;
do {
dirty=this.$$digestOnce();
if (dirty && !(ttl--)) {
throw '10 digest iterations reached';
}
} while (dirty)
}

基于值的检测与基于地址的检测

我们之前对比oldVal与newVal时,使用的是’===’操作符,这在检测基本值类型的数据时是完全没有问题的。当检测引用类型的值时比较的是引用的地址。但是有时候我们需要检测对象或数组内部数据的变更,这时候我们就需要基于值的检测,而不是基于地址的检测了。
由于基于值的检测时一项比较消耗性能(相对)的操作,所以我们应该提供一个可选参数,让使用者自己决定在什么时候开启(全等比较函数源码见下篇)。

代码实现

1
2
3
4
5
6
7
8
9
Scope.porototype.$watch=function (watchFn,listenerFn,valueEq) {
var watcher={
watchFn:watchFn,
listenerFn:listenerFn,
//这里通过两次取反,使得强制转换成布尔类型,如果未传递值undefined会被转换成false
valueEq:!!valueEq
};
this.$$watchers.push(watcher);
}

基于值的检测意味着如果数据时对象或数组类型,脏值查询会遍历数据的每一项,如果数据存在嵌套的数组或对象,还会递归的按值进行比较。

另一个问题

这里我们的深度检测只发生在newVal与oldVal比较的时候。回忆下,我们的新值会保存在watch.last中。试想如果保存的是一个引用类型的值,那么同一个值通过watchFn取得的和watch.last取得的其实是同一个地址。也就是说watch.fn(旧值)的变化会实时表现在watch.last(新值)中。这样就无法检测到引用类型值的变化了。
所以我们保存在watch.last中的数据应该是一个值,而不是一个地址。也就是说我们需要深度克隆一份引用类型的值。(源码分析见下一篇)

源码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Scope.prototype.$$digestOnce = function() {
var that = this;
var dirty;
this.$$watchers.forEach(function(watch) {
var newValue = watch.watchFn(that);
var oldValue = watch.last;
if (!that.$$areEqual(newValue, oldValue, watch.valueEq)) {
watch.listenerFn(newValue, oldValue, that);
dirty = true;
}
watch.last = (watch.valueEq ? this.$$cloneDeep(newValue) : newValue);
});
return dirty;
};

最后一点小问题

在原生js中NaN!==NaN。如果我们的数据中包含NaN,在进行引用类型检测时(值类型的检测我们封装的$$areEqual已经帮我们处理了)由于NaN!==NaN那么这个值会始终是脏的。所以我们需要手动处理他。

源码实现

1
2
3
4
5
6
7
8
9
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {
if (valueEq) {
return this.$$isEqual(newValue, oldValue);
} else {
return newValue === oldValue ||
(typeof newValue === 'number' && typeof oldValue === 'number' &&
isNaN(newValue) && isNaN(oldValue));
}
};

Angular源码解析1——脏值查询的基本原理

发表于 2016-09-22

数据双向绑定

在VUE中,通过ES5提供的getter和setter实现单向数据流。而在Angular中通过所谓的脏值查询实现了双向数据绑定,即View层和ViewModel层的双向数据流。具体是如何做到的呢。

基本原理

首先我们要了解订阅-发布模式(原理见我的另一篇博文EVENT事件库的原理)。Angular的脏值查询在此基础上实现。
这里我们先构建作用域构造函数,并设置事件池

1
2
3
function Scope () {
this.$$watchers=[];
}

数据变动的检测基于$scope作用域两个内置方法$watch和$digest。$watch类似事件库里的on方法,用于在事件池(在Angular中是作用域中的$$watchers属性)中存放事件名和对应回调函数。
类似这样

1
2
3
4
Scope.prototype.$watch=function (watchFn,listenerFn) {
var watcher={watchFn:watchFn,listenerFn:listenerFn};
this.$$watchers.push(watcher);
}

  • watchFn 为监控函数,返回所监控的数据的值
  • listenerFn 为监听函数,当数据发生变化后作出行为
    一个监控name值的简单监控函数
    1
    2
    3
    4
    //监控作用域中name属性的值
    function (scope) {
    return scope.name;
    }

$digest类似事件库中的emit方法,用来执行所在作用域中事件池($$watchers数组)中保存的方法对应的回调函数(即数据的监听函数),并把执行后新的值保存在$watch中。这样我们再次调用$digest,就能比较新值与旧值的变化。
类似这样

1
2
3
4
5
6
7
8
9
10
11
12
13
Scope.prototype.$digest=function () {
var self=this;
this.$$watchers.forEach(function (watcher) {
var newVal=watcher.watchFn(self);
var oldVal=watcher.last;
if (newVal!==oldVal) {
//如果发现值变化了就调用监听函数
watcher.listener(newVal,oldVal,self);
}
//每次调用都会更新数据
watch.last=newVal;
})
}

这就是Angular作用域的本质:添加监听器,在$digest中运行他们。
这也揭示了Angular作用域的性能特性:

  1. 在作用域中添加数据并不会带来性能折扣。Angular并不会遍历作用域的数据,只会遍历绑定在$$watchers中的事件。
  2. $digest会调用每个监控函数。因此,最好关注监听器的数量,还有每个独立的监控函数或者表达式的性能。

存在的问题

现在我们的脏值检查还存在很多问题,其中比较重要的一个应用场景咱们就没有实现——当监听函数自身也修改作用域上的属性。如果这个发生了,另外有个监听器在监控被修改的属性,有可能在同一个digest里面检测不到这个变动。

express源码实现6————重定向

发表于 2016-09-20

什么是重定向

重定向就是指当访问某个路由时自动跳转到另一个路由(比如注册成功自动跳转到首页)。

重定向代码实现

1
2
3
4
5
6
7
8
9
10
//用中间件的形式实现
module.exports=function (req,res,next) {
res.redirect=function (url) {
//设置http状态码为302(重定向)
res.statusCode=302;
//设置响应头,注意状态码与响应头缺一不可
res.setHeader('Location',url);
res.end('');
}
}

socket.io入门

发表于 2016-09-16

什么是socket.io

一般说法是socket.io是一个websocket库。不过我更喜欢把他称为一个前后端交互库。因为他不仅封装了websocket的方法,在不支持websocket的浏览器中他会调用其他方法(如轮询,长轮询,iframe流)实现交互。

socket.io的特点

  1. 简易:封装了客户端与服务器端的api。
  2. 跨平台:可以在自己喜欢的平台开发实时应用。
  3. 兼容性:这点开篇已经说过。最低兼容到IE5.5.

简单的部署

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//与express配合使用
var express=require('express');
var app=express();
var server=require('http').createServer(app);
var io=require('socket.io')(server);
server.listen(1234);
//检测客户端的连接
io.on('connection',function (socket) {
//检测连接来的客户端发送的信息
socket.on('message',function (msg) {
})
//向socket客户端发送消息
socket.send('123');
})

客户端

  • 服务端运行后会在根目录动态生成socket.io的客户端js文件 客户端可以通过固定路径/socket.io/socket.io.js添加引用
  • 客户端加载socket.io文件后会得到一个全局的对象io
  • connect方法可以接受一个url参数,url可以是socket服务的url,也可以是相对路径,如果省略则表示默认连接当前路径 创建index.html文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <script src="/socket.io/socket.io.js"></script>>
    <script>
    var socket=io.connect('/');
    //监听是否连接成功
    socket.on('connect',function () {
    //连接成功后向服务器发送消息
    socket.send('123');
    })
    //监听是否与服务器断开连接
    socket.on('disconnect',function () {
    })
    </script>

服务端事件汇总

事件名 含义
connection 客户端成功连接到服务器
message 接收到客户端发送的消息
disconnect 客户端断开连接
error 监听错误

客户端事件汇总

事件名 含义
connect 成功连接到服务器
message 接收到服务器发送的消息
disconnect 客户端断开连接
error 监听错误

划分房间

一个服务端可以有很多不同的房间,客户端可以进入不同的房间,在房间内通信不会影响到房间非该房间的客户端

1
2
socket.join('chatroom'); //客户端进入chatroom房间
socket.leave('chatroom'); //客户端立刻chatroom房间

全局广播

服务端发送信息有send和emit方法 事件。其中send方法只有房间内的客户端能监听到。而emit(广播)所有客户端都可以监听到。

1
io.emit('message','全局广播');

send与emit比较

send只是emit封装后的方法
emit源码如下:

1
2
3
4
5
6
7
8
9
Socket.prototype.send = function(){
//ES6方法,类数组转数组
var args = toArray(arguments);
//推入事件名message
args.unshift('message');
//执行
this.emit.apply(this, args);
return this;
};

WebSocket入门

发表于 2016-09-16

什么是WebSocket

WebSocket是html5提供的一种浏览器和服务器进行全双工通信的技术。要解释全双工通信,首先得介绍一下WebSocket之前浏览器与服务器通信的技术。

WebSocket之前的通信技术

  1. 轮询
    为了实时获得数据,浏览器周期性的向服务器发送请求,如果服务器没有新的数据则返回空响应。

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var xhr = new XMLHttpRequest();
    setInterval(function(){
    xhr.open('GET','/data',true);
    xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200){
    document.querySelector('#content').innerHTML = xhr.responseText;
    }
    }
    xhr.send();
    },1000);

    缺点:1.大量无意义的请求造成网络压力

    2.不能实时得到新数据
    
  2. 长轮询
    为了弥补轮询的缺陷,浏览器向服务器发送请求,如果服务器有响应新数据,浏览器再在接收响应的时候继续发送请求,使数据随时保持更新。

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //封装请求
    function send() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/data', true);
    xhr.onreadystatechange = function () {
    if (xhr.readyState == 4 && xhr.status == 200) {
    document.querySelector('#content').innerHTML = xhr.responseText;
    //回调函数中递归调用
    send();
    }
    }
    xhr.send();
    }
    send();

    缺点:如果是频繁的发送数据,浏览器会一直处于加载的状态

  3. iframe流
    利用iframe接收的服务器响应中如果包含脚本就会执行的原理,用iframe标签的src属性向服务器发起请求。

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //客户端网页
    <iframe src="http://localhost:1234/time" frameborder="0"></iframe>
    //服务端
    app.get('/time',function (req,res) {
    setInterval(function () {
    //如果调用end、send会结束响应, write方法不会,能实现持续写入
    res.write(`<script>
    //由于iframe会形成自己的document,所以要用parent.document取得页面
    parent.document.querySelector('#time').innerHTML= new Date().toLocaleString();
    </script>`);
    },1000)
    })

WebSocket的优势

使用WebSocket,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者之间就直接可以数据互相传送

  • 节省资源:互相沟通的Header是很小的-大概只有 2 Bytes。
  • 推送信息:不需要客户端请求,服务器可以主动传送数据给客户端
  • 支持跨域:WebSocket天生支持跨域

    解释一下单工、双工

  • 单工指只能由一端向另一端发送数据。
  • 半双工指客户端、服务器可以互相发送数据,但不能在同一时间。必须有先后顺序。我们平时利用http协议与后台的通信都是半双工。
  • 双工指服务器和客户端可以随时向对方发送数据与接收对方的数据。

WebSocket API

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
var WebSocketServer=require('ws').Server;
var server=new WebSocketServer({port:8000});
//监听客户端请求 ,即与客户端握手
server.on('connection',function (websocket){
//监听从客户端传来的信息
websocket.on('message',function (msg) {
console.log(msg);
//向客户端发送信息
websocket.send(msg);
})
})

Node客户端

1
2
3
4
5
6
7
8
9
10
11
var WebSocket=require('ws');
var ws=new WebSocket('ws://localhost:8080/');
//检测服务器是否开放,即与服务端握手
ws.on('open',function () {
//如果服务器开放向服务器发送信息
ws.send('hello')
});
//监听从服务器传来的数据
ws.on('message',function (data,flag){
console.log(data);
})

Web客户端

1
2
3
4
5
6
7
8
9
10
var ws=new WebSocket('ws://localhost:8080/');
//监听连接事件
ws.onopen=function (){
//向服务器发送数据
ws.send(123);
}
//监听服务器传来的数据
ws.onmessage=function (){
}

简单的依赖加载器实现

发表于 2016-09-12

为了验证闭包的学习,有必要实现一个简单的依赖加载器。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var myModules=(function () {
//保存所有定义的模块
var modules={};
/**
*定义新模块,接收3个参数
*name:模块名
*deps:模块依赖的其他模块
*impl:模块的定义
**/
function define(name,deps,impl) {
//遍历依赖每一项,取出每个模块
for (var i=0;i<deps.length;i++) {
deps[i]=modules[deps[i]];
}
//将新模块存储进模块池,并注入依赖
modules[name]=impl.apply(impl,deps);
}
//从模块池中取出模块
function get (name) {
return modules[name];
}
//暴露api
return {
define: define,
get: get
}
})()

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
myModules.define('bar',[],function () {
function hello (who) {
//代码体
}
return {
hello:hello
}
})
myModules.define('foo',['bar'],function (bar) {
functin hello2 () {
//代码体
}
return {
hello2:hello2
}
})
var bar=myModules.get('bar');
var foo=myModules.get('foo');
  • 内容转自《你不知道的Javascript》

express源码实现现5——静态服务器

发表于 2016-09-11

什么是静态服务器

-当浏览器向服务器发送请求时,一部分请求是用户主动发送的——比如表单的提交。还有一部分是浏览器页面渲染过程中自动向后端发送的请求——比如对css文件、对图片的请求。这后面一部分文件有一个特点——他们不会经常改变,是作为静态文件存在于服务器的。
-这部分文件如果也像其他文件一样每一个都设置一个请求回调,将会十分繁琐。这时候就需要一个能自动加载静态文件的插件。在express中通过中间件的形式实现。

原理

在优先级很高的中间件中指定一个静态文件夹,每次响应请求时都会先搜索一遍静态文件夹,如果找到匹配的文件就返给客户端。

源码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fs=require('fs');
var path=require('path');
//作为中间件使用
module.exports=function (p) {
return function (req,res,next) {
var file=path.join(p,req.path);
fs.exists(file,function (exist){
//如果文件存在则发送给客户端
if (exist&&file!='/') {
fs.createReadStream(file).pipe(res);
} else next();
})
}
}

express源码实现4——res.send

发表于 2016-09-09

res.send的作用

原生node中res.end()方法中只能传递String和Buffer类型,并且传递中文字符串的时候由于编码的问题会显示乱码。为了解决这个问题,express中通过中间件在res上封装了一个send方法,可以传递字符串、对象(转换成JSON)、状态码。

源码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
res.send=function (msg) {
//是字符串或buffer
if (typeof msg=='string'||Buffer.isBuffer(msg)) {
res.setHeader('content-type','text/plain;charset=utf8');
res.end(msg);
}
//是对象
if (typeof msg=='object') {
res.setHeader('content-type','applicaton/json;charset=utf8');
res.end(JSON.stringify(msg));
}
//是数字类型,转换成状态码
if (typeof msg=='number') {
var STATUS_CODE=require('http').STATUS_CODES;
res.statusCode=msg;
res.end(STATUS_CODE[msg]);
}
}
12345
BetaSu

BetaSu

一言不合撸源码

50 日志
© 2017 BetaSu
由 Hexo 强力驱动
主题 - NexT.Pisces