或许,我们都是远视眼,总是活在对别人的仰视里
或许,我们都是近视眼,往往忽略了身边的幸福
说明
个人做的学习总结,方便以后查看,大家简单看看就好
概念介绍
异步
异步指的是让CPU暂时搁置当前请求的响应,处理下一个请求,当通过轮询或其他方式得到回调通知后,开始运行。
举例:打一个电话没人接,转到语音邮箱留言(注册),然后等待对方回电(call back)
看起来异步是最高效,充分利用资源,可以想像整个系统能支持大规模并发。但问题是调试很麻烦,不知道什么时候call back。
同步
同步指对在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。
举例:打一个电话一直到有人接为止
并发
并发在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
举例:你说一句话,吃一口饭
并发是多个任务交替使用CPU,但同一时刻还是只有一个任务在跑。
并行
并行是指计算机系统中能同时执行两个或多个处理。
举例:你可以边打电话边吃饭
进程
进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
线程
线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,进程是线程的容器)
控制反转
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。
说明:a依赖b,但a不控制b的创建和销毁,仅使用b,那么b的控制权交给a之外处理,这叫控制反转(IOC),而a要依赖b,必然要使用b的instance,那么
- 通过
a的接口,把b传入; - 通过
a的构造,把b传入; - 通过设置
a的属性,把b传入;
这个过程叫依赖注入(DI)。
若还不懂,看这里:
《如何理解:程序、进程、线程、并发、并行、高并发?》
《如何用最简单的方式解释依赖注入?依赖注入是如何实现解耦的?》
异步:现在与将来
分块的程序
我们将JavaScript程序看成是由多个块构成的,最常见的块单位是函数。在这些块中只有一个是现在执行,其余的则会在将来执行。
注意: 程序中将来执行的部分并不一定在现在运行的部分执行完之后就立即执行。
以异步 Ajax 请求为例,现在我们发出一个请求,然后在将来才能得到返回的结果。
从现在到将来的“等待”,最简单的方法是使用一个通常称为回调函数的函数。虽然Ajax支持同步请求,但在任何情况下都不应该使用这种方式,因为它会锁定浏览器UI(按钮、菜单、滚动条等),并阻塞所有的用户交互。
异步控制台
console.* 方法族是由宿主环境(浏览器)提供的,在某些条件下,某些浏览器的 console.log(..) 并不会把传入的内容立即输出。主要原因是,I/O是非常低速的阻塞部分。所以浏览器在后台异步处理控制台 I/O 能够提高性能,这时用户甚至可能根本意识不到其发生。
可能出现的情况:
var a = {
index: 1
};
console.log( a ); // ??
a.index++;
这段代码运行的时候,浏览器可能会认为需要把控制台 I/O 延迟到后台,在这种情况下,
等到浏览器控制台输出对象内容时,a.index++ 可能已经执行,因此会显示 { index: 2 }。
到底什么时候控制台 I/O 会延迟,甚至是否能够被观察到,这都是游移不定的。如果在调试的过程中遇到对象在 console.log(..) 语句之后被修改,可你却看到了意料之外的结果,
要意识到这可能是这种 I/O 的异步化造成的。
此时的选择:
- 在
JavaScript调试器中使用断点 - 使用
JSON.stringify(..),强制执行一次“快照”
事件循环
宿主环境都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JavaScript 引擎,这种机制被称为事件循环。
JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。
注意:setTimeout(..) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器,当定时器到时后,环境会把你的回调函数放在事件循环中,在未来某个时刻才会执行这个回调。比如这时候事件循环中已经有 20 个项目了,那么你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。所以setTimeout(..) 定时器的精度可能不高,它只能确保你的回调函数不会在指定的
时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的
状态而定。
并发
Javascript是单线程的。就像学校食堂的孩子们,不管在门外多么拥挤,最终他们都得站成一队才能拿到自己的午饭!单线程事件循环是并发的一种形式。
非交互
var res = {};
function foo(results) {
res.foo = results;
}
function bar(results) {
res.bar = results;
}
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
上面的程序,无论按哪种顺序执行都无所谓,foo可能先执行,bar可能先执行,但因为它们是独立运行的,所以不会相互影响。
交互
var res = [];
function response(data) {
res.push( data );
}
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
上面的程序,由于执行顺序的不确定性,会导致res中数据的存放顺序也是不确定的。假定我们期望res[0]存放"http://some.url.1"的结果,res[1]存放"http://some.url.2"的结果,在这种情况下,我们就需要协调交互顺序。下面是一种做法:
var res = [];
function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
协作
考虑这里有一个执行时间特别长的程序,它的执行会阻塞其他程序的进行,因为JS是单线程的,同时UI不能进行刷新,因为JS引擎线程与渲染线程互斥,此时我们就需要将它分割成多个步骤或多批任务,使得其他并发程序有机会将自己的运算插入到事件循环队列中交替运行,这就是并发协作。
以处理1000万条数据为例,我们采取并发协作的方式来实现:
var res = [];
// response(..)从Ajax调用中取得结果数组
function response(data) {
// 一次处理1000个
var chunk = data.splice( 0, 1000 );
// 添加到已有的res组
res = res.concat(
// 创建一个新的数组把chunk中所有值加倍
chunk.map( function(val){
return val * 2;
} )
);
// 还有剩下的需要处理吗?
if (data.length > 0) {
// 异步调度下一次批处理
setTimeout( function(){
response( data );
}, 0 );
}
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
我们每次处理1000条数据,然后将剩余数据通过定时器方式再加入事件循环队列末尾,好让其他程序有执行的机会,不断反复,就达到了交替执行。
回调
continuation(延续)
// A
ajax( "..", function(..){
// C
} );
// B
// A 和 // B 表示现在的部分,// C表示将来的部分。前半部分立刻执行,然后是一段时间不确定的停顿。在未来的某个
时刻,如果 Ajax 调用完成,程序就会从停下的位置继续执行后半部分。
我们可以理解为:回调函数包裹或者说封装了程序的延续。
顺序的大脑
我们人类同一时刻只能执行一个有意识的、故意的动作,一心只能一用。我们必须承认,我们是单任务执行者,在任何特定的时刻,我们只能思考一件事情。
我们在假装并行执行多个任务时,实际上极有可能是在进行快速的上下文切换。我们是在两个或更多任务之间快速连续地来回切换,同时处理每个任务的微小片段。我们切换得如此之快,以至于对外界来说,我们就像是在并行地执行所有任务。这其实就是并发,JS的事件循环队列就类似于这样。
嵌套回调与链式回调
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} );
这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔(得名于嵌套缩进产生的横向三角形状)。
这种回调所表现出的问题:
- 难以追踪代码的执行顺序
- 前面的回调会阻塞后面代码的执行,若前面失败,后面都不会执行
- 代码复杂且极其脆弱
- 程序若出现执行顺序偏离的异常情况,结果难以预测
信任问题
我们以Ajax异步请求为例,有时候 ajax(..)(也就是你交付回调 continuation 的第三方)不是你编写的代码,也不在你的直接控制下。多数情况下,它是某个第三方提供的工具。我们把控制权转交给了第三方,这叫控制反转。
信任问题就出现在这个第三方,下面列举他们在调用你的回调时可能出现的出错情况:
- 调用回调过早(如参数还未传给第三方就调用)
- 调用回调过晚(或没有调用)
- 调用回调的次数太少或太多
- 没有把所需的环境 / 参数成功传给你的回调函数
- 吞掉可能出现的错误或异常
- ......
虽然我们可以在回调函数内部,构建一些防御性的机制(如参数检查),但至此,你觉得你还能够真正信任理论上(在自己的代码库中)你可以控制的工具吗?
回调最大的问题是控制反转,它会导致信任链的完全断裂。
省点回调
回调设计存在几个变体,意在解决前面讨论的一些信任问题(不是全部!)。这种试图从回调模式内部挽救它的意图是勇敢的,但却注定要失败。
为了更优雅地处理错误,有些 API 设计提供了分离回调(一个用于成功通知,一个用于出错通知):
function success(data) {
console.log( data );
}
function failure(err) {
console.error( err );
}
ajax( "http://some.url.1", success, failure );
ES6 Promise API 使用的就是这种分离回调设计。
还有一种常见的回调模式叫作error-first 风格:其中回调的第一个参数保留用作错误对象。如果成功的话,这个参数就会被清空 / 置假(后续的参数就是成功数据)。不过,如果产生了错误结果,那么第一个参数就会被置起 / 置真(通常就不会再传递其他结果):
function response(err,data) {
// 出错?
if (err) {
console.error( err );
}
// 否则认为成功
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );
- 调用超时取消事件
function timeoutify(fn,delay) {
var intv = setTimeout( function(){
intv = null;
fn( new Error( "Timeout!" ) );
}, delay ) ;
return function() {
// 还没有超时?
if (intv) {
clearTimeout( intv );
fn.apply( this, arguments );
}
};
}
ajax( "http://some.url.1", timeoutify( response, 500 ) ); // 使用上面的response函数
- 调用过早,指在某个关键任务完成之前调用回调
有效的建议: 永远异步调用回调,即使就 在事件循环的下一轮,这样,所有回调就都是可预测的异步调用了。
如果你不确定关注的 API 会不会永远异步执行怎么办呢?可以创建一个类似于这个“验证
概念”版本的 asyncify(..) 工具:
function asyncify(fn) {
var orig_fn = fn,
intv = setTimeout( function(){
intv = null;
if (fn) fn();
}, 0 );
fn = null;
return function() {
// 触发太快,在定时器intv触发指示异步转换发生之前?
if (intv) {
fn = orig_fn.bind.apply(
orig_fn,
// 把封装器的this添加到bind(..)调用的参数中,
// 以及克里化(currying)所有传入参数
[this].concat( [].slice.call( arguments ) )
);
}
// 已经是异步
else {
// 调用原来的函数
orig_fn.apply( this, arguments );
}
};
}
// 使用
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", asyncify( result ) );
a++;
最后
关于Event Loop,推荐大家看这篇《从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理》