千夜Promise基础

86 阅读8分钟

千夜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 property
      • let 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,捕获所有错误

  • 任意时机定义回调

    • 当状态转移了,回调一旦定义立即(按照微任务)执行
    • 当状态没转移,回调定义后等待,一旦状态转移立即(按照微任务)执行
  • 使用链式调用解决回调地狱