千夜Promise
0 前言:回调地狱
考虑一个场景:进入网站自动登录,并显示个人页面
简化一下:发送登陆请求,成功后发送获取个人信息请求,成功后展示在页面上
再简化一下:
假设使用定时器模拟Ajax请求,1-2秒后输出0,输出0后1-2秒后输出1,输出1后1-2秒后输出2
function waitTime(){
return Math.random()*1000 + 1000;
}
// 写法0
// 错误的写法,相当于几个“线程并发”,没有执行先后顺序
setTimeout(()=>{console.log(0);}, waitTime());
setTimeout(()=>{console.log(1);}, waitTime());
setTimeout(()=>{console.log(2);}, waitTime());
// 写法1
setTimeout(()=>{
console.log(0);
setTimeout(()=>{
console.log(1);
setTimeout(()=>{
console.log(2);
}, waitTime())
}, waitTime())
}, waitTime());
// 写法2
// 错误的函数封装:因为先从内层func开始执行,所以内层定时器已经开启
// 可以看出普通函数不能承接异步操作
/*
function func(value, callback=()=>{}){
setTimeout(()=>{
console.log(value);
callback();
}, waitTime());
}
func(0,
func(1,
func(2)
)
);
*/
// 写法3
function funcP(value){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve(value);
}, waitTime())
})
}
funcP(0).then(res=>{
console.log(res);
return funcP(1)
}).then(res=>{
console.log(res);
return funcP(2)
}).then(res=>{
console.log(res);
})
// 写法4
(async function funcA(){
console.log(await funcP(0));
console.log(await funcP(1));
console.log(await funcP(2));
})()
显然,写法3和写法4是较好的写法:
-
写法0是错误的写法,相当于几个“线程并发”,没有执行先后顺序
-
写法1正确,当外层定时器的回调执行时才开启内部定时器
-
写法2错误,因为先从内层函数执行,所以内层定时器已经先开启
- 说明普通函数不能承接异步操作
-
写法3使用链式调用优化了嵌套地狱
-
写法4更像是同步的写法,await是then成功的语法糖
1 预备知识(JS复习)
1.1 回调函数、同步回调与异步回调
回调函数
一般来说回调函数如 func(()=>{console.log("hello")});,回调函数有5个特点:
- 作为某个函数调用时传入的参数,或者设置为某个对象的属性或方法
- 一般为匿名函数,或传名函数
- 隐式调用
- 回调函数调用时,可能会被传入参数
- 一般没有返回值,或返回值不直接起作用
同步回调
回调函数不一定是异步函数,以下是同步回调函数:
- 数组遍历函数
- Promise的executor
同步回调相当于仅仅是作为参数被传入父函数调用,然后立即执行,所以是同步
// 立即执行
[1, 2, 3].forEach(item => {
console.log(item);
});
// 立即执行
new Promise(resolve=>{
console.log("hello");
})
异步回调
当然大部分回调函数是异步函数,如:
- 定时器函数,setTimeout、setInterval
- DOM事件回调
- Ajax请求
- Promise的then、catch
异步回调的执行时间不定,需要等待事件轮询
// 定时器
setTimeout(()=>{
console.log("hello")
}, 1000);
// DOM事件
let button = document.createElement("button");
button.innerHTML = "button";
button.addEventListener("click",(e)=>{
console.log(e);
})
document.body.append(button);
// ajax
(function ajax() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
myFunction(this);
}
};
xhttp.open("GET", "xxxxx", true);
xhttp.send();
})();
// promise
Promise.resolve().then((res)=>{
console.log(res);
})
1.2 错误(异常)
本节了解即可
-
类型
-
Error
-
ReferenceError:引用的变量不存在
console.log(a); // not defined
-
TypeError:数据类型不正确
let a; console.log(a.name); // cannot read propertylet a = 1; a(); // is not function
-
RangeError:数据范围不正确
(function func(){func()})(); // Maximum call stack size exceeded
-
SyntaxError:语法错误(一般ide直接报错)
-
-
处理
-
捕获:
try{ }catch( ){ }finally{ }- 没有捕获,则Uncaught Error之后的代码不会执行
-
抛出:
throw new Error( )- throw需要try catch,否则Uncaught Error
-
-
对象属性
- message
- stack
2 Promise
2.1 构造函数
- Promise是构造函数,即Promise是类
- new Promise( )中传入同步回调函数executor,该函数接收两个参数,两个参数均为executor的回调函数,需要在函数体内的合适时机执行
- new Promise( )中传入同步回调函数executor会立即执行
- 为了防止其立即执行,通常使用高阶函数封装(返回值是函数的高阶函数),这样可以控制其调用
// 立即执行
new Promise(()=>{
console.log("hello world");
});
// 返回值是Promise
function func(){
return new Promise((resolve)=>{
console.log("hello world");
resolve();
});
}
2.2 工厂方法
Promise提供了快速创建对象的工厂方法,替代new Promise
// 以下值等价
Promise.resolve(0);
new Promise((resolve)=>{
resolve(0);
});
Promise.reject(0);
new Promise((reject)=>{
reject(0);
});
2.3 回调函数参数resolve和reject
-
Promise承诺仅有三种状态,仅有两种状态转移,状态转移之后不变:
- pending到resolved(fullfilled)
- pending到rejected
-
状态转移之后不代表之后代码不执行
-
resolve和reject没有返回值
// 状态为[resolved 1]
// 0 undefined 2 undefined 4
new Promise((resolve, reject)=>{
console.log(0);
console.log(resolve(1));
console.log(2);
console.log(reject(3));
console.log(4);
});
2.4 Promise对象的then和catch方法
参数
- then里的第一个参数是成功的回调函数,其参数为res(value),也可以接收第二个参数失败的回调函数,其参数为err(reason)
- 传入的参数res和err,由resolve和reject传入的参数决定
- 应使用catch捕获then中可能出现的错误,而不是then的第二个回调函数参数
// resolve里的值传入到res
Promise.resolve(0).then(res => {
console.log(res); // 0
})
// 以下等价
Promise.reject(new Error("err")).then(null, (err)=>{
console.log(err);
});
Promise.reject(new Error("err")).catch((err)=>{
console.log(err);
});
// 以下不等价
Promise.resolve(0).then((res)=>{
console.log(res); // 0
throw new Error("err");
}, (err)=>{
console.log(err); // 错误没有被捕获
});
Promise.resolve(0).then((res)=>{
console.log(res); // 0
throw new Error("err");
}).catch((err)=>{
console.log(err); // 错误被捕获
});
返回值
- then和catch返回一个新的Promise对象
- 新的Promise对象一般为resolved,值为其返回值
// 注意以下输出:Promise[pending] 0
// 因为console.log()是同步
console.log(
Promise.resolve(0).then((res)=>{
console.log(res); // 0
})
);
// 以下输出:0 Promise[resolved, undefined]
let a = Promise.resolve(0).then((res)=>{
console.log(res); // 0
})
setTimeout(()=>{
console.log(a);
})
// 以下输出:0 Promise[resolved, 1]
let a = Promise.resolve().then((res)=>{
return 1;
})
setTimeout(()=>{
console.log(a);
})
// 以下等价
Promise.resolve().then((res)=>{
return 1;
});
Promise.resolve(1);
回调函数的返回值
- 当回调函数没有返回值时,视为return(return undefined),封装为Promise对象
- 当回调函数返回值为普通值时,封装为Promise对象,返回值作为resolve状态的值
- 当回调函数返回值为Promise对象时,替换默认的Promise对象
- 当回调函数中抛出异常时,返回Promise的reject状态
一般使用链式调用检测上一个回调的返回值
// undefined
Promise.resolve().then(res=>{
}).then(res=>{
console.log(res);
});
// 0
Promise.resolve().then(res=>{
return 0;
}).then(res=>{
console.log(res);
});
// 1
Promise.resolve().then(res=>{
return Promise.resolve(1);
}).then(res=>{
console.log(res);
});
// 2
Promise.resolve().then(res=>{
return Promise.reject(2);
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err); // 2
});
// 2
Promise.resolve().then(res=>{
return Promise.reject(2);
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err); // 2
});
// err
Promise.resolve().then(res=>{
throw new Error("err");
}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err); // err
});
回调定义的时机
- promise可以任意时机定义then和catch的回调
// 先执行Promise,后进行回调定义
Promise.resolve(0).then(res=>{
console.log(res); // 0
})
// 延迟Promise 0 1
new Promise((resolve)=>{
setTimeout(()=>{
console.log(0);
resolve(1);
}, 1000)
}).then(res=>{
console.log(res);
})
// 延迟回调定义,此时一旦定义,立即执行回调
let p = new Promise((resolve)=>{
console.log(0);
resolve(1);
})
setTimeout(()=>{
p.then(res=>{
console.log(res);
})
}, 1000);
链式调用
嵌套的异步回调陷阱
- 所有的异步任务需要封装成Promise返回,否则将成为”另一个线程“
// 以下输出 0 undefined 1
Promise.resolve().then(res=>{
console.log(0);
setTimeout(()=>{
console.log(1);
return 2;
}, 1000);
}).then(res=>{
console.log(res);
})
// 正确写法 0 1 2
Promise.resolve().then(res=>{
console.log(0);
return new Promise((resolve)=>{
setTimeout(()=>{
console.log(1);
resolve(2);
}, 1000);
})
}).then(res=>{
console.log(res);
})
错误(异常)穿透(传透)
- 发生错误立即中断原来得程序,进行透传,直到遇到then的第二个参数或catch进行捕获
- 捕获后没有新错误则返回resolved
// 2 err 3
Promise.reject(new Error("err")).then(res=>{
console.log(0,res);
}).then(res=>{
console.log(1,res);
}).catch(err=>{
console.log(2,err);
}).then(res=>{
console.log(3,res);
}).catch(err=>{
console.log(4,err);
})
// 当不写then的第二个参数时,默认为throw err
Promise.reject(new Error("err")).then(null, err=>{
console.log(err); // err
throw err;
}).catch(err=>{
console.log(err); // err
})
// 处理异常后,视为成功
Promise.reject(new Error("err")).then(null, err=>{
console.log(err); // err
}).catch(err=>{
console.log(err);
})
2.5 Promise的嵌套使用
- Promise可以嵌套使用,相当于在同步回调中进行了一次异步回调
构造函数中的嵌套
new Promise((resolve, reject)=>{
new Promise((resolve2,reject2)=>{
resolve2(0);
}).then((res)=>{
resolve(res);
})
}).then(res=>{
console.log(res);
})
// 上面写法类似于
new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve(0);
});
}).then(res=>{
console.log(res);
})
then和catch中的嵌套
- 同样的,只有Promise构造函数能控制异步操作的回调,then中也不能直接写
new Promise((resolve, reject)=>{
resolve(0);
}).then(res=>{
console.log(res);
setTimeout(()=>{
return 1;
}, 1000);
}).then(res=>{
console.log(res); // undefined
})
new Promise((resolve, reject)=>{
resolve(0);
}).then(res=>{
console.log(res);
return new Promise((resolve)=>{
setTimeout(()=>{
resolve(1);
}, 1000);
})
}).then(res=>{
console.log(res); // 1
})
2.6 Promise的其他静态方法
- promise.all(arr:Promise[]):Promise
- promise.race(arr:Promise[]):Promise
- all相当于逻辑与,当数组中的所有promise都为resolved的状态时才为resolved,当任意promise为rejected,则为rejected
- race相当于逻辑或
3 async函数和await表达式
3.1 async函数
- 返回Promise对象,状态为resolve,值为其返回值
- 同理,当返回值是Promise对象时,替换默认的Promise对象
- 当抛出异常时,返回rejected的Promise对象
// 返回[resolved, undefined]
async function func1(){
}
func1();
// 返回[resolved, 2]
async function func2(){
return 2;
}
func2();
// 返回[rejected, 3] uncaught error
async function func3(){
return Promise.reject(3);
}
func3();
3.2 await表达式
- await后加Promise对象,得到的值为Promise成功的值
- await后不是Promise对象,则值为其本身
- await是.then成功参数的语法糖
// 0 1 2 3
async function func(){
try{
console.log(0);
console.log(await 1);
console.log(await Promise.resolve(2));
console.log(await Promise.reject(3));
}catch(err){
console.log(err);
}
}
func();
4 总结
promise的优点:
-
书写一次catch,捕获所有错误
-
任意时机定义回调
- 当状态转移了,回调一旦定义立即(按照微任务)执行
- 当状态没转移,回调定义后等待,一旦状态转移立即(按照微任务)执行
-
使用链式调用解决回调地狱