文章简介
本文为【JavaScript 漫游】专栏的第 017 篇文章,主要记录了 ES5 规范中异步操作的基本知识点。
- 单线程模型
- 同步任务和异步任务
- 任务队列和事件循环
- 异步操作的模式
单线程模型
单线程模型指的是,JS 只在一个线程上运行。它同时只能执行一个任务,其他任务都必须在后面排队等待。
JS 之所以采用单线程模型,是不想让浏览器变得太复杂。多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说过于复杂。
采用该模型的好处是实现起来比较简单,执行环境相对单纯。而坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。
常见的浏览器无响应,往往就是因为某一段 JS 代码长时间运行,导致整个页面卡在这个地方,其他任务无法执行。JS 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不畅通,就会导致脚本的长时间停滞。
JS 采用了事件循环机制来解决上述问题。如果想要利用多核 CPU 的计算能力,HTML5 提出了 Web Worker 标准,允许 JS 脚本创建多个线程,分为主线程和其他子线程,子线程完全受主线程控制,且不得操作DOM。
总的来说,单线程模型虽然对 JS 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JS 程序是不会出现堵塞的。
同步任务与异步任务
程序里面所有的任务,可以分成两类:同步任务synchronous
和异步任务 asynchronous
。
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有堵塞效应。
举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。
任务队列和事件循环
JS 运行时,除了一个正在运行的主线程,引擎还提供了一个任务队列(task queue
),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。
异步操作的模式
回调函数
回调函数是异步操作最基本的方法。
下面是两个函数 f1
和 f2
,编程的意图是 f2
必须等到 f1
执行完成,才能执行。
function f1(callback) {// ...callback();
}function f2() {// ...
};f1(f2);
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
事件监听
另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以 f1
和 f2
为例。首先,为 f1
绑定一个事件(这里采用的是 jQuery
的写法)。
f1.on('done', f2);
当 f1
发生 done
事件,就执行 f2
。然后,对 f1
进行改写:
function f1(){setTimeout(function() {// ...f1.trigger('done');}, 1000);
};
f1.trigger('done')
表示,执行完成后,立即触发 done
事件,从而开始执行 f2
。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以去耦合(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布/订阅
事件完全可以理解成信号,如果存在一个信号中心,某个任务执行完成,就向信号中心发布一个信号,其他任务可以向信号中心订阅这个信号,从而知道什么时候自己可以开始执行。这就叫做发布/订阅模式(publish-subscribe pattern
),又叫观察者模式(observer pattern
)。
这个模式有多种实现,具体的实现可以在使用 vue
或 react
技术栈的时候再进行研究,这里不作记录。