跳至正文

JavaScript入门教程笔记(15)-异步操作

1 单线程模型

单线程模型是指,JavaScript只在一个线程上运行,同时只能执行一个任务。

但是,这不是说JavaScript引擎只有一个线程,事实上,JavaScript引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其它线程都是在后台配合。

JavaScript之所以采用单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源,且有可能互相修改,对于一种脚本语言来说,这太复杂了。所以,为了避免复杂性,JavaScript一开始就是单线程,这是JavaScript语言的核心特征,将来也不会改变。

2 同步任务和异步任务

任务分成两类:同步任务(synchronous)和异步任务(asynchronous)。

同步任务是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务是不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了,该任务才会进入主线程执行。异步任务不具有“堵塞”效应。

3 任务队列和事件循环

JavaScript运行时,除了一个主线程,引擎还提供一个任务队列(task queue),里面是各种需要处理的异步任务。

首先,主线程执行所有的同步任务。等到同步任务全部执行完,就去查看任务队列里面的异步任务。如果满足条件,那么异步任务就会进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,各方就结束执行。

异步任务的写法通常是回调函数,一旦异步任务重新进入主线程,就会执行对应的回调函数。

那么,JavaScript引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会不断地检查任务队列。这种检查机制,就叫做“事件循环”。

4 异步操作的模式

异步操作有几种模式。

4.1 回调函数

回调函数是异步操作最基本的方法。

function f1(callback) {
    // ...
    callback();
}

function f2() {
    // ...
}

f1(f2);

上面代码中,f2是f1的回调函数,必须是f1执行完了,才会去执行f2。

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪,而且每个任务只能指定一个回调函数。

4.2 事件监听

事件驱动模式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

首先,为f1绑定一个事件(jQuery的写法)

f1.on('done', f2);

上面代码的意思是,当f1发生done事件,就执行f2。然后改写f1:

function f1() {
    setTimeout(function() {
        // ...
        f1.trigger('done');
    }, 1000);
}

上面代码中,f1.trigger('done')表示,执行完成后,立即触发'done'事件,从而开始执行f2。

这种方法的优点是容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,有利于模块化。缺点是整个程序都要变成事件驱动型,运行流程变得不清晰,阅读代码的时候,很难看出主流程。

4.3 发布/订阅

事件可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其它任务可以向信号中心“订阅”(subscribe)这个信号,从而知道自己什么时候可以开始执行。这就叫做“发布/订阅模式”,以称“观察者模式”。

还是以jQuery举例。首先,f2向信号中心jQuery订阅done信号。

jQuery.subscribe('done', f2);

然后改写f1:

function f1() {
    setTimeout(function() {
        // ...
        jQuery.publish('done');
    }, 1000);
}

上面代码中,jQuery.publish('done')的意思是,向信号中心jQuery发布done信号,从而引发f2的执行。

f2完成执行后,可以取消订阅(unsubscribe)

jQuery.unsubscribe('done', f2);

这种方法的性质与“事件监听”类似,但是明显更优。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。

注:本文适用于ES5规范,原始内容来自 JavaScript 教程,有修改。

标签:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注