1 同步和异步
1.1 js同一时刻只能做一件事
首先,我们需要理解js是个单线程语言,同一时刻只能做一件事。
我们可以通过js执行
和DOM渲染
共用一个线程来理解这个原理:
- js修改dom的时候,浏览器不会对dom进行渲染,即dom渲染被阻塞
而上面这种被阻塞的行为,我们称之为具有同步性
1.2 同步与异步的概念
同步
- 英文:
Synchronization
,通常缩写为Sync
- 定义:同步行为对应内存中顺序执行的处理器指令,如果这句话不理解,你就理解为顺序执行js代码(别以为这是废话,和下面的异步定义好好对比一下)
其实就是代码要等待到结果,才能继续进行【你可以理解为同步阻塞了代码继续执行】
比如下面的例子,递归函数阻塞了最后一句的执行,因此打开控制台会发现2不是立刻输出的
function wait() {
let start = new Date();
while (new Date() - start < 4000) { // 阻塞4秒
}
console.log(2);
}
// 核心逻辑
console.log(1)
wait();
console.log(3); // 这一句的执行被阻塞了4秒
// 输入结果:1 2 3
异步
- 英文:
Asynchronization
,通常缩写为Async
- 定义:类似于
操作系统中的中断
,即当前进程外部的实体可以触发
代码执行 其实就是代码不用等待到结果,就能继续进行【你可以理解为异步不阻塞代码继续执行】
啥意思咧,举个例子
function wait() {
setTimeout(() => console.log(2), 4000)
}
// 核心逻辑
console.log(1);
wait(); // 4秒钟后,得到结果
console.log(3); // 如果没有异步的话,这句得等4秒钟才能执行,所以,感谢异步
// 输入结果:1 3 2
2 异步的应用
2.1 常见的异步场景
1.网络请求
// Ajax
console.log(1);
$.get(url1, data1 => {
console.log(data1);
console.log(2);
})
console.log(3);
// 1 3 2
2.定时任务,如setTimeout和setTimeInterval
console.log(1);
setTimeout(() => console.log(2), 1000);
console.log(3);
console.log(1);
setInterval(() => console.log(2), 1000);
console.log(3);
3.事件监听
// 加载图片
console.log(1);
let img = document.createElement('img');
// onload是个回调,图片一加载才会触发
img.onload = () => console.log(2);
// src赋值之后图片就会开始加载
img.src = '/xxx.png';
console.log(3);
// 1 3 2
2.2 常见的异步问题
2.1 图片加载问题
// 前提条件:用户的浏览器第一次请求这个图片,也就是用户的浏览器未缓存
document.getElementsByTagNames('img')[0].width // 宽度为 0
为什么width会为0呢?
因为js运行的时候,img并没有下载完毕
解决方案
let imgNode = document.getElementsByTagName('img')[0]
imgNode.addEventListener('onload',function () {
// 回调
console.log(this.width)
})
2.2 面试题常考的异步问题
// 假设有5个li
let liList = document.querySelectorAll('li')
for (var i = 0; i < liList.length; i++) {
liList[i].onclick = function () {
console.log(i) // 5 5 5 5 5
}
}
为什么呢?
因为onclick事件是异步处理的,用户触发onclick事件时,循环早已结束
又因为var,i被提升变成了全局变量,此时的i是5
因此有上述情况发生
解决方案一【立即执行函数创建独立作用域(不推荐)】
// 假设有5个li
let liList = document.querySelectorAll('li')
for (var i = 0; i < liList.length; i++) {
!(function (j) {
liList[j].onclick = function () {
console.log(j) // 1 2 3 4 5
}
})(i)
}
解决方案二【使用let】
let让i变成了for循环的{}里的局部变量
// 假设有5个li
let liList = document.querySelectorAll('li')
for (let i = 0; i < liList.length; i++) {
liList[i].onclick = function () {
console.log(i) // 1 2 3 4 5
}
}
3 拿到异步结果的方式 —— 回调
3.1 什么是回调?
回调:把函数作为其它函数的参数进行传递
function printInfo(info) {
console.log(info);
}
function getInfo(fn) {
fn('大汪');
}
// printInfo作为getInfo的参数进行了传递
getInfo(printInfo); // '大汪'
- printInfo就是
回调函数
printInfo
这个函数存在的意义就是能够通过getInfo被调用
- 在getInfo函数中调用printInfo行为就是
触发回调函数
3.2 常见的回调形式
Node.js 的 error-first 形式
先判断error是否存在,存在则说明出现了错误,不存在则成功
fs.readFile('./1.txt', (error, content) => {
if (error) {
// 失败
} else {
// 成功
}
})
jQuery 的 success / error 形式
$.ajax({
url: '/xxx',
success: () => {
},
error: () => {
}
})
jQuery 的 done / fail / always 形式
$.ajax({
url: '/xxx',
}).done(() => {
}).fail(() => {
}).always(() => {
})
还有一种就是Prosmise的then形式,在第4节将详细阐述
3.3 嵌套异步回调——回调地狱(callback hell)
在js中,针对异步的问题,我们可以通过回调解决但是,更大的困难就是如何解决串联异步的问题,在promise之前通用的解决方案就是嵌套异步回调,又称之为回调地狱
回调地狱
:回调套回调套回调套回调套回调套回调套回调套回调套回调
下面的例子生动地展示了异步的结果通过回调的方式来处理
只不过一直嵌套回调
// 获取data1
$.get(url1, data1 => {
console.log(data1);
// data1获取后再获取data2
$.get(url2, data2 => {
console.log(data2);
// data2获取后再获取data3
$.get(url3, data3 => {
console.log(data3);
// ...可以无限重复
})
})
})
4 Promise
4.1 先看一个例子
你需要知道
- axios是个库
- axios()返回一个Promise实例
- 你可以把axios()理解为$.ajax(),它们功能相近,只不过axios遵循promise规范
axios({
url: './xxx.json'
}).then((resolve) => {
console.log(resolve)
return '我是第二个then'
}, (reject) => {
console.log(reject)
}).then((resolve_2) => {
console.log(resolve_2) // '我是第二个then'
}, (reject_2) => {
console.log(reject_2)
})
为了防止你对这个链式调用
看得眼花缭乱,我把这个给简化一下
axios({
url: './xxx.json'
}).then(成功回调1, 失败回调1)
.then(成功回调2, 失败回调2)
相信你对上面的代码一定还是看得一头雾水,因为你还不会promise
下面我们先来了解Promise的一些基本概念
4.2 Promise的基本概念
4.2.1 Promise的作用
Promise是专门用来解决异步编程问题的,避免了层层嵌套的回调函数[即Callback Hell]
下面是一个用传统方法Callback Hell来写的异步代码
可以非常明显地看出来,Callback Hell的方式让代码的可读性变得非常差
let src = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
function loadImg(src, callback, fail) {
let img = new Image()
// 成功回调
img.onload = function () {
callback(img)
}
// 失败回调
img.onerror = fail
img.src = src
}
// 下面代码其实就是loadImg(src1, 成功回调1, 失败回调1);
// 但在成功回调1执行时,再一次loadImg(src2, 成功回调2, 失败回调2);
loadImg(src1, function (img) {
console.log(img.width)
loadImg(src2, function (img) {
console.log(img.width)
}, function () {
console.log('error2')
})
}, function () {
console.log('error1')
})
4.2.2 new Promise()
new Promise(myExecutorFunc)
返回一个Promise实例,myExecutorFunc
要求是一个函数,其形式要求为:
let myExecutorFunc = (resolve, reject) => {};
具体可以看MDN文档
我们这里主要了解
new Promise()
如何获取异步操作的结果
一般一个异步操作,比如axios({url:'./xxx.json'})
我们肯定是希望获取一个json对象,再把这个对象传递出去
1.许多人常犯的错误就是以为return就可以拿到结果,然而这只是大错特错:
let p = new Promise(() => {
let data = 3; // 这是我们想要获得的结果
return data; // 这个return一点用都没有
});
2.new Promise()
必须通过向myExecutorFunc
的两个回调函数resolve
和reject
传参才可以
let p = new Promise((resolve, reject) => {
let data = 3; // 这是我们想要获得的结果
resolve(data);
// 或者
reject(data);
});
在控制台输入p:
当然,上述只是让大家看看promise实例p的情况,要想真正处理这个结果,需要通过then/catch等实例方法
,这些之后会聊
let p = new Promise((resolve, reject) => {
let data = 3; // 这是我们想要获得的结果
resolve(data);
// 或者
reject(data);
});
p.then(data => {
console.log(data); // 3
}, undefined);
4.2.3 Promise实例的三个状态
pending
是初始态fulfilled(又称resolved)
异步操作成功,由pending转变而来rejected
异步操作失败,由pending转变而来
Promise实例代表一个异步操作的结果,且只有异步操作的结果,可以决定当前是哪一种状态
任何其他操作都无法改变这个状态,peding可转化为fulfilled与rejected,但fulfilled与rejected不可相互转化
我们可以通过控制台看到这三个状态,注意:
Promise.resolve()
返回一个fulfilled态
的Promise实例Promise.reject()
返回一个rejected态
的Promise实例【下图可以看到控制台报错,因为rejected本身就是一种异步错误类型】
那知道这三个状态又有什么用咧?
OK,我们看下面的代码
axios({
url: './xxx.json'
}).then(成功回调, 失败回调)
对axios({url: '.'})
而言,其返回一个Promise对象,即一个异步操作的结果
异步操作成功代表了pending
-> fulfilled
-> then里的第一个参数
【成功回调】
异步操作失败代表了pending
-> rejected
-> then里的第二个参数
【失败回调】
4.3 then和catch的链式调用
因为Promise.prototype.then
和Promise.prototype.catch
方法返回promise对象
所以它们可以被 链式调用
OK,下面让我们仔细看一下回调触发机制究竟怎样的过程
axios({
url: './xxx.json'
}).then(成功回调1, 失败回调1)
.then(成功回调2, 失败回调2)
是不是看的有点晕?没关系,下面我来根据axios的例子详细解释一下
你一定有一些疑问:
1.为什么第一个then都调用失败回调1
了,第二个then也有可能调用成功回调2
呢
答:因为第二个then调用进入哪个回调函数,完全是看第一个then返回的Promise是什么状态,换言之 —— 看异步操作成功与否
即使第一个then调用了成功回调1
,第二个then仍有可能进入失败回调2
,举一个栗子:
axios({
url: './xxx.json'
})
.then((resolve_1) => {
return Promise.reject();
// 在这种情况下,then返回的Promise的状态是rejected
}, (reject_1) => {})
.then((resolve_2) => {
console.log(1)
}, (reject_2) => {
// 所以第二个then只会调用它的失败回调2,即在这里执行
console.log(2)
})
2.你咋不提catch咧?
因为catch就是then的一个语法糖呀
catch等价于then只有第二个参数【失败回调】的形式
上面的例子用catch,可以这么写
axios({
url: './xxx.json'
})
.then((resolve_1) => {
return Promise.reject();
// 在这种情况下,then返回的Promise的状态是rejected
}, (reject_1) => {})
.catch((reject_2) => {
// 执行catch的回调
console.log(2)
})
// 其等价于
axios({
url: './xxx.json'
})
.then((resolve_1) => {
return Promise.reject();
// 在这种情况下,then返回的Promise的状态是rejected
}, (reject_1) => {})
.then(undefined, (reject_2) => {
// 所以第二个then只会调用它的失败回调2,即在这里执行
console.log(2)
})
4.4 自己使用Promise
上面,我们借用axios
学习了promise的基本使用,但只是在axios封装promise的基础上学习的下面我来学习怎么自己封装一个promise
4.4.1 没有异步逻辑的Promise
第一步
// 声明一个函数,让这个函数返回一个Promise实例
let setPromise = function () {
return new Promise();
}
第二步
let isSuccess = true; // 控制执行resolve还是reject
// new Promise()接受一个函数
// 规定这个函数必须要有两个参数,这两个参数必须是函数,即【成功回调,失败回调】
let setPromise = function () {
// resolve是成功回调,reject是失败回调
let fn = (resolve, reject) => {
if (isSuccess) resolve('成功'); // 执行resolve,则promise状态会由pending->fulfilled
else reject('失败'); // 执行reject,则promise状态会由pending->reject
}
return new Promise(fn)
}
大家可以猜一下,此时下面的promiseInstance
的状态时什么,如果把isSuccess
改为false
,状态又是什么?自己试一试
let promiseInstance = setPromise();
console.log(promiseInstance); // ?
第三步:研究一下参数
仔细看我写的注释
let isSuccess = true; // 控制执行resolve还是reject
let setPromise = function () {
let fn = (resolve, reject) => {
// 这里传递的两个字符串'成功'和'失败'会成为promise实例第一个then里的参数
if (isSuccess) resolve('成功');
else reject('失败');
}
return new Promise(fn)
}
let promiseInstance = setPromise();
promiseInstance
.then(param => {
console.log(param) // '成功'
return '成功2' // 返回值会成为下一个then里的param
}, param => {
console.log(param);
return '失败2'
})
.then(param => {
console.log(param) // '成功2'
}, param => {
console.log(param)
})
同样的,如果把isSuccess
改为false
,输出又是什么?自己试一试吧
4.4.2 带异步逻辑的promise
下面用Promise改写4.2.1小节的例子
let src = 'https://avatar-static.segmentfault.com/198/713/1987139774-59c8bdbc3b36b_huge256' // 正确路径
// let src = 'xxx'; // 错误路径
let loadImg = new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => {
resolve(img);
}
img.onerror = () => {
reject('图片加载失败');
}
img.src = src;
})
loadImg.then(param => {
console.log('图片加载成功');
let body = document.querySelector('body');
body.appendChild(param);
}, param => {
console.log(param);
})
4.5 处理多个Promise
4.5.1 Promise.all()
语法:看mdn文档
作用:
对迭代器的元素用
Promise.resolve()
进行包装,再对包装的结果进行合成,但合成结果遵循以下原则:
- 如果有至少一个Promise的结果是
pending
,合成的结果就是pending- 如果有至少一个Promise的结果是
reject
,合成的结果就是reject【该条规则优先级高于上一条】
let p1 = new Promise(() => {});
// pending + reject === reject
Promise.all([p1, Promise.reject('error')])
.then(param => {
console.log(param)
}, param => {
console.log(param) // error
})
Promise.all的解决的值的传递:
- 如果
结果是rejected
,只会把第一个出现rejected状态的Promise实例的结果传递出去- 如果
结果是fulfilled
,会把所有fulfilled状态的Promise实例的结果封装成数组传递出去
// 规则一
Promise.all([Promise.reject('error1'), Promise.reject('error2')])
.then(param => {
console.log(param)
}, param => {
console.log(param) // error1
})
// 规则二
Promise.all([Promise.resolve('success1'), Promise.resolve('success2')])
.then(param => {
console.log(param) // [ 'success1', 'success2' ]
}, param => {
console.log(param)
})
4.5.2 Promise.race()
语法:与Promise.all()一样作用:
对迭代器的元素用
Promise.resolve()
进行包装,返回第一个fulfilled
或rejected
的包装的结果
let p1 = new Promise((resolve) => {
setTimeout(resolve, 1000, 'success');
})
let p2 = new Promise((resolve, reject) => {
setTimeout(reject, 500, 'error');
})
// p2第一个结束,因此Promise.race()的结果是rejected,会把拒绝理由传递给then
Promise.race([p1, p2])
.then(param => {
console.log(param);
}, param => {
console.log(param); // error
})
4.6 用生活化语言理解promise
Promise的中文翻译是承诺(高程4th将其翻译为期约)
,then的中文翻译是然后
所以,你可以想象你去买橘子,结果店里没有进货,店员对你Promise
,只要他店里到货(fulfilled)或者不再进货(rejected),then
他就会通知你
5 async / await 语句
5.1 async
5.1.1 async关键字声明一个异步函数
先看一下async
的用法:在函数声明前加上async
就会让这个函数成为一个异步函数
// 下面的fn都被async声明为了异步函数
async function fn() {};
let fn = async () => {};
5.1.2 异步函数的特性
1.异步函数 始终返回一个Promise实例
,其 本质是 将异步函数的返回值用Promise.resolve()
包装
let fn = async () => {
// 没写return,相当于return undefined
}
console.log(fn()); // Promise {: undefined}
上面的写法与下面的写法产生的效果几乎相同
let fn = () => {
return Promise.resolve(undefined);
}
console.log(fn()); // Promise {: undefined}
2.异步函数 本质上是为await提供一个运行环境,如果不包含await关键字,其执行上与普通函数一样
什么意思呢?
async关键字其实只是告诉浏览器,这个函数是一个异步函数,里面有异步的代码,方便浏览器对其进行解析。
但你如果不写异步代码(就像上面的例子一样),浏览器也拿你没办法,只是解析完后才发现这实际上只是个普通函数,浏览器为异步函数做的一些准备没派上用场,做了无用功。
从这点来看,async关键字
其实就只是一个标识符。
5.2 await
5.2.1 await的用法
作用:await
操作符用于等待一个Promise实例
的结果,本质上是让Promise.resolve()对await操作符之后的表达式进行包装限制:只能在异步函数
async function
中使用
先来看一个例子
let result;
async function fn1() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('10秒后');
resolve(1);
}, 10000);
})
result = await p;
console.log(2);
}
fn1();
在运行代码的过程中,我们会发现一些问题:
- 如果你在控制台不断输入result,控制台会不断地提示:
undefined
,10s后再输入才会有结果 console.log(2);
这句也是在10s后才执行的
为啥会这样呢?
因为await在等待p这个Promise实例有结果后,才会执行result =
这一句和下面的语句
也就是说await阻塞
了之后代码的执行
5.2.2 await让异步代码变得像同步代码
这个标题有些绕,我们先对5.2.1小节
的例子做个修改,去掉await,看看结果
let result;
async function fn1() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('10秒后');
resolve(1);
}, 10000);
})
result = p; // 删去await
console.log('result:', result); // 打印一下result
console.log(2);
}
fn1();
我们可以很明显的看到,console.log('result:', result);
和console.log(2);
立刻被执行了
但是!卧槽?result的结果怎么是个pending状态的Promise的实例呢?
其实很简单,因为result是一个Promise实例,是一个异步代码
,而console.log('result:', result);是一个同步代码
,因此只能获取到result产生最终结果之前的状态,也就是pending。
再结合await
,我们通过result = await p;
可以直接拿到p最后的结果,实际上是让p最终结果产生之后,即异步代码结束后再继续执行下面的代码
而这不就是同步的方式吗?
也就是说,await通过阻塞同步代码的执行,改变了整个代码的执行顺序,它可以让你用写同步代码的方式去写异步代码
5.3 await的使用细节
5.3.1 await一个fulfilled的Promise实例
没啥好说的,可以直接拿到结果
let p = new Promise((resolve) => {
setTimeout(resolve, 1000, 1);
})
async function fn() {
let result = await p;
console.log('result:', result); // result: 1
}
fn();
5.3.2 await一个pending的Promise实例
注意,如果一个Promise实例永远是pending态,那么 await就会永远等待他进行转变。即:await后面的代码永远不会执行
let p = new Promise((resolve) => {});
async function fn() {
let result = /* 前面和下面的代码将永远都不会执行 */ await p;
console.log('result:', result);
}
fn();
5.3.3 await一个rejected的Promise实例
此时我们会发现,浏览器会直接报错,而且 await后面的代码又永远不会执行了
let p = new Promise((resolve, reject) => {
reject('error');
})
async function fn() {
let result = /* 前面和下面的代码将永远都不会执行 */ await p;
console.log('result:', result);
}
fn();
根据文档,这个时候我们必须要使用
try...catch...
进行错误处理
let p = new Promise((resolve, reject) => {
reject('error');
})
async function fn() {
try {
let result = /* 前面和下面的代码将永远都不会执行 */ await p;
console.log('result:', result);
} catch (e) {
// 下面这句会执行
console.log(e);
}
}
fn();
等等!try...catch...
不是只能抓同步代码的错吗,比如下面这样浏览器还是会报错
try {
Promise.reject('error');
} catch (e) {
console.log(e);
}
那为什么现在用了await又能抓异步代码的错误了呢?
其实很简单,之前已经解释过,await
让异步代码变得像同步代码一样,因此try...catch...
并非是抓了异步代码的错,仍然是在抓取同步代码的错误
5.4 await练习
我们在4.5小节
举了多个关于Promise.all()
和Promise.race()
,为了演示这两个方法的最终结果,我们使用了then方法
现在,我们完全可以用await
来改写
5.4.1 演示Promise.all()
let p1 = new Promise(() => {});
// pending + reject === reject
Promise.all([p1, Promise.reject('error')])
.then(param => {
console.log(param)
}, param => {
console.log(param) // error
})
可改写为
async function fn() {
let p1 = new Promise(() => {});
// pending + reject === reject
try {
let result = await Promise.all([p1, Promise.reject('error')])
console.log(result);
} catch (e) {
console.log(e); // error
}
}
fn();
其它例子兄弟们可以自己练习,我就不演示了
5.4.2 演示Promise.race()
let p1 = new Promise((resolve) => {
setTimeout(resolve, 1000, 'success');
})
let p2 = new Promise((resolve, reject) => {
setTimeout(reject, 500, 'error');
})
// p2第一个结束,因此Promise.race()的结果是rejected,会把拒绝理由传递给then
Promise.race([p1, p2])
.then(param => {
console.log(param);
}, param => {
console.log(param); // error
})
可改写为
async function fn() {
let p1 = new Promise((resolve) => {
setTimeout(resolve, 1000, 'success');
})
let p2 = new Promise((resolve, reject) => {
setTimeout(reject, 500, 'error');
})
// p2第一个结束,因此Promise.race()的结果是rejected
try {
let result = await Promise.race([p1, p2])
console.log(result);
} catch (e) {
console.log(e); // error
}
}
fn();