前言
正文
一、同步和异步
同步
在学习异步之前我们先来看一下同步,比如在调用函数取得返回值的时候,能够直接得到预期结果(得到了预期的返回值),是按照你的代码顺序执行的,是连续的,那么就说这个函数是同步执行的。
下边看一个例子:
//在函数返回的时候,获得了预期的效果,即在控制台上打印了‘123’
var A = function(){};
A.prototype.n = 123;
var b = new A();
console.log(b,n); // 123
如果函数是同步的,即是调用函数执行的任务比较耗时,也会一直等待直到得到预期结果。 因为它是按代码执行顺序执行的。
异步
如果在调用函数返回值的时候,不直接得到预期结果(预期的返回值),而是需要通过一定的方式获得,是不连续的不按代码顺序执行的,那么就可以说这个函数是异步的。
如下所示:
//读取文件
fc.readFile('hello','utf8',function(err,data){
console.log(data)
});
//网络请求
var pzh = new XMLHttpRequest();
pzh.onreadystatechange = yyy; // 这里添加回调函数
pzh.open('GET',url);
pzh.send();//发起函数
上述示例中读取文件函数 readFile和网络请求的发起函数 ,send都将执行耗时操作,虽然函数会立即返回,但是不能立刻获取预期的结果,因为耗时操作交给其他线程执行,暂时获取不到预期结果。而在JavaScript中通过回调函数 function(err, data) { console.log(data); }和 onreadystatechange ,在耗时操作执行完成后把相应的结果信息传递给回调函数,通知执行JavaScript代码的线程执行回调。
简单来说:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。
二、首先要知道的异步机制
浏览器内核的多线程
我们都知道JavaScript是单线程的,但是浏览器的内核是多线程的;他们在内核的控制下互相配合以保持同步,一个浏览器至少实现三个常驻:Javascrpt引擎线程、GUI渲染线程、浏览器事件触发线程。
-
JS引擎:基于事件驱动单线程执行的,JS引擎一直等待这任务队列中任务的到来,然后加以处理;浏览器无论什么时候都只有一个JS线程在运行JS程序。
-
GUI渲染线程:当界面需要重绘或由于某种操作引发回流时,线程就会执行。这里需要注意的是,渲染线程和JS引擎线程是不能同时进行的。
-
事件触发线程:当一个事件被触发时,该线程会把世间添加到等待队列的队尾,等待JS引擎的处理,这些时间可来自JavaScript引擎执行当前的代码块,如:setTimeOut,也可以来自浏览器内核和其他线程如鼠标点击;AJAX异步请求等,但是由于JS 的单线程关系,所有这些事情都得排队等待JS引擎处理。
事件循环机制
如上图所示,左边的栈存储的是同步任务,就是那些能立即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不需要回调函数的操作都可归为这一类。
右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每个异步任务都和回调函数相关联。
JS引擎线程用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环(Eventloop)。
什么是宏任务与微任务?
我们都知道 Js 是单线程的,但是一些高耗时操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。
在异步模式下,创建异步任务主要分为宏任务与微任务两种。ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。
- 1)宏任务 (macrotask):优先级低,先定义的先执行。包括:ajax,setTimeout,setInterval,事件绑定,postMessage,MessageChannel(用于消息通讯)。
- 2)微任务 (microtask):优先级高,并且可以插队,不是先定义先执行。包括:promise.then,async/await [generator],requestAnimationFrame,observer,MutationObserver,setImmediate。
由上图可以看到:
从JS主线程(整体代码)开始第一次循环,发起异步任务后,由(橙色)线程执行异步操作,而JS引擎主线程继续执行堆中的其他同步任务,直到堆中的所有异步任务执行完毕。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局)。然后执行所有的micro-task,当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的macro-task,这样一直循环下去。
根据事件循环机制,我们重新梳理一下流程:
1)首先执行栈里的任务
2)先找微任务队列,如果微任务队列中有,先从微任务队列中,一般按照存放顺序获取并且去执行。
3)如果微任务队列中没有,则再去宏任务队列中查找,在宏任务队列中,一般是按照谁先到达执行的条件,就先把谁拿出来执行。
4)以此循环
明白事件循环之后我们要知道Javascript异步编程先后经历了四个阶段,分别是Callback阶段,Promise阶段,Generator阶段和Async/Await阶段。
三、回调函数(Callback)阶段
回调函数是异步操作最基本的方法。
demo1:假定有一个异步操作(asyncFn),和一个同步操作(normalFn)。
function asyncFn(){
setTimeout(() => {
console.log('asyncFn');
),0)
}
function normalFn(){
console.log('normalFn');
}
asyncFn(); //asyncFn
normalFn(); //normalFn
如果按照正常的JS处理机制来说,同步操作一定发生在异步之前。如果我想要将顺序改变,最简单的方式就是使用回调(callback)的方式处理。
function asyncFn(callback){
setTimeout(() => {
console.log('asyncFn');
callback();
},0);
}
function normalFn(){
console.log('normalFn');
}
asyncFn(normalFn);
//asyncFn
//normalFn
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况,容易出现回调地狱,可读性差),而且每个任务只能指定一个回调函数。此外不能使用 try catch 捕获错误,不能直接return。
回调函数易混淆点——传参:
一,将回调函数的参数作为与回调函数同等级的参数进行传递。
二,回调函数的参数在调用回调函数内部创建。
事件监听、发布订阅
事件监听
事件监听也是一种非常常见的异步编程模式,它是一种典型的逻辑分离方式,对代码解耦很有用处。
下边看例子:还是以函数f1和f2为例
f1.on('done', f2); //f2必须等到f1执行完成,才可执行
function f1() {
setTimeout(function () { // ...
f1.trigger('done');
}, 1000);
}
以上,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”,有利于实现模块化。
缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
发布订阅模式
发布订阅式的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。
假定,一家三口,妈妈作为“发布者”(publisher)实施和发布信号,爸爸作为中介“订阅”(subscribe)和处理这个信号,最后小明"订阅者"(subscriber)知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”(publish-subscribe pattern)。
下边来看代码:
//订阅者接收到消息
function eat() {
console.log('妈妈做好饭啦,去吃饭啦');
}
function cooking() {
console.log('妈妈认真做饭中');
//发布者向订阅中介发布消息
setTimeout(() => {
console.log('孩儿他爸饭做好了,叫小明来吃饭')
Dad.publish("done");//中介接收消息
},3000)
}
function read(){
console.log('小明假装学习') //订阅者等消息
Dad.subscribe('done',eat);
}
//执行代码
cooking();
read()
/*执行顺序
妈妈认真做饭中
小明假装学习
孩儿他爸饭做好了,叫小明来吃饭
妈妈做好饭啦,去吃饭啦
*/
这种模式下实现的异步编程,本质上还是通过回调函数实现的 ,但是依然存在回调嵌套和无法捕捉异常问题的情况,接下来进入Promise阶段,看看是否能解决这两个问题。
四、Promise阶段
Promise 并不是指某种特定的某个实现,它是一种规范(PromiseA+规范),是一套处理JavaScript异步的机制。
1.Promise的三种状态
- Promise有三种状态pending,fulfilled和rejected
- 状态转换只能是 pending到 resolved
- 或者pending到 rejected
状态一旦转换完成,不能再次转换。
可以由下图表示:
附上代码栗子:
let p = new Promise((resolve,reject) => {
reject('reject');
resolve('success') //无效代码不会执行
})
p.then(
value => {
console.log(value)
},
reason => {
console.log(reason) //reject
}
)
当我们构造Promise 的时候,构造函数内部的代码是立即执行的
2.链式Promise
先看两个例子:
demo1;
//例1:
Promise.resolve(1)
.then(res => {
console.log(res); //打印 1
return 2 //包装成Promsie.resolve(2)
})
.catch(err => 3); //这里catch会捕获没有捕获的异常
.then(res => console.log(res)) //打印 2
当Promise创建对象调用resolve(...)或reject(...)时,这个Promise通过then(...)注册的回调函数就会在新的异步时间点上被触发。(then的链式调用); 在then中使用return,那么return的值会被Promise.resolve()包装。
demo2:以家务分配为例:
function read() {
console.log('小明认真读书');
}
function eat() {
return new Promise((resolve, reject) => {
console.log('好嘞,吃饭咯');
setTimeout(() => {
resolve('饭吃饱啦');
}, 1000)
})
}
function wash() {
return new Promise((resolve, reject) => {
console.log('唉,又要洗碗');
setTimeout(() => {
resolve('碗洗完啦');
}, 1000)
})
}
const cooking = new Promise((resolve, reject)=>{
console.log('妈妈认真做饭');
setTimeout(() => {
resolve('小明快过来,开饭啦');
}, 2000);
})
cooking.then(msg => {
console.log(msg);
return eat();
}).then(msg => {
console.log(msg);
return wash();
}).then(msg => {
console.log(msg);
console.log('做完家务了,可以玩了')
})
read();
/* 执行顺序:
妈妈认真做饭
小明认真读书
小明快过来,开饭啦
好嘞,吃饭咯
饭吃饱啦
唉,又要洗碗
碗洗完啦
做完家务了,可以玩了
*/
其实可以看出Promise.then()可以解决的回调地狱(callback hell),但是无法捕获异常,还需要调用回调函数来解决。
五、生成器Generators/yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
function *
会定义一个生成器函数,并返回一个Generator(生成器)对象,其内部可以通过yield
暂停代码,通过调用next
恢复执行。
简单看一下例子:
function * gen() {
yield console.log('hello');
yield console.log('world');
return console.log('ending');
}
var hw = gen();
在控制台输入hw.next():
hw.next();
index.html:156 hello
hw.next();
index.html:157 world
hw.next();
index.html:158 ending
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
必须调用遍历器对象的next()
方法,使得指针移向下一个状态。每次调用next方法,内部指针就从上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return语句)为止。
generator很方便处理异步(一般要配合tj/co库来用),这里举例说一下co
,
co
是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
安装co
库只需:npm install co
也可以自己去github找一下源码,了解一下
index.js
var co = require('co')
var fs = require('fs')
// wrap the function to thunk
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, function(err, date) {
if (err) reject(err)
resolve(data)
})
})
}
// generator 函数
function *gen() {
var file1 = yield readFile('./file/1.txt') // 1.txt内容为:content in 1.txt
var file2 = yield readFile('./file/2.txt') // 2.txt内容为:content in 2.txt
console.log(file1)
console.log(file2)
return 'done'
}
// co
co(gen).then(function(err, result) {
console.log(result)
})
// content in 1.txt
// content in 2.txt
// done
co 函数库可以让你不用编写 generator 函数的执行器,generator 函数只要放在 co 函数里,就会自动执行。 再来看一个例子
co(function *(){
try {
var res = yield get('http://baidu.com');
console.log(res);
} catch(e) {
console.log(e.code)
}
})
co 最大的好处在于通过它可以把异步的流程以同步的方式书写出来,并且可以使用 try/catch。
六、async/await
使用async/await,可以轻松地达成之前使用生成器和co函数所做到的工作; 一句话,async 函数就是 Generator 函数的语法糖。
然后用async/await实现上边(两个文件)的例子就可以这么写:
var asyncReadFile = async function (){
var f1 = await readFile('./file/1.txt');
var f2 = await readFile('./file/2.txt');
console.log(f1.toString());
console.log(f2.toString());
};
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await。
1.async函数的特点:
1.执行 async 函数,返回的都是Promise 对象
async function test1(){
return 123;
}
async function test2(){
return Promise.resolve(2);
}
const result1 = test1();
const result2 = test2();
console.log('result1',result1); //promise
console.log('result2',result2) //promise
自己可以 打印验证一下。
2.Promise.then 成功的情况对应 await
async function test3(){
const p3 = Promise.resolve(3);
p3.then(data => {
console.log('data',data);
})
//await 后边跟一个promise对象
const data =await p3;
console.log('data',data); //data3
}
test3()
async function test4(){ //await跟一个普通的数
const data4 = await 4; //await Promise.resolve(4)
console.log('data4',data4); //data4.4
}
test4();
async function test5(){
const data5 = await test1(); // await跟一个异步的函数
console.log('data5',data5); //data5.123
}
test5()
- Promise.catch 异常的情况 对应 try...catch
async function test6(){
const p6 = Promise.reject(6);
// const data6 = await p6;
// console.log('data6',data6) //报错:Uncaught (in promise) 6
try{
const data6 = await p6;
console.log('data6',data6);
}catch(k){
console.error('k',k); //捕获异常 k 6
}
}
test6()
总结了这么多,如果还是不太理解,我推荐看一下这些 实战题(ES6Promise实战练习题)加速帮助消化。
此文章为个人学习笔记分享,技术有限,欢迎大家一起讨论学习。
参考文章:
1.JavaScript异步机制详解
2.JS 异步编程六种方案
3.Javascript异步编程的4种方法
4.JS 基础之异步(五):Generator
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!