内容提要:javascript
同步:必定要等任务执行完了,获得结果,才执行下一个任务。 异步:不等任务执行完,直接执行下一个任务。html
平常工做的大部分场景,咱们均可以使用同步代码来实现。告诉编译器,你应该先作什么,再作什么:前端
do('来左边儿 跟我一块儿画个龙'); // 第1步
do('在你右边儿 画一道彩虹(走起)'); // 第2步
do('来左边儿 跟我一块儿画彩虹'); // 第3步
do('在你右边儿 再画个龙(别停)'); // 第4步
do('在你胸口上比划一个郭富城'); // 第5步
do('...'') //...
复制代码
若是函数是同步的,即便调用函数执行的任务比较耗时,也会一直等待直到获得预期结果。java
可是也会有一些场景,同步并不能知足,这时就须要用到异步的写法:面试
// 定时器
setTimeout(() => {
console.log('Hello');
}, 3000)
//读取文件
fs.readFile('hello.txt', 'utf8', function(err, data) {
console.log(data);
});
//网络请求
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数
复制代码
若是函数是异步的,发出调用以后,立刻返回,可是不会立刻返回预期结果。调用者没必要主动等待,当被调用者获得结果以后会经过回调函数主动通知调用者。编程
"异步模式"很是重要。在浏览器端,耗时很长的操做都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操做。在服务器端,"异步模式"甚至是惟一的模式,由于执行环境是单线程的,若是容许同步执行全部http请求,服务器性能会急剧降低,很快就会失去响应。promise
在上面介绍异步的过程当中就可能会纳闷:既然JavaScript是单线程,怎么还存在异步,那些耗时操做到底交给谁去执行了?浏览器
众所周知javascript是单线程的,它的设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一是保证UI线程必定不能阻塞,不然体验不佳,甚至界面卡死。bash
所谓"单线程",就是指一次只能完成一件任务。若是有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。服务器
JavaScript其实就是一门语言,说是单线程仍是多线程得结合具体运行环境。JS的运行一般是在浏览器中进行的,具体由JS引擎去解析和运行。下面咱们来具体了解一下浏览器。
目前最为流行的浏览器为:Chrome,IE,Safari,FireFox,Opera。浏览器的内核是多线程的。 一个浏览器一般由如下几个常驻的线程:
须要注意的是,渲染线程和JS引擎线程是不能同时进行的。渲染线程在执行任务的时候,JS引擎线程会被挂起。由于JS能够操做DOM,若在渲染中JS处理了DOM,浏览器可能就不知所措了。
一般讲到浏览器的时候,咱们会说到两个引擎:渲染引擎和JS引擎。渲染引擎就是如何渲染页面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不一样的引擎对同一个样式的实现不一致,就致使了常常被人诟病的浏览器样式兼容性问题。这里咱们不作具体讨论。
JS引擎能够说是JS虚拟机,负责JS代码的解析和执行。一般包括如下几个步骤:
不一样浏览器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。
之因此说JavaScript是单线程,就是由于浏览器在运行时只开启了一个JS引擎线程来解析和执行JS。那为何只有一个引擎呢?若是同时有两个线程去操做DOM,浏览器是否是又要不知所措了。
因此,虽然JavaScript是单线程的,但是浏览器内部不是单线程的。一些I/O操做、定时器的计时和事件监听(click, keydown...)等都是由浏览器提供的其余线程来完成的。
经过以上了解,能够知道其实JavaScript也是经过JS引擎线程与浏览器中其余线程交互协做实现异步。可是回调函数具体什么时候加入到JS引擎线程中执行?执行顺序是怎么样的?
这一切的解释就须要继续了解消息队列和事件循环。
如上图所示,左边的栈存储的是同步任务,就是那些能当即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不须要回调函数的操做均可归为这一类。 右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每一个异步任务都和回调函数相关联。
JS引擎线程用来执行栈中的同步任务,当全部同步任务执行完毕后,栈被清空,而后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,若是没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
上图以AJAX异步请求为例,发起异步任务后,由AJAX线程执行耗时的异步操做,而JS引擎线程继续执行堆中的其余同步任务,直到堆中的全部异步任务执行完毕。而后,从消息队列中依次按照顺序取出消息做为一个同步任务在JS引擎线程中执行,那么AJAX的回调函数就会在某一时刻被调用执行。
引用一篇文章中提到的考察JavaScript异步机制的面试题来具体介绍。
执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(>5s)后,再点击两下,整个过程的输出结果是什么?
setTimeout(function(){
for(var i = 0; i < 100000000; i++){}
console.log('timer a');
}, 0)
for(var j = 0; j < 5; j++){
console.log(j);
}
setTimeout(function(){
console.log('timer b');
}, 0)
function waitFiveSeconds(){
var now = (new Date()).getTime();
while(((new Date()).getTime() - now) < 5000){}
console.log('finished waiting');
}
document.addEventListener('click', function(){
console.log('click');
})
console.log('click begin');
waitFiveSeconds();
复制代码
要想了解上述代码的输出结果,首先介绍下定时器。 setTimeout
的做用是在间隔必定的时间后,将回调函数插入消息队列中,等栈中的同步任务都执行完毕后,再执行。由于栈中的同步任务也会耗时,因此间隔的时间通常会大于等于指定的时间。 setTimeout(fn, 0)
的意思是,将回调函数fn马上插入消息队列,等待执行,而不是当即执行。看一个例子:
setTimeout(function() {
console.log("a")
}, 0)
for(let i=0; i<10000; i++) {}
console.log("b")
// b a
复制代码
打印结果代表回调函数并无马上执行,而是等待栈中的任务执行完毕后才执行的。栈中的任务执行多久,它就得等多久。
理解了定时器的做用,那么对于输出结果就容易得出了。
首先,先执行同步任务。其中 waitFiveSeconds
是耗时操做,持续执行长达5s。
0
1
2
3
4
click begin
finished waiting
复制代码
而后,在JS引擎线程执行的时候,'timer a'对应的定时器产生的回调、 'timer b'对应的定时器产生的回调和两次 click 对应的回调被前后放入消息队列。因为JS引擎线程空闲后,会先查看是否有事件可执行,接着再处理其余异步任务。所以会产生 下面的输出顺序。 。
click
click
timer a
timer b
复制代码
最后,5s 后的两次 click 事件被放入消息队列,因为此时JS引擎线程空闲,便被当即执行了。
click
click
复制代码
理解了JS异步实现的机制后,咱们再看看JS异步编程方式的演化。
异步发展史能够简单概括为: callback -> promise -> generator + co -> async+await(语法糖)
下面会一步一步展示各类方式。
这是异步编程最基本的用法。
实现1秒后打印消息:
function asyncPrint(value, ms) {
setTimeout(() => {
console.log(value);
},ms)
}
asyncPrint('Hello World', 1000);
复制代码
回调函数的优势是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,并且每一个任务只能指定一个回调函数。
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最先提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
Promise 究竟是什么。当咱们经过new Promise建立 Promise 的时候,你实际建立的只是一个简单的 JavaScript 对象,这个对象能够调用两个方法then和catch。这是关键所在,当 Promise 的状态变为fulfilled的时候,传递给.then的函数将会被调用。若是 Promise 的状态变为rejected,传递给.catch的函数将会被调用。这就意味着,在你建立 Promise 的时候,要经过.then将你但愿异步请求成功时调用的函数传递进来,经过.catch将你但愿异步请求失败时调用的函数传递进来。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操做成功 */){
resolve(value);
} else {
reject(error);
}
});
复制代码
好比,上面的例子,能够写成:
function asyncPrint(value, ms) {
return new Promise((resolve,reject) => {
setTimeout(resolve, ms, value);
})
}
asyncPrint('Hello Wolrd', 1000).then((value) => {
console.log(value);
});
复制代码
Promise 不只能够避免回调地狱,还能够统一捕获失败的缘由。但这种方法的缺点就是编写和理解,都相对比较难
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
复制代码
使用 Generator,上面的例子能够改写成:
function *asyncPrint(value, ms) {
let timer = yield setTimeout((value)=>{console.log(value)}, ms, value);
return timer;
}
var a = asyncPrint('Hello World', 1000);
a.next();
复制代码
随着前端的迅速发展,大神们以为要像同步代码同样写异步,co问世了,co是 TJ 大神结合了promise 和 生成器 的一个库,实际上仍是帮助咱们自动执行迭代器
function asyncPrint(value, ms) {
return new Promise((resolve,rejuect) => {
setTimeout(resolve, ms, value);
})
}
function *print() {
let a = yield asyncPrint('Hello World', 1000);
return a;
}
function co(gen) {
let it = gen();//咱们要让咱们的生成器持续执行
return new Promise(function (resolve, reject) {
!function next(lastVal) {
let {value,done} = it.next(lastVal);
if(done){
resolve(value);
}else{
value.then(next,reject);
}
}()
});
}
co(print).then(function (data) {
console.log(data);
});
复制代码
async await是语法糖,内部是generator+promise实现 async函数就是将Generator函数的星号(*)替换成async,将yield替换成await。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 1000);
复制代码
有了前面内容的热身,咱们直接趁热打铁,再来看一道比较典型的问题。
红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
请分别用上面的几种方法实现,具体实现可参考:红绿灯任务控制
因为篇幅的缘故,其中没有提到的宏任务、微任务,以及 Promise、 Generator基础 等知识点,你们能够自行百度或Google。
以上就是对 JavaScript 异步编程的所有介绍,若是以为有收获,欢迎点赞和留言咯。