你不知道的 Promise 对象黑科技

Promise 必知必会(十道题)

一、resolve 后的执行情况

无论是 resolve, reject,都会将函数剩余的代码执行完

const promise = new Promise((resolve, reject) => {
    console.log('mark 1');
    resolve('hello world');     // reject('hello world');
    console.log('mark 2');
});

promise.then(result => {
    console.log(result);
}).catch(err => {
    console.log(err);
});

相当于:

const promise = new Promise((resolve, reject) => {
    console.log('mark 1');
    console.log('mark 2');
    resolve('hello world');     // reject('hello world');
});

promise.then(result => {
    console.log(result);
}).catch(err => {
    console.log(err);
});

如果你不想在 resolve 或 reject 后执行剩下的代码段,可以在 resolve 后将其返回

const promise = new Promise((resolve, reject) => {
    console.log('mark 1');
    return resolve('hello world');     // reject('hello world');
    console.log('mark 2');             // never be here
});

promise.then(result => {
    console.log(result);
}).catch(err => {
    console.log(err);
});

二、串行执行和并行执行:

  1. 串行执行:有一堆 Promise 对象,它们的执行顺序是固定的,前一个 promise 执行完后,后一个 promise 才开始执行,比如数据库查询,它们往往有前后的因果关系。
  2. 并行执行:有一堆 Promise 对象,它们的执行顺序是不固定的,没有前后因果关系,可以并发地去执行。

并行执行很好解决,在 Promise中有 all 这个函数支持, Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。当多个 Promise 实例执行完后才去执行最后新的 Promise 实例。

const datum = [];
for(let i = 0; i < 10; i++) {
    datum.push(i);
}

Promise.all(datum.map(i => {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(i * 200 + " ms 后执行结束");
        resolve("第 " + (i + 1) + " 个 Promise 执行结束");
    }, i * 200);
    })
})).then((data) => {
    console.log(data);
});

如果不使用 Promise.all 这个方法的话,你也可以使用像 ES7 的 async/await

const asyncFun = async () => {
    const datum = []
    for(let i = 0; i < 10; i++) {
        datum.push(new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(i * 200 + 'ms 后执行结束')
                resolve('第 ' + (i + 1) + ' 个 Promise 执行结束')
            }, i * 200)
        }))
    }
    const result = []
    for(let promise of datum) {
        result.push(await promise)
    }
    console.log(result)
}
asyncFun()

串行执行:这里提供两种方式

const datum = [];
for(let i = 0; i < 10; i++) {
    datum.push(i);
}

let serial = Promise.resolve();

for(let i of datum) {
    serial = serial.then(data => {
        console.log(data);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
        console.log(i * 200 + " ms 后执行结束");
        resolve("第 " + (i + 1) + " 个 Promise 执行结束");
        }, i * 200);
    })    
    });
}

另外可以使用 reduce 来串行:

const datum = [];
for(let i = 0; i < 10; i++) {
    datum.push(i);
}

datum.reduce((prev, cur) => {
    return prev.then(data => {
    console.log(data);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
        console.log(cur * 200 + " ms 后执行结束");
        resolve("第 " + (cur + 1) + " 个 Promise 执行结束");
        }, cur * 200);
    })    
    })
}, Promise.resolve(true));

三、值穿透问题:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Hello World!');
    }, 1000)
});

promise.then('呵呵哒').then((data) => {
    console.log(data);           // Hello World
})

这是一种值穿透的情况,一般有下面两种情况:
promise 已经是 FULFILLED/REJECTED 时,通过 return this 实现的值穿透:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Hello World!');
    }, 1000)
});

promise.then(() => {
    promise.then().then(null).then('呵呵哒').then((res) => {
        console.log(res)
    })
    promise.catch().catch(null).then('呵呵哒').then((res) => {
        console.log(res) 
    })
})

promise 是 PENDING 时,通过生成新的 promise 加入到父 promise 的 queue,父 promise 有值时调用 callFulfilled->doResolve 或 callRejected->doReject(因为 then/catch 传入的参数不是函数)设置子 promise 的状态和值为父 promise 的状态和值。如:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Hello World!');
    }, 1000)
});

let a = promise.then('呵呵哒');
a.then(res => {
    console.log(res);
});

let b = promise.catch('呵呵哒');
b.then(res => {
    console.log(res);
})

总而言之,当你给 then() 传递一个非函数(比如一个 promise )值的时候,它实际上会解释为 then(null) ,这会导致之前的 promise 的结果丢失。例如:

Promise.resolve('First Value').then(Promise.resolve('Second Value')).then(null).then((value) => {
    console.log(value)    // First Value
})

四、不要在异步回调函数中使用 throw Error

不仅 reject,抛出的异常也会被作为拒绝状态被 Promise 捕获

let promise = new Promise((resolve, reject) => {
    reject('This is an error');
});

promise.then(result => {
    console.log(result);
}).catch(error => {
    console.log('handle error: ', error);  //handle error:  Error: This is an error
})

但是,永远不要在回调队列中抛出异常,因为回调队列脱离了运行上下文环境,异常无法被当前作用域捕获。

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        throw Error('This is an error');
    });
});

promise.then(result => {
    console.log(result);
}).catch(error => {
    console.log('handle error: ', error);  // Error: This is an error
});

简单说来,回调队列指的是 JS 事件循环中的 macrotask 队列,比如 setTimeout setInterval 会插入到 macrotask 中。如果要在回调函数中捕获异常,请使用 reject,永远不要使用 Error。
上述的代码应改成:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('This is an error');
    });
});

promise.then(result => {
    console.log(result);
}).catch(error => {
    console.log('handle error: ', error);  // Error: This is an error
});

五、then 的第二个参数跟 catch 的区别 【面试常问】

我们都知道 then 的第二参数跟 catch 用法很像,都是用来进行错误处理的,比如下面这段代码:

let promise1 = new Promise((resolve, reject) => {
    reject('this is an error');
});

promise1.then(data => {
    console.log(data);
}, err => {
    console.log('handle err:', err);    // handle err: this is an error
});

let promise2 = new Promise((resolve, reject) => {
    reject('this is an error');
});
promise2.then(data => {
    console.log(data);
}).catch(err => {
    console.log('handle err:', err);    // handle err: this is an error
});

当时这两者还是区别的,区别于 then 的第二参数无法处理第一参数函数中的错误。

let promise1 = Promise.resolve();
promise1.then(() => {
    throw Error('this is a error');   //UnhandledPromiseRejectionWarning: Unhandled promise rejection
}, err => {
    console.log(err);
})

let promise2 = Promise.resolve();

promise2.then(() => {
    throw Error('this is a error');  
}).catch(err => {
    console.log('handle err:', err);    //handle err: Error: this is a error
})

当你使用then( resolveHandler, rejectHandler)格式,如果 resolveHandler 自己抛出一个错误 rejectHandler 并不能捕获。第一个 Promise 对象无法处理同级 then 中的函数抛出的异常,所以在一般情况下,最后直接使用 catch 来进行异常捕获比较保险。

六、处理最后 catch 函数中的异常

一般我们用 catch 来捕捉前面抛出的异常,但是如果试想一下如果最后一个 catch 函数也抛出了异常,应该怎么处理呢?

let promise = new Promise((resolve, reject) => {
    reject('Hello World')
});

promise.catch((err) => {
    throw('Unexpected Error');   // Uncaught (in promise) Unexpected Error
})

面对这样的错误,不管以 then 方法或 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)这里提供两种思路:

  • 拓展 Promise.prototype 的方法,添加一个 done 函数,将错误抛向全局。

    window.onerror = (err) => {

    console.log(err);
    

    }
    Promise.prototype.done = function (onFulfilled, onRejected) {

    this.then(onFulfilled, onRejected)
      .catch(function (reason) {
        // 抛出一个全局错误
        setTimeout(() => { throw reason }, 0);
      });
    

    };
    let promise = new Promise((resolve, reject) => {

    reject('Hello World')
    

    });

    promise.catch((err) => {

    throw('Unexpected Error');     // Uncaught Unexpected Error
    

    }).done()

  • 在全局添加 unhandledrejection 事件捕获 Promise 异常。

window.addEventListener("unhandledrejection", (e) =>{
    console.log(e.reason)
})    

let promise = new Promise((resolve, reject) => {
    reject('Hello World')
});

promise.catch((err) => {
    throw('Unexpected Error');     // Unexpected Error
})

七、未捕获的错误可以被恢复

let promise = new Promise((resolve, reject) => [
    reject('Hello world')
]).then(() => {
    console.log('resolve')
})

setTimeout(() => {
    promise.catch((e) => {
        console.log(e)
    }).then(() => {
        console.log('catch resolve')
    })
}, 1000)

八、resolved 状态的 Promise 不会立即执行

let i = 0;
Promise.resolve('resolved promise').then(() => {
    i += 2
})
console.log(i)  // 0

即使是 resolve 的 Promise 调用 then 方法也是异步执行。

九、结合 async/await 编写同步代码

  1. async/await 函数可以帮助我们彻底摆脱回调地狱的烦恼,用一种同步的方式来编写异步函数。
  2. await 后面可以接数值,如果是异步请求的话可以接 Thunk 函数和 Promise 对象。

const timeout = (ms) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(ms + ' passed')
        }, ms)
    })
}

const asyncFunc =  async () => {
    const value1 = await timeout(2000)
    console.log(value1)
    const value2 = await timeout(2000)
    console.log(value2)
}

asyncFunc()
console.log('now')

十、调用 then 方法返回新的 Promise 对象

let promise1 = new Promise((resolve) => {
    resolve('Hello world')
})

let promise2 = promise1.then()

console.log(promise1 === promise2)    // false
console.log(promise1 instanceof Promise)  // true
console.log(promise2 instanceof Promise)  // true

每次调用 then 方法后都会返回一个新的 Promise 对象,并不是返回原本的 Promise 对象。