准备
函数调用
回调函数
- 是自定义函数
- 不用调用,自己执行
JavaScript的异步处理可以用回调函数,回调函数的使用就是确保一段代码执行完毕之后再执行另一段代码的方式
function doHomework(subject, callback){
console.log("start my " + subject + " homework.");
callback();
}
function finish(){
console.log("finish my homework.");
}
doHomework('math', finish);
// start my math homework.
// finish my homework.
finish函数作为回调函数,确保doHomework函数执行完毕之后才执行
同步回调
按顺序立即执行
const arr = [1, 3, 5]
arr.forEach((item)=>{
console.log(item)
})
console.log("finish")
输出 1 3 5 finish
异步回调
不会立即执行,放入任务队列中等待同步任务执行完毕再执行
setTimeout(()=>{
console.log("s")
},0)
console.log("w")
输出 w s
实例对象和函数对象
括号左边的是函数,点的左边是对象
function Fn(){ }
const fn = new Fn() // fn是实例对象(简称对象),Fn是构造函数
console.log(Fn.prototype) // Fn是函数对象
错误
常见的内置错误
ReferenceError
表示引用的变量不存在
console.log(a) // ReferenceError: a is not define
TypeError
表示数据类型不正确
let b = {}
b.xxx() // TypeError: b.xxx is not a function
RangeError
表示数据值不在其允许的范围内
function fn(){
fn()
}
fn() // RangeError: Maximum call stack size exceeded
函数的调用有次数限制,超过这个maximum次数就报错(溢出)
SnytaxError
表示语法错误
const c = """" // SytaxError: Unexpected sytax
错误处理
捕获错误 try…catch
error是一个对象,有message和stack属性
try{
let b = {}
b.xxx()
}catch(error){
console.log(error.message)
console.log(error.stack)
}
抛出错误 throw error
如果满足什么条件就主动抛出错误
下面例子判断当前日期,如果为奇数就正常执行,为偶数就alert无法执行
function something(){
if (Date.now()%2 == 1){
console.log("当前时间为奇数,可以执行任务")
}else{ // 创建Error对象,抛出错误
throw new Error("当前时间为偶数,无法执行任务")
}
}
// 捕获处理异常,因为something里面可能出现异常,要在这里捕获,否则如果出现异常就会报错
try{
something()
}catch(error){
alert(error.message)
}
Promise的理解和使用
理解Promise
- Promise是JS中进行异步编程的新的解决方案(旧的是回调函数)
- 从语法上来说,Promise是一个构造函数
- 从功能上来说,Promise对象用来封装一个异步操作并可以获取其结果
直接console.dir()出来看Promise
发现Promise是一个构造函数,其只接受一个参数,是一个回调函数function(),并且传入两个参数:resolve,reject,这两个参数都是回调函数,在这个函数参数function内部调用。
Promise构造出来的实例函数有then、catch等方法(来自于原型链)
Promise的状态改变
Promise函数基本用法
其实Promise函数的使命,就是构建出它的实例,并且负责帮我们管理这些实例。而这些实例有以下三种状态:
- pending: 初始状态,未履行或拒绝
- resolve: 意味着操作成功,返回value
- rejecte: 意味着操作失败,返回reason
基本用法如下,只写then或catch分别表示这个promise只执行resolve或reject的情况
// 1. 创建一个新的Promise对象
new Promise(function(resolve,reject){
// 2. 执行异步操作任务
setTimeout(()=>{
const time = Date.now()
// 3.1 如果成功了,则调用resolve(value)
if (time%2 == 0){
resolve('成功的数据,time=' + time)
}
// 3.2 如果失败了,则调用reject(reason)
else{
reject('失败的数据,time=' + time)
}
}, 1000)
}).then(value=>{
console.log('成功的回调', value)
}).catch(reason=>{ //这行也可以简写成 }, function(){,不用catch
console.log('失败的回调', reason)
});
resolve和reject都带参数的实例,resolve方法和reject方法调用时,都带有参数。它们的参数会被传递给回调函数。
用Promise处理AJAX的成功和失败
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
为什么要用Promise函数,Promise的优点
1. Promise处理异步比回调函数更灵活
不仅可以处理异步,还可以结合setTimeout设置函数执行的时间
function handleSuccess(value){
console.log("success "+value)
}
function handleFailure(error){
console.log("error "+error)
}
var p = new Promise((resolve,reject)=>{
var time = Date.now()
if(time%2 == 0){
resolve(time)
}else{
reject(time)
}
})
setTimeout(()=>{
p.then(handleSuccess,handleFailure),1000}) // 注意这里只写函数,不用handleSuccess(value)去调用
2. 支持链式调用,解决回调地狱问题
回调地狱:回调函数嵌套调用,外部回调函数异步执行的结果时嵌套回调函数执行的条件或参数
回调地狱缺点:不便于阅读、多个异常处理
以下伪代码展示
function fn1(){
return fn2(
}
如何使用Promise
Promise.prototype.then方法:链式操作
Promise.prototype.then(resolveFunction[, rejectFuntion]) ,第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。该方法返回的是一个新的 Promise 对象,因此可以采用链式写法
new Promise(function(resolve,reject){
//...
}).then(function(result){
console.log(result)
return new Promise // 链式调用的关键,上一层结束后返回一个Promise对象
}).then(function(result){ // 在下一层then执行上面Promise的resolve
console.log(result)
})
上面的代码使用 then 方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
Promise.prototype.catch方法:捕捉错误
Promise.prototype.catch(function) 方法是 Promise.prototype.then(resolveFunction, rejectFuntion) 的别名,用于指定发生错误时的回调函数。
new Promise(function(resolve,reject){
//...
}).then(function(result){
console.log(result)
return new Promise
}).catch(function(error){ // 处理前一个回调函数运行时发生的错误
console.log(error)
})
Promise.resolve() 和Promise.reject()
Promise.resolve()和Promise.reject()其实就是Promise的语法糖,简化代码,将现有对象转为Promise对象
var p = new Promise(resolve,reject){
resolve('Hello')
}
// 以上可以简化为
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上述生成一个新的Promise对象实例p,状态为resolve,Promise.resolve()的参数就是回调函数的参数s
var p = Promise.reject('出错了');
p.catch(function (s){
console.log(s)
});
// 出错了
上述生成一个新的Promise对象实例p,状态为reject,Promise.reject()的参数就是回调函数的参数s
Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]).then().catch();
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
以下是一个异步延时的例子
var sleep = function(ms){
return new Promise(function(resolve,reject){
// 判断输入类型,reject给后面的函数catch传入参数
if(typeof ms != "number"){reject(new Error("输入错误"))}
// 用计时器设置延时输出,resolve给后面的函数then传入参数
setTimeout(function(){
resolve("延迟了 " + ms + " 毫秒输出")
},ms)
}).catch(function(error){console.log("promises: "+error)}) // 这里捕获子Promise的错误,如果出错则promise.all还能继续执行
}
// 用map创建一个Promise实例数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return sleep(id);
});
Promise.all(promises).then(function (result) {
console.log(result)
}).catch(function(error){
console.log("promise.all: "+ error)
});
输出数组["延迟了 2 毫秒输出", "延迟了 3 毫秒输出", "延迟了 5 毫秒输出", "延迟了 7 毫秒输出", "延迟了 11 毫秒输出", "延迟了 13 毫秒输出"]
Promise.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]).then().catch()
p的状态是只要p1、p2、p3之中有一个实例最先改变状态,p的状态就跟着改变。那个最先改变的 Promise 实例的返回值,就传递给p的回调函数。
异步延时的例子
var sleep = function(ms){
return new Promise(function(resolve,reject){
// 判断输入类型,reject给后面的函数catch传入参数
if(typeof ms != "number"){reject(new Error("输入错误"))}
// 用计时器设置延时输出,resolve给后面的函数then传入参数
setTimeout(function(){
resolve("延迟了 " + ms + " 毫秒输出")
},ms)
}).catch(function(error){console.log("promises: "+error)}) // 这里捕获子Promise的错误,如果出错则promise.all还能继续执行
}
// 用map创建一个Promise实例数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return sleep(id);
});
Promise.race(promises).then(function(result){
console.log(result)
}).catch(function(error){
console.log("promise.race: "+ error)
})
输出结果延迟了 2 毫秒输出
同步和异步操作中的输出优先级
规则
- 按先后顺序执行完所有同步任务;
- 再执行异步任务,先按先后顺序执行微任务,再按先后顺序执行宏任务:
- macro-task: script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering.
- micro-task: process.nextTick, Promise(原生),Object.observe,MutationObserver
(一个简便常用的记法:Promise > set…)
async和await搭配相当于Promise,async的函数内要有await才会实行异步,await紧跟的任务执行完,再执行所有同步任务,然后才到await下一行的任务(其实就是await下一行都是异步任务,所有肯定是先执行所有同步任务)
例题1 顺丰面试题
(function test() {
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve(null)
}).then(v => {
console.log(3)
})
console.log(4);
})()
- 同步任务按顺序先输出2和4
- 异步任务先输出微任务promise.then的3,再宏任务setTimeout的1
例题2 比较全面的检测
setTimeout(function() {
console.log("s1")
}, 0);
setTimeout(function() {
console.log("s2")
}, 1000);
new Promise(function(resolve){
console.log("p1");
resolve();
console.log("p2");
}).then(function(){
console.log("p3");
});
console.log("w1");
async function test1() {
console.log("a1");
await test2();
console.log("a2");
}
async function test2() {
console.log("a3");
}
test1();
console.log("w2")
- 同步任务按顺序,输出p1 p2 w1 a1 a3 w2
- 异步任务,先微任务按顺序,输出p3 a2,后宏任务,输出s1 s2
参考解析
例题3 来自《深入浅出Node.js》
//加入两个nextTick的回调函数
process.nextTick(function () {
console.log('n1');
});
process.nextTick(function () {
console.log('n2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
console.log('s1');
// 进入下次循环
process.nextTick(function () {
console.log('n3');
});
});
setImmediate(function () {
console.log('s2');
});
console.log('w');
- 同步任务按顺序,输出 w
- 异步任务,先微任务输出 n1 n2,再宏任务输出 s1 s2,最后输出 n3
在新版的Node中,process.nextTick执行完后,会循环遍历setImmediate,将setImmediate都执行完毕后再跳出循环。所以两个setImmediate执行完后队列里只剩下第一个setImmediate里的process.nextTick。最后输出”n3”。(这里暂时还没弄懂为啥最后才到嵌在宏任务中的微任务)
例题4 Promise的异步
Promise.resolve(1)
.then((res) => {
console.log(res);
return 2;
})
.catch((err) => {
return 3;
})
.then((res) => {
console.log(res);
});
- 执行第一个异步任务输出1,返回2
- 因为是resolve函数,所以跳过catch,执行第二个then,输出2
例题4
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});
const start = Date.now();
console.log("w1")
promise.then((res) => {
console.log(res, Date.now() - start);
});
promise.then((res) => {
console.log(res, Date.now() - start);
});
- 先执行同步任务,Promise的executor函数,内部是异步任务setTimeout,先挂起;
start赋值为当前时间;
再输出w1 - 执行异步任务,虽然Promise.then是微任务优先于setTimeout,但是没有调用它就不会执行,所以先执行setTimeout,5s后输出“开始”;
再调用resolve,执行第一个then,Date.now()是5s后的时间,输出success 5001;
执行第二个then,输出success 5001