一、单线程的概念
首先javascript是一门单线程的语言,因此,JavaScript在同一个时间只能做一件事。js单线程意味着:如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务。【js是单线程的是因为:我们需要对dom元素进行操作,如果是多线程,一个线程删除dom节点的内容,一个增加节点内容,听谁的呢?】
如果一个任务要读取文件或者ajax操作,文件的读取和数据的获取都需要时间,难道要一直等到结果返回再继续执行下一个任务吗?
二、同步和异步任务
1.为什么会有同步和异步
Javascript在设计的时候,就考虑到这个问题了,主线程可以完全不用等待文件的读取完毕或ajax的加载成功,可以先挂起处于等待中的任务,先运行排在后面的任务,等到文件的读取或ajax有了结果后,再回过头执行挂起的任务,因此,任务就可以分为同步任务和异步任务。
2.同步任务
同步任务是指在主线程上排队执行的任务,只有上一个任务执行完毕,才能执行下一个。
3.异步任务
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
4.异步机制
JavaScript中的异步是怎么实现的呢?那要需要说下回调和事件循环这两个概念了。
首先要先说下任务队列,我们在前面也介绍了,异步任务是不会进入主线程,而是会先进入任务队列,任务队列其实是一个先进先出的数据结构,也是一个事件队列,比如说文件读取操作,因为这是一个异步任务,因此该任务会被添加到任务队列中,等到IO完成后,就会在任务队列中添加一个事件,表示异步任务完成啦,可以进入执行栈了,但是这时候,主线程不一定有空,当主线程处理完其它任务有空时,就会读取任务队列,读取里面有哪些事件,排在前面的事件会被优先进行处理,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务了。
单线程从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等到,直到有新的任务,这就叫做任务循环,因为每个任务都是由一个事件触发的,因此也叫作事件循环。
总的来说,JavaScript的异步机制包括以下几个步骤
(1)所有同步任务都在主线程上执行,形成一个执行栈;
(2) 主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件;
(3) 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行 ;
(4) 主线程不断的重复上面的第三步;
原文链接:blog.csdn.net/jiaweiok123…
5.异步转同步的方式
为了解决异步编程,出现了三种类似的用于解决异步操作的方案。(个人理解这种方式为了解决异步操作的,都是异步操作变成同步;需要取到上个方法返回的值,才能继续正常往下执行,见下方回调函数具体需求的描述)
1)回调函数(回调函数就是将一个函数当作另一个主函数的参数来使用的函数)
这是异步编程最基本的方法
需求: 假定有两个函数 test1 和 test2,后者等待前者的执行结果。 如果test1()是一个比较耗时的任务,就会把test1放入任务队列中,先执行test2的代码,这样test2需要的变量会报错undefind;所以可以考虑改写test1(),把test2()写成test1()的回调函数,改写如下:
//同步操作是这样的
function test1(){
setTimeout(() =>{
console.log('执行了test1');
}, 2000);
}
function test2(){
console.log('执行了test2');
}
test1()
test2() //输出结果是:执行了test2 (2s后) 执行了test1
//异步是这样的
function test1(callback){ //(主函数)
setTimeout(function () {
console.log('执行了test1'); //主函数任务代码
callback();
}, 3000);
}
function test2(){ //回调函数
console.log('执行了test2');
}
test1(test2); // 3s后输出:执行了test1 执行了test2
解读: 回调函数是传统的一种异步编程解决方案,其原理就是将一个函数当作参数传递到另一个主函数中,当主函数执行完自身的内容之后,再运行传递进来的回调函数。
优点:简单,容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱。
缺点:一个任务只能有一个回调函数
在这里补充一下回调地狱的概念:回调函数里面嵌套回调函数。
//地狱回调
setTimeout(function () { //第一层
console.log('张三');//等3秒打印张三在执行下一个回调函数
setTimeout(function () { //第二层
console.log('李四');//等2秒打印李四在执行下一个回调函数
setTimeout(function () { //第三层
console.log('王五');//等一秒打印王五
}, 1000)
}, 2000)
}, 3000)
回调地狱是为了让我们代码顺序执行的一种操作(解决异步,变成同步),但是它会使我们的可读性非常差。
2)promise
已经知道setTimeout是一种异步操作了,因此这里的例子可以将每一个setTimeout拟作一次接口请求
需求: 存在两个函数a(), b(), 需要在a() 函数所有内容执行完毕之后,再执行b()函数;但是a()函数又存在异步操作(获取数据); 如果按顺序执行a(),b(),就会导致b()先执行,出现数据内容不存在的情况(a是异步的)(可能想表达的是b函数依赖a函数的某些数据吧); 因此使用promise来处理
因为a()函数里有异步操作,就会放入任务队列,等同步操作执行完毕再从任务队列进入主线程执行;
现在想按照 a() =>b() 顺序执行;所以需要promise,通过resolve获取成功的值之后,结果作为.then的返回值,再去调用b()
先不用.then()处理异步任务
用.then() 处理异步任务
这样就达成了理想的效果,先执行a()函数,然后执行b()函数。
也可以调用catch方法捕获错误,接收rejected状态的值.
3)async-await的基础用法
async作为一个关键字放在函数前面,表示该函数是一个异步函数,异步函数意味着该函数的执行不会阻塞后面代码的执行;而 await 用于等待一个异步方法执行完成;
async/await的作用就是使异步操作以同步的方式去执行
3.1 async 函数和普通函数一样执行,是generator的语法糖。
3.2 async函数的返回值是Promise 对象,所以可以用.then方法指定下一步的操作,return语句返回的值,会成为then方法回调函数的参数
// 使用async/await获取成功的结果
// 定义一个异步函数,3秒后才能获取到值(类似操作数据库)
function getSomeThing(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('获取成功')
},3000)
})
}
async function test(){
let a = await getSomeThing();
console.log(a)
}
test(); // 3秒后输出:获取成功
3.3 基本使用示例:多个异步操作完成后才会进行后续操作
await后面的函数建议 返回 Promise对象(即return new Promise((resolve,reject)=>{})) 并且主动调用resolve()才能够进行后续的then或者是后续的await操作,倘若是执行了reject(reject的参数会被catch方法的回调函数接收到)或者throw抛出错误之类的就会导致当前执行中断。
注意1:await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中。
注意2:多个await命令后面的异步操作,如果不存在先后关联,最好让它们同时触发。不然会增加耗时;
这样做的好处就是,如果两个await直接运行则需要2秒的时间才会运行后续的内容,
但是像这样处理一下,两个就会同时开始,即只需要1秒就可以运行后续的内容了。