当面试官问Promise的时候他想知道什么

·  阅读 10746
当面试官问Promise的时候他想知道什么

希沃ENOW大前端

公司官网:CVTE(广州视源股份)

团队:CVTE旗下未来教育希沃软件平台中心enow团队

本文作者:

瀚程名片.png

前言

Promise 是一种异步编程的解决方案,可以认为它是一个容器,里面保存着未来发生的事件结果。 它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),状态一旦发生改变就不能再次改变。

什么是回调地狱?

在处理异步请求时,我们一般都会使用回调函数这么处理,这么看完全没毛病,逻辑清晰。

http.post(data,function(res) {
    // do something here
})
复制代码

但是如果我们需要根据前一个请求返回的结果来发起下一个请求的话,代码则变成了这样:

http.post(data,function(res1) {
  http.post(res1,function(res2) {
    // do something here
  })
})
复制代码

随着产品和业务逻辑逐渐复杂,可能就会滋生出这种代码:

http.post(data,function(res1){
	http.post(res1,function(res2){
    http.post(res2,function(res3){
      http.post(res3,function(res4){
        http.post(res4,function(res5){
          http.post(res5,function(res6){
              // do something here
          })
        })
      })  
    })  
  })
})
复制代码

这便是臭名昭著的回调地狱了,带来的负面影响也是不言而喻的:

  • 代码臃肿,可读性差
  • 耦合程度高,可维护性差
  • 只能在回调函数内部处理异常

如果使用 Promise 我们可以写成这样:

fetch(data).then(res1 => {
  return fetch(res1);
}).then(res2 => {
  return fetch(res2);
}).then(res3 => {
  return fetch(res3);
}).catch(err => {
  console.error('err: ', err);
})
复制代码

Promise 有什么不足吗?

Promise 的链式调用可以让代码变得更加直观,虽然相对看起来逻辑清晰点,但依然还是存在then调用链,有代码冗余的问题,还存在以下的不足:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

这段代码的输出是?为什么?

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve();
  setTimeout(() => {
    console.log(2);
  })
  reject('error');
})
promise.then(() => {
    console.log(3);
}).then(() => {
    console.log(5)
}).catch(e => console.log(e))
console.log(4);
复制代码

老生常谈的话题,也就是考察宏任务和微任务。重点主要是:

  • Promise 函数体内的代码同步执行
  • 先执行宏任务,再执行微任务,执行完微任务后,就再次查找是否有需要执行的宏任务,如此循环往复
  • Promise 的状态一旦发生变化,就不可以再次改变

正确的输出顺序是1、4、3、5、2

可以手写 Promise 吗?

这里简单的实现一个能够满足then方法链式调用的Promise

class Promise {
  constructor(params) {
    //初始化state为pending
    this.state = 'pending';
    //成功的值,返回一般都是undefined
    this.value = undefined;
    //失败的原因,返回一般都是undefined
    this.reason = undefined;
    //成功执行函数队列
    this.onResolvedCallbacks = [];
    //失败执行函数队列
    this.onRejectedCallbacks = [];

    //success
    let resolve = value => {
      if (this.state === 'pending') {
        //state change
        this.state = 'fulfilled';
        //储存成功的值
        this.value = value;
        //一旦成功,调用函数队列
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };

    //error
    let reject = reason => {
      if (this.state === 'pending') {
        //state change
        this.state = 'rejected';
        //储存成功的原因
        this.reason = reason;
        //一旦失败,调用函数队列
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };
    try {
      params(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => {
      throw err
    };
    let promise2 = new Promise((resolve, reject) => {
      //当状态是fulfilled时执行onFulfilled函数
      if (this.state === 'fulfilled') {
        //异步实现
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      //当状态是rejected时执行onRejected函数
      if (this.state === 'rejected') {
        //异步实现
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      //当状态是pending时,往onFulfilledCacks、onRejectedCacks里加入函数
      if (this.state === 'pending') {
        this.onResolvedCallbacks.push(() => {
          //异步实现
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          //异步实现
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
      };
    });
    return promise2;
  }
  catch(fn) {
    return this.then(null, fn);
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  //循环引用报错
  if (x === promise2) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  //防止多次调用
  let called;
  //判断x
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, y => {
          if (called) return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if (called) return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}
//resolve方法
Promise.resolve = function (val) {
  return new Promise((resolve, reject) => {
    resolve(val)
  });
}
//reject方法
Promise.reject = function (val) {
  return new Promise((resolve, reject) => {
    reject(val);
  });
}

const test = new Promise((res, rej) => {
  setTimeout(() => {
    res('resolve after 2000ms');
  }, 2000)
})

test.then(res => {
  console.error('res: ', res);	// res: resolve after 2000ms
})
复制代码

在这里我们的回调函数用setTimeout实现,把它们放到了宏任务队列里,那有什么办法可以把它放到微任务队列里吗?该怎么做?有兴趣的童鞋可以尝试一下实现一个符合Promise/A+规范Promise

除了 Promise 还有其他异步解决方案吗?

还可以用 ES6 中的 Generator 来处理,Generator 的执行有点类似于传统编程语言的协程协程的执行步骤大致如:

  • 协程A开始执行,执行到需要被挂起的地方
  • 协程A暂停,执行权交给协程B
  • 协程B执行完后,把执行权还给协程A
  • 协程A恢复执行,返回结果

Javascript 中的异步任务就类似于上述的协程A,分成两段(或多段)执行。

GeneratorPromise 类似,都可以认为是一个容器,不同之处在于 Generator 的容器是用来装异步任务的而不是状态。在需要异步操作的地方,使用 yield 交出控制权即可,使用next方法则可以夺回控制权,恢复执行,且next方法的参数可以作为上一个yield表达式的返回值。

还是同一个例子,我们用 Generator 来实现一波:

function* getData(data) {
  const res1 = yield http.post(data);
  const res2 = yield http.post(res1);
  const res3 = yield http.post(res2);
  const res4 = yield http.post(res3);
  return http.post(res4);    
}

const g = getData(123);
const res1 = g.next(); 						// {value: res1,done: false}
const res2 = g.next(res1.value);	// {value: res2,done: false}
const res3 = g.next(res2.value);	// {value: res3,done: false}
const res4 = g.next(res3.value);	// {value: res4,done: false}
const res5 = g.next(res4.value);	// {value: res5,done: true}
const res6 = g.next()							// {value: undefined,done: true}
复制代码

当调用getData时,并不会返回结果,而是返回了一个指针对象g。指针对象g上的next方法,可以让内部指针指向下一个yield语句,并返回一个表示当前执行状态的对象。value 属性是当前yield表达式的值,done属性表示当前的遍历是否结束。当遍历结束后,如果继续调用next方法,则会返回undefined

此外,Generator 还可以提前被终止,只需要调用指针对象上的return方法即可,返回对象上的done属性为true,之后再次调用next方法,总是返回donetrue

function* getData(data) {
  yield 1;
  yield 2;
  yield 3;
}

const g = getData();
g.next();						// {value: 1, done: false}
g.return('done');		// {value: 'done', done: true}
g.next();						// {value: undefined, done: true}
复制代码

对于错误捕获, Generator 可以在外部捕获错误

function* getData(data) {
  try{
		const res = yield data;
  } catch(error) {
    console.error('inner catch error: ', error);
  }
}

const g = getData(123);
g.next();

try {
  g.throw('err1');						// inner catch error: throw err
  g.throw('err2')					// outer catch error: throw err
} catch (error) {
  console.error('outer catch error: ', error);
}
复制代码

async/await有了解吗?

async 函数是什么?简单来说,它就是Generator函数的语法糖。Generator 的用法还是有点晦涩难懂的,用起来总感觉有点复杂,所以 ES7 中推出了 async/await。语法其实也是和 Generator 类似,只是将*换成asyncyield 换成await。不过Generator返回的是一个迭代器,而async/await返回的则是一个Promise对象,也就意味着可以使用thencatch等方法了。

Generator函数的执行依赖执行器。而async函数自带执行器,将 Generator 函数和自动执行器,包装在一个函数里,所以它不需要使用next方法来逐步控制函数的执行,和普通函数的调用是一致的。

async function getData(data) {
  const res1 = await fetch(data);
  const res2 = await fetch(res1);
  const res3 = await fetch(res2);
  const res4 = await fetch(res3);
  const res5 = await fetch(res4);
  return res5;
}

const finalRes = getData(123);
复制代码

总结

异步编程的终极解法,就是像同步编程一样处理异步操作。

参考文章

Promise 对象

Generator 函数

async 函数

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改