JavaScript 只有一个主线程,是单线程模式。但是为了解决长时间排队的问题,CPU可以不管IO操作,挂起处于等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。
程序中的任务可以分成两个:同步任务和异步任务
同步任务:没有被引擎挂起、在主线程上排队执行的任务。
异步任务:是那些被引擎放在一边,不进入主线程、而进入任务队列里的任务。
只有引擎认为某个异步任务可以执行了(例如Ajax操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入到主线程。
异步任务的模式
1、回调函数 => 异步操作最基本的方法
//f2 必须等到 f1 执行完成才能执行
function f1(){
//...
}
function f2(){
//...
}
f1()
f2()
此时,如果 f1 是异步操作, f2 会立即执行,不会等到 f1 结束再执行
使用回调函数:将 f2 写成 f1 的回调函数
function f1(callback){
//...
callback()
}
function f2(){
//...
}
f1(f2)
回调函数的优点是简单、容易理解和实现。缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪,并且每个任务只能指定一个回调函数。
2、事件监听 => 采用事件驱动模式
f1.on('done',f2) //jQuery的写法
当 f1 发生 done 事件,就执行 f2 对 f1 进行改写
function f1(){
setTimeout(function(){
//...
f1.trigger('done') //执行完成后,立刻触发 done 事件
},1000);
}
事件监听的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而去可以去耦合,有利于实现模块胡啊。缺点是整个程序都是变成事件驱动型,运行流程会变得很不清晰。
3、发布/订阅 => 观察者模式
事件可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就像信号中心“发布”一个信号,其他任务也可以想信号中心“订阅”这个信号,从而知道什么时候自己可以开始执行。
jQuery.subscribe('done',f2);// Ben Alman 的 Tiny Pub/Sub jQuery 的一个插件
f2 向信号中心 jQuery 订阅 done 信号,对 f1 进行改写
function f1(){
setTimeout(function(){
//...
jQuery.publish('done')// f1 执行完成后,向信号中心 jQuery 发布 done 信号
},1000);
}
Promise
Promise 是异步编程的一种解决方案,比传统的解决方案--回调函数和事件--更合理和更强大。
Promise => 一个保存着某个未来才会结束的事件(通常是一个异步操作)结果的容器。
Promise 对象有两个特点:
1、对象的状态不受外界影响 三种状态: pending(进行中)、 fulfilled(已成功)、 rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。 状态变化只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。
const promise = new Promise(function(resolve,reject){
if(/*异步操作成功*/){
resolve(value)//将Promise对象的状态由pending变为fulfilled
}else{
reject(error)//将Promise对象的状态由pending变为rejected
}
})
Promise 实例生成以后,可以调用then方法分别指定 fulfilled 状态和 rejected 状态的回调函数。
promise.then(function(value){
//success
},function(error){
//failure
})
then 方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。
Promise 新建之后就会立即执行
let promise = new Promise(function(resolve,reject){
console.log('Premise')
resolve();
});
promise.then(function(){
console.log('resolved.')
});
conlose.log('Hi!')
//Promise Promise立即执行 打印
//Hi!
//resolved then 方法指定的回调函数,将在当前脚本所有的同步任务执行完才会执行,所以resolved 最后输出
如果调用resolve函数和reject函数时 带有参数,那么他们的参数会被传递给回调函数。reject 函数的参数通常是Error对象的实例,表示抛出的错;resolve 函数的参数除了正常的值以外,还可能是另一个 Promise 实例
const p1 = new Promise(function(resolve,reject){
//...
})
const p2 = new Promise(function(resolve,reject){
//...
resolve(p1); // 此时 p1 的状态就会传递给p2,p1 的状态决定了 p2 的状态。如果 p1 的状态是 pending ,那么 p2 的回调函数就会等待 p1 的状态改变;如果 p1 的状态已经是 fulfilled 或者 rejected ,那么 p2 的回调函数将会立刻执行
})
const p1 = new Promise(function(resolve,reject){
setTimeout(()=> reject(new Error('fail')),3000)
})
const p2 = new Promise(function(resolve,reject){
setTimeout(()=> resolve(p1),1000)
})
p2.then(result =>console.log(result)).catch(error=>console.log(error))
//Error:fail
在上面代码中,p1是Promise,3秒之后变为 rejected。 p2 的状态在1秒之后改变,resolve 方法返回的是p1。 由于 p2 返回的是另一个Promise,导致 p2 自己的状态无效了,由 p1 的状态决定 p2 的状态。所以,后面的 then 语句都变成针对后者(p1)。又过了 2 秒,p1 变为 rejected ,导致触发 catch 方法指定的回调函数。(当p2 内部是一个异步任务的时候,p2 的then方法会比resolve方法先执行)
Promise 指定多个回调 => 当Promise对象状态改变为对应状态时都会调用,如果Promise对象状态没有改变时就不会调用
let p = new Promise((resolve,reject)=>{
resolve('OK')
})
//指定回调 - 1
p.then(value=>{
console.log(value)
})
//指定回调 - 2
p.then(value=>{
alert(value)
})
//OK
//弹框 OK
改变 Promise 对象状态和指定回调函数谁先执行?
1、都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调
2、如何先改变状态再指定回调?
1、再执行器中直接调用 resolve/reject
2、延迟更长时间才调用then
3、什么时候才能得到数据?
1、如果先制定的回调,那当状态发生改变时,回调函数就会调用,得到数据
2、如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据。
promise.then() 返回新的 promise 的结果状态由什么决定?
1、简单表述:由then() 指定的回调函数执行的结果决定
2、详细表达:
-
如果抛出异常,新 promise 变为 rejected,reason 为抛出的异常
-
如果返回的是非 promise 的任意值,新 promise 变为 fulfilled ,value为返回的值
-
如果返回的是另一个新promise,此 promise的结果就会成为新 promise 的结果
let p = new Promise((resolve,reject)=>{ resolve('') }); //执行 then 方法 let result = p.then(value=>{ //console.log(value) //1、抛出错误 //throw "出了问题" //2、返回结果是非 Promise 类型的对象 // return 521; //3、返回结果是 Promise 对象 return new Promise((resolve,reject){ //resolve('success') //success //reject('error') //error }) },reason=>{ console.warn(reason) }) console.log(result)
promise 如何串连多个操作任务?
1、promise 的then() 返回一个新的promise ,可以开成 then() 的链式调用
2、通过 then() 的链式调用串连多个同步/异步任务
let p = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('ok')
},1000)
})
p.then(value=>{
return new Promise((resolve,reject)=>{
resolve('success')
})
}).then(value=>{
console.log(value) //success
}).then(value=>{
console.log(value) //undifined
})
promise 异常穿透 1、当使用promise 的then 链式调用时,可以在最后指定失败的回调 2、前面任何操作出了异常,都会传到最后失败的回调中处理
let p = new Promise((resolve,reject) =>{
setTimeout(()=>{
//resolve('ok')
reject('Error)
},1000)
})
p.then(value=>{
console.log(111);
//中断promise链的唯一方法
return new Promise(()=>{})
}).then(value=>{
console.log(222)
}).then(value=>{
console.log(333)
}).catch(reason=>{
console.warn(reason)
})
Promise.prototype.then()
为Promise 实例添加状态改变时的回调函数
Promise.prototype.catch()
指定发生错误时的回调函数
Promise.prototype.finally()
指定不管 Promise 对象最后状态如何,都会执行的操作
Promise.all()
将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.all([p1,p2,p3])
p的状态由 p1、p2、p3 决定
1、只有 p1、p2、p3 的状态都变成 fulfilled, p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
2、只要 p1、p2、p3 之中有一个被 rejected ,p的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给p的回调函数
Promise.race()
const p = Promise.race([p1,p2,p3])
只要 p1、p2、p3 之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.any()
Promise.any([
fetch('https://v8.dev/').then(()=>'home'),
fetch('https://v8.dev/blog').then(()=>'blog'),
fetch('https://v8.dev/docs').then(()=>'docs'),
]).then((first) =>{ //只要有一个 fetch() 请求成功
console.log(first);
}).catch((error) =>{ //所有三个 fetch() 全部请求失败
console.log(error);
})
async函数
函数的返回值是一个promise对象
promise 对象的结果由async 函数执行的返回值决定
async function main(){
//1、如果返回值是一个非Promise类型的数据
//return 521;
//打印的promise 状态 fulfilled 值 521
//2、如果返回的是一个Promise 对象
//return new Promise ((resolve,reject) => {
// //resolve('ok') //状态fulfilled 值 ok
// //reject('error') //状态rejected 值 error
//})
//打印的promise的状态和结果都根据返回的promise 的状态,结果一致
//3、抛出异常
throw "oh NO";
//打印的promise 状态rejected 值 抛出的异常值 oh NO
}
let result = main()
console.log(result)
await 表达式
await 右侧的表达式一般为 promise对象,但也可以是其他的值
如果表达式时promise 对象,await 返回的是 promise 成功的值
如果表达式时其他值,直接将此值作为 await 的返回值
注意:
await 必须写在 async 函数中, 但是 async 函数中可以没有await
如果 await 的promise 失败了,就会抛出异常,需要通过 try ... catch 捕获处理
async function main(){
let p = new Promise((resolve,reject)=>{
//resolve('ok')
reject('error')
})
//1、右侧为promise的情况
//let res = await p;
//console.log(res) //await 返回 promise对象成功的那个值 ok
//2、右侧为其他类型的数据
//let res2 = await 20;
//console.log(res) //await 右侧的值是什么await 返回的值就是什么 20
//3、如果promise 是失败的状态
try{
let res3 = await p;
}catch(e){
console.log(e) //error
}
}
main()
async 和 await 结合实践
/**
* resource 读取 1.html 2.html 3.html 文件内容 并且拼接输出
*/
const fs = require('fs')
const ytil = require('util')
const mineReadFile = util.promisify(fs.readFile);
//普通回调函数方法
fs.readFile('./resource/1.html',(err,data1 =>{
if(err) throw err;
fs.readFile('./resource/2.html',(err,data2)=>{
if(err) throw err
fs.readFile('./resource/3.html',(err,data3)=>{
if(err) throw err
console.log(data1+data2+data3)
})
})
})
//async 和 await
async function main(){
try{
//读取第一个文件的内容
let data1 = await mineReadFile('./resource/1.html')
//读取第二个文件的内容
let data2 = await mineReadFile('./resource/2.html')
//读取第三个文件的内容
let data3 = await mineReadFile('./resource/3.html')
console.log(data1+data2+data3)
}catch(e){
console.log(e)
}
}
main()