单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其余任务都必须在后面排队等待。
注意,JavaScript 只在一个线程上运行,不表明 JavaScript 引擎只有一个线程。事实上,
JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其余线程都是在后台配合。
JavaScript 之因此采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,缘由是不想让浏览器变得太复杂,
由于多线程须要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来讲,这就太复杂了。若是 JavaScript 同时有两个线程,
一个线程在网页 DOM 节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?是否是还要有锁机制?
因此,为了不复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,未来也不会改变。
程序里面全部的任务,能够分红两类:同步任务(synchronous)和异步任务(asynchronous)。
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务能够执行了(好比 Ajax 操做从服务器获得告终果),
该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会立刻运行,也就是说,异步任务不具备“堵塞”效应。
举例来讲,Ajax 操做能够看成同步任务处理,也能够看成异步任务处理,由开发者决定。若是是同步任务,主线程就等着 Ajax 操做返回结果,再往下执行;
若是是异步任务,主线程在发出 Ajax 请求之后,就直接往下执行,等到 Ajax 操做有告终果,主线程再执行对应的回调函数。
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各类须要当前程序处理的异步任务。
(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。) 首先,主线程会去执行全部的同步任务。等到同步任务所有执行完,就会去看任务队列里面的异步任务。
若是知足条件,那么异步任务就从新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。 异步任务的写法一般是回调函数。一旦异步任务从新进入主线程,就会执行对应的回调函数。
若是一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会从新进入主线程,由于没有用回调函数指定下一步的操做。 JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,
引擎就会去检查那些挂起来的异步任务,是否是能够进入主线程了。这种循环检查的机制,就叫作事件循环(Event Loop)。
维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
把f2
写成f1
的回调函数。javascript
function f1(callback) { // ... callback(); } function f2() { // ... } f1(f2);
回调函数的优势是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤为是多个回调函数嵌套的状况),并且每一个任务只能指定一个回调函数。java
f1.on('done', f2); function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); }
f1.trigger('done')
表示,执行完成后,当即触发done
事件,从而开始执行f2
promise
这种方法的优势是比较容易理解,能够绑定多个事件,每一个事件能够指定多个回调函数,并且能够“去耦合”(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。浏览器
事件彻底能够理解成“信号”,若是存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其余任务能够向信号中心“订阅”(subscribe)这个信号,从而知道何时本身能够开始执行。这就叫作“发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。服务器
f2
向信号中心jQuery
订阅done
信号。多线程
jQuery.subscribe('done', f2); function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); }
上面代码中,jQuery.publish('done')
的意思是,f1
执行完成后,向信号中心jQuery
发布done
信号,从而引起f2
的执行。异步
f2
完成执行后,能够取消订阅(unsubscribe)。async
jQuery.unsubscribe('done', f2);
这种方法的性质与“事件监听”相似,可是明显优于后者。由于能够经过查看“消息中心”,了解存在多少信号、每一个信号有多少订阅者,从而监控程序的运行。模块化
Promise 对象经过自身的状态,来控制异步操做。Promise 实例具备三种状态。函数
异步操做未完成(pending) 异步操做成功(fulfilled) 异步操做失败(rejected)
上面三种状态里面,fulfilled
和rejected
合在一块儿称为resolved
(已定型)。
这三种的状态的变化途径只有两种。
从“未完成”到“成功” 从“未完成”到“失败”
一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。
所以,Promise 的最终结果只有两种。
异步操做成功,Promise 实例传回一个值(value),状态变为fulfilled。 异步操做失败,Promise 实例抛出一个错误(error),状态变为rejected。
JavaScript 提供原生的Promise
构造函数,用来生成 Promise 实例。
var promise = new Promise(function (resolve, reject) { // ... if (/* 异步操做成功 */){ resolve(value); } else { /* 异步操做失败 */ reject(new Error()); } });
上面代码中,Promise
构造函数接受一个函数做为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用本身实现。
resolve
函数的做用是,将Promise
实例的状态从“未完成”变为“成功”(即从pending
变为fulfilled
),在异步操做成功时调用,并将异步操做的结果,做为参数传递出去。reject
函数的做用是,将Promise
实例的状态从“未完成”变为“失败”(即从pending
变为rejected
),在异步操做失败时调用,并将异步操做报出的错误,做为参数传递出去。
下面是一个例子。
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100)
上面代码中,timeout(100)
返回一个 Promise 实例。100毫秒之后,该实例的状态会变为fulfilled
。
Promise 的用法,简单说就是一句话:使用then
方法添加回调函数。可是,不一样的写法有一些细微的差异,请看下面四种写法,它们的差异在哪里?
// 写法一 f1().then(function () { return f2(); }); // 写法二 f1().then(function () { f2(); }); // 写法三 f1().then(f2()); // 写法四 f1().then(f2);
为了便于讲解,下面这四种写法都再用then
方法接一个回调函数f3
。写法一的f3
回调函数的参数,是f2
函数的运行结果。
f1().then(function () { return f2(); }).then(f3);
写法二的f3
回调函数的参数是undefined
。
f1().then(function () { f2(); return; }).then(f3);
写法三的f3
回调函数的参数,是f2
函数返回的函数的运行结果。
f1().then(f2()) .then(f3);
写法四与写法一只有一个差异,那就是f2
会接收到f1()
返回的结果。
f1().then(f2) .then(f3);
优势:让回调函数变成了规范的链式写法,程序流程能够看得很清楚。它有一整套接口,能够实现许多强大的功能,好比同时执行多个异步操做,等到它们的状态都改变之后,再执行一个回调函数;再好比,为多个回调函数中抛出的错误,统一指定处理方法等等。
并且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,不管什么时候查询,都能获得这个状态。这意味着,不管什么时候为 Promise 实例添加回调函数,该函数都能正确执行。因此,你不用担忧是否错过了某个事件或信号。若是是传统写法,经过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
缺点:编写的难度比传统写法高,并且阅读代码也不是一眼能够看懂。你只会看到一堆then
,必须本身在then
的回调函数里面理清逻辑。
Promise 的回调函数属于异步任务,会在同步任务以后执行。
new Promise(function (resolve, reject) { resolve(1); }).then(console.log); console.log(2); // 2 // 1
上面代码会先输出2,再输出1。由于console.log(2)
是同步任务,而then
的回调函数属于异步任务,必定晚于同步任务执行。
可是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间必定早于正常任务。
setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 // 2 // 1
上面代码的输出结果是321
。这说明then
的回调函数的执行时间,早于setTimeout(fn, 0)
。由于then
是本轮事件循环执行,setTimeout(fn, 0)
在下一轮事件循环开始时执行。