當面試官問Promise的時候他想知道什麼

希沃ENOW大前端javascript

公司官網:CVTE(廣州視源股份)前端

團隊:CVTE旗下將來教育希沃軟件平臺中心enow團隊java

本文做者:es6

瀚程名片.png

前言

Promise 是一種異步編程的解決方案,能夠認爲它是一個容器,裏面保存着將來發生的事件結果。 它有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗),狀態一旦發生改變就不能再次改變。編程

什麼是回調地獄?

在處理異步請求時,咱們通常都會使用回調函數這麼處理,這麼看徹底沒毛病,邏輯清晰。promise

http.post(data,function(res) {
    // do something here
})
複製代碼

可是若是咱們須要根據前一個請求返回的結果來發起下一個請求的話,代碼則變成了這樣:markdown

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
          })
        })
      })  
    })  
  })
})
複製代碼

這即是臭名昭著的回調地獄了,帶來的負面影響也是不言而喻的:async

  • 代碼臃腫,可讀性差
  • 耦合程度高,可維護性差
  • 只能在回調函數內部處理異常

若是使用 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 的狀態一旦發生變化,就不能夠再次改變

正確的輸出順序是一、四、三、五、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 函數

相關文章
相關標籤/搜索