- 简介:控制流停留在某个函数调用内,任务完成后返回
- 结果:通过函数返回值传递任务的执行结果
- 错误:通过抛出异常或者特殊返回值来表明任务失败
例子:
- 自己编写的、操作繁重的函数,如计算 10000000 以内的素数
alert("Hello")
,result = confirm("Are you sure?")
等浏览器模态对话框- NodeJS 的某些同步操作,如
fs.readFileSync
等Sync
结尾的函数
优点:
- 编码简单直观,直接按顺序模式编写
- 能使用
try
捕获异常 - 严格按照顺序执行,所有的代码执行路径都是确定的
缺点:
- 阻塞主线程,会使网页/服务器停止响应,影响其他任务处理
- 用户体验差(用户可能会误认为死机)
- 无法并行处理,在任务执行期间不能做任何其他操作
- 很多系统或第三方API不提供同步的用法
- 不存在将异步转换为同步的机制(不能同步等待某任务完成)
- 因此,同步的方法不适合包含异步任务的任务(组合任务)
- 简介:JS环境中,很多系统事件通知用户代码的一种机制
- 这并不是一种任务模式,这只是一种底层实现而已
- 但是了解这种模式对于编写异步代码是必须的,所以这里就介绍一下
例子:
setTimeout(func1, 1000); func2();
其中 func1 和 func2 为函数。
代码的实际执行方式:
setTimeout
向系统注册一个倒计时,时间为 1000 毫秒setTimeout
执行完毕返回(此时倒计时还没激发)- 执行
func2();
- ☆ 1000 毫秒时间到,系统向事件队列添加“调用 func1”的项目,此时如果 func2 还在执行中,那么不会打断 func2 的执行
- ☆
func2();
返回,用户代码结束(没有任何需要同步执行的JS代码了),控制权返回给系统(环境) - 系统从事件队列中取出第一个事件(这个例子中是 “调用 func1”)
- 系统调用
func1()
,将控制权再转交给用户代码 - func1 执行
- func1 返回,控制权又一次返还给系统。
- 如果此时事件队列为空,则系统可以自由分配时间进行内部处理(但不调用任何用户代码)
- (比如,浏览器可以利用空闲时间来绘制页面)
- 下一个事件进入队列时,系统在合适的时机再调用用户代码。
☆ :标记 ☆ 的两项,其顺序可能颠倒。具体取决于 func2()
的执行时间是否大于 1000 毫秒。 但是,这两项颠倒并不会造成代码上的任何差异。
因为事件队列需要等待空闲(之前的代码返回),所以 func1
的实际执行时间可能会远远超过 1000 毫秒(如果 func2 真的执行了很久)。
func1 始终在 func2 之后执行,因为 func1 需要先进入事件队列,而 func2 是顺序执行。
JS 的机制决定了在主线程上,同一时间只能有一个执行流。也就是说,不会有两个函数同时在主线程上执行。
有一些特殊的系统调用允许代码在另外一个线程执行,但通常来说线程之间不能共享任何变量(可以认为是另一个执行环境),只能通过消息来传递数据。
而消息接受方是使用事件队列来处理消息的,所以……即使是这样的情况,也不存在两段代码在一个线程同时执行。
因此JS代码也无需任何线程安全的特殊机制,不需要线程锁什么的(因为一段代码只属于一个线程,不存在同时修改同一个变量的情况)。
- 简介:执行任务时传递进去一个函数,当任务执行完毕后该函数会被调用
- 结果:通过回调函数的参数来传递结果
- 错误:通过回调函数的参数来传递错误。成功失败可以共用同一个回调,也可以分成两个回调函数。
代码:
calcSum(1, 2, function (result) {
console.log(result); // 3
});
例子:
setTimeout(func, 1000);
,其中 func 是一个函数- 一些比较新的浏览器API,如
Notification.requestPermission(callback);
。 -
- 在 Chrome 中效果是顶端出现要求用户确认的通知,但页面仍然可以正常使用。当用户同意或拒绝后调用 callback.
- NodeJS 的某些异步操作,如
fs.readFile('a.txt', function (err, result) { /* ... */ });
- 自己也很容易实现这样的模式,如
var calcSum = function (a, b, callback) { callback(a + b); };
优点:
- 实现较为简单,只需在任务完成后调用一个参数即可。不需要第三方库支持。
- 同步和异步都可以用
- 对于 setTimeout 等,能有效利用事件队列机制,实现在空闲时再调用回调。
缺点:
- 文档写得不好的话,真的很难理解和调用API
- 完成一件任务需要写很多回调函数,从而导致代码嵌套太深,难以管理
- 尤其是 NodeJS ,参见: http://callbackhell.com/
- 如果一个任务需要包含几个子任务,那么不仅要写回调还要调用回调,很混乱
- 还不够面向对象,也难以同时处理多种不同类型的任务
- 简介:适合于对象可能会激发多种事件的情况
- 结果:通过事件处理函数(其实就是回调函数)来传递结果
- 错误:可以通过事件处理回调函数,也可以用另外一个事件(比如 error 事件)
代码:
// DOM
someElement.addEventListener('click', function (e) {
console.log(e);
}, false);
// jQuery
$('#some-element').on('click', function (e) {
console.log(e);
});
例子:
- DOM 和 jQuery ,以及很多第三方库
- AJAX 请求
优点:
- 一个对象可以有多个事件,可以在创建对象之后再添加事件,方便使用
- 在事件需要反复激发时尤其适合
- 事件处理函数是依附于对象存在的,方便管理
- 允许添加/删除事件处理函数
缺点:
- 如果事件先激发了,再添加事件处理函数,那么就收不到任何消息
- 所以事件其实并不适合表示一次性的”任务“
- 自己实现事件模式很复杂,需要借助第三方库
- 仍然无法避免嵌套的尴尬
- 如果一个任务包含的子任务需要事件,那么会比只有回调还要混乱……
- 简介:使用一个对象来表示任务,任务有未完成,成功和失败三个状态,并且有返回值(成功)或错误(失败)。
- 结果:Promise 对象内保存结果。使用
.then
方法来添加结果回调。 - 失败:Promise 对象内保存错误。使用
.catch
方法来捕获错误。
代码:
var request = fetchUrl('http://www.google.com/');
request.then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
例子:
- jQuery deferred (类似于 Promise ,但不支持链)
- 一些第三方库选择使用 Promise
- 一些正在规划中的 HTML5 新的 API
优点:
- 对象表示任务,对象自身是有状态的
- 任务已经完成之后仍然可以使用
.then
和.catch
,因为任务会保存返回值或者错误 - 事件会错过成功或失败事件,然而 Promise 不会错过,原因如上
- Promise 可以链式使用,彻底避免了 callback hell,代码可读性强
- ES6 已经添加了 Promise 支持,无需第三方库即可实现,且实现简便
缺点:
- 如果没有ES6,就需要用第三方库来构造 Promise 对象 (推荐 Bluebird)
- 用对象管理任务有一点开销,不过对性能影响不大……
- 需要用第三方库来把回调样式的 API 转换成 Promise 便于使用(推荐 Bluebird)
var aFollowedByB = A.then(function f1(aResult) { return B; })
执行这些语句后,aFollowedByB 是一个新的 Promise (不是 A 也不是 B)。
新的 Promise 的规则如下:
- 当 A 完成, f1 回调完成,并且 B 完成后,新的 Promise 才完成,且返回结果为 B 的结果。
- 当 A 失败时新 Promise 直接失败,错误为 A 的错误。不调用 f1 和 B.
- 当 A 完成,但 f1 抛出异常时,新 Promise 直接失败,错误为 f1 抛出的异常
- 当 A 完成且 f1 完成时,新 Promise 的状态与 B 相同
- 也就是说 B 成功则 aFollowedByB 成功,B 失败则 aFollowedByB 失败,且返回值/错误与 B 相同
总结来说,就是 .then
方法返回一个新的 Promise ,而这个新的 Promise 在 A, B 都成功时成功,
在 A, B 任意一个失败时,或 f1 抛出异常时直接失败。
也就是说, .then
可以用来表示连续的任务,即先做A再做B。实际使用的例子如下:
var request = fetchUrl('http://api.example.com/hello');
request.then(function (response) {
return JSON.parse(response.body); // *1
}).then(function (data) {
return writeToFile('data.txt', data); // *2
}).catch(function (error) {
console.log(error); // *3
});
这个例子可以理解成,先请求一个网址,然后将请求结果转换成 JSON (*1),最后再将结果写到文件中(*2)。
一旦任何一个环节出了错误,那么会直接打印错误(*3),而不会继续执行了。
Promise 不仅能提供连续做事情的方法,而且还能统一捕获多个阶段的错误,所以更接近于顺序结构。
那么,有没有办法写起来完全就像是顺序结构呢?能不能用 try 来捕获异步异常呢?请看下文。
run(function *() {
var url = "http://www.google.com";
var response = yield makeHttpRequest(url)
try {
var filelength = yield writeFile("data.txt", response.data)
} catch (ex) {
console.log(ex);
}
});
这个模式需要 ES6 Generator 的支持。在 NodeJS 里,通过 --harmony
参数启用,而在
浏览器中不推荐使用。
注意 yield 关键字,这表示 Generator 的迭代返回,而在 coroutine 模式下,yield 返回一个 Promise 相当于等待 Promise 成功后返回值。如果 Promise 成功,返回 Promise 的返回值并继续。如果 Promise 失败会直接抛出异常,能被 try..catch 捕获。
注意第一行的 function *
语法,其中星号表示这个函数是 Generator 。而 run 函数才
是真正处理 Promise 的地方,负责等待 yield 返回的 Promise 并提供结果或抛出异常。这
个 run 函数可以是第三方库的代码。
需要注意的是,这个代码只是看起来像是顺序模式,但其实并不是同步的,而是异步的。只 不过语法上有差别,而概念上和 Promise 还是一样的。
JS 的未来将会走向 coroutine 模式,一旦 ES6 得到广泛支持,我们就可以开始写这样的代码了。 或者在自己的NodeJS项目中已经可以开始用了。
不过前提是……我们需要更多的库和系统API支持 Promise ……
所以,今天我们能做的,就是尽力推广 Promise 的使用。在写自己的代码的时候,多考虑 Promise 吧。