Skip to content

Instantly share code, notes, and snippets.

@quietlynn
Created August 12, 2014 11:14
Show Gist options
  • Save quietlynn/20eb2bc472df9a02060b to your computer and use it in GitHub Desktop.
Save quietlynn/20eb2bc472df9a02060b to your computer and use it in GitHub Desktop.
JavaScript 第七次记录整理

任务的各种模式

同步模式

  • 简介:控制流停留在某个函数调用内,任务完成后返回
  • 结果:通过函数返回值传递任务的执行结果
  • 错误:通过抛出异常或者特殊返回值来表明任务失败

例子:

  • 自己编写的、操作繁重的函数,如计算 10000000 以内的素数
  • alert("Hello"), result = confirm("Are you sure?") 等浏览器模态对话框
  • NodeJS 的某些同步操作,如 fs.readFileSyncSync 结尾的函数

优点:

  • 编码简单直观,直接按顺序模式编写
  • 能使用 try 捕获异常
  • 严格按照顺序执行,所有的代码执行路径都是确定的

缺点:

  • 阻塞主线程,会使网页/服务器停止响应,影响其他任务处理
  • 用户体验差(用户可能会误认为死机)
  • 无法并行处理,在任务执行期间不能做任何其他操作
  • 很多系统或第三方API不提供同步的用法
  • 不存在将异步转换为同步的机制(不能同步等待某任务完成)
  • 因此,同步的方法不适合包含异步任务的任务(组合任务)

机制:系统事件队列

  • 简介:JS环境中,很多系统事件通知用户代码的一种机制
  • 这并不是一种任务模式,这只是一种底层实现而已
  • 但是了解这种模式对于编写异步代码是必须的,所以这里就介绍一下

例子:

setTimeout(func1, 1000); func2(); 其中 func1 和 func2 为函数。

代码的实际执行方式:

  1. setTimeout 向系统注册一个倒计时,时间为 1000 毫秒
  2. setTimeout 执行完毕返回(此时倒计时还没激发)
  3. 执行 func2();
  4. ☆ 1000 毫秒时间到,系统向事件队列添加“调用 func1”的项目,此时如果 func2 还在执行中,那么不会打断 func2 的执行
  5. func2(); 返回,用户代码结束(没有任何需要同步执行的JS代码了),控制权返回给系统(环境)
  6. 系统从事件队列中取出第一个事件(这个例子中是 “调用 func1”)
  7. 系统调用 func1(),将控制权再转交给用户代码
  8. func1 执行
  9. func1 返回,控制权又一次返还给系统。
  10. 如果此时事件队列为空,则系统可以自由分配时间进行内部处理(但不调用任何用户代码)
  11. (比如,浏览器可以利用空闲时间来绘制页面)
  12. 下一个事件进入队列时,系统在合适的时机再调用用户代码。

☆ :标记 ☆ 的两项,其顺序可能颠倒。具体取决于 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 模式

  • 简介:使用一个对象来表示任务,任务有未完成,成功和失败三个状态,并且有返回值(成功)或错误(失败)。
  • 结果: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 来捕获异步异常呢?请看下文。

coroutine 模式

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 吧。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment