從Promise的實現來看有限狀態機

寫在前面

有限狀態機在我讀研的時候是一門必修的課程,也就是大部分CS研究生都要接觸的一門課程。這門課說簡單也蠻簡單的,可是其中內含的內容以及應用實在是太多了。javascript

有人說爲何這麼簡單的一個東西要用看起來很複雜的數學模型來表示呢?由於咱們平時接觸到的不少編程相關的知識都是從這個數學模型上發展出來的。可能在你本身不知道的時候,已經使用了這個理論來coding。數學抽象可以讓人更加系統的瞭解這個理論,而且進行推導。前端

幹說是無味的。咱們能夠從前端的一些東西中看到有限狀態機的影子。那麼就從一些實際應用開始,來理解下有限狀態機。java

這一個系列可能分紅好幾篇文章,又臭又長,從簡單到抽象。git

有限狀態機

先來簡單描述一下有限狀態機,百科上給的解釋很簡單:github

有限狀態機(英語:finite-state machine:FSM)又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動做等行爲的數學模型。----來自wiki編程

有限狀態機這個名稱的核心是兩個詞:有限和狀態(彷佛說了句廢話。。)。promise

有限在這裏是定語,表示狀態是有限的,也就是一個正確的有限狀態機只有有限個狀態,可是容許這個狀態機不間斷地運行下去。瀏覽器

最簡單也最經常使用的有限狀態機的例子就是紅綠燈,三個顏色就表明了三個不一樣的狀態,根據必定的條件(這裏就是延時),觸發狀態的轉變。bash

構成一個狀態機的最簡單的幾個因素(這裏以紅綠燈做爲例子):框架

  1. 狀態集:紅、黃、綠;
  2. 初始狀態:紅綠燈的啓動狀態;
  3. 狀態轉移函數:延時或者動態調度;
  4. 最終狀態:紅綠燈關閉的狀態,固然也可能不存在;

說了這麼多,都是數學模型。那麼在前端領域有沒有一個簡單的有限狀態機呢。固然不少不少,這篇就先來講一下Promise和狀態機的關係。

Promise的實現

直接說Promise和狀態機的關係可能沒有那麼好理解,這裏先把結論留下,再看一個簡單的Promise的實現代碼,就能夠大體理解有限狀態機在Promise中的功能了。

Promise是一個具備四個狀態的有限狀態機,其中三個核心狀態爲PendingFulfilled以及Rejected分別表示該Promise掛起,完成以及拒絕。還有一個額外的初始狀態,表示Promise還未執行,這個狀態嚴格上來講不算是Promise的狀態,可是在實際Promise的使用過程當中,都會具有這個狀態。

根據上面的闡述,就大概搭建起了這個有限狀態機的框架。

那麼就能夠從有限狀態機的構成因素,來本身實現一個Promise了。

Promise

Promise是ES6標準提供的一個異步操做的很好的語法糖,對於本來的回調函數模式進行了封裝,實現了對於異步操做的鏈式調用。而且配上generator以及async語法糖來使用更加方便。

雖然Promise當前在不少瀏覽器上都已經獲得了支持,可是在看Promise的時候,發現對於Promise的不少地方仍然不是很瞭解。包括其內部的實現機制,寫這個代碼的目的也是在於對Promise的使用更加了如指掌。

Promise的具體使用方法能夠看個人這一篇博客,這裏就不對Promise對象自己的使用進行說明了,默認你們都已經掌握基本的Promise的使用方法了。若是不甚瞭解的話,請看Promisegeneratorasync/await

下面具體的代碼能夠參見個人github中的fake-promise

初始狀態:new Promise

首先,對於ES6原生的Promise對象來講,在初始化的過程當中,咱們傳遞的是一個function(resolve, reject){}函數做爲參數,而這個函數是用來進行異步操做的。

目前javascript中的大部分異步操做都是使用callback的方式進行的,Promise的回調傳入一個函數,這個函數的接受兩個參數,分別在狀態轉變爲FULFILLED以及REJECTED的時候被調用。

若是異步操做失敗的話,那麼天然就是將失敗緣由處理以後,調用reject(err)函數。

var p = new Promise(function(resolve, reject) {
  fs.readFile('./readme', function(err, data) {
    if (err) {
      reject(err);
    } else {
      resolve(data);
    }
  });
});
複製代碼

也就是這個兩個參數函數不管如何,都是會在異步操做完成以後調用的。

那針對這一點,能夠先這樣寫Promise的構造函數(這是Promise-polyfill的大致框架和初始化函數):

const promiseStatusSymbol = Symbol('PromiseStatus');
const promiseValueSymbol = Symbol('PromiseValue');
const STATUS = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
};
const transition = function(status) {
  var self = this;
  return function (value) {
    this[promiseStatusSymbol] = status;
    this[promiseValueSymbol] = value;
  }
}
const FPromise = function(resolver) {
  if (typeof resolver !== 'function') {
    throw new TypeError('parameter 1 must be a function');
  }
  this[promiseStatusSymbol] = STATUS.PENDING;
  this[promiseValueSymbol] = [];
  this.deps = {};
  resolver(
    // 這裏返回兩個函數,這兩個函數也就是resolver和reject。
    // 這兩個函數會分別對於當前Promise的狀態和值進行修改
    transition.call(this, STATUS.FULFILLED),
    transition.call(this, STATUS.REJECTED)
  );
}
複製代碼

在進行了new Promise的初始化以後,這個Promise就進入了本身的第一個狀態,也就是初始態。

初始狀態

狀態集:PENDINGFULFILLEDREJECTED

這裏的FULFILLED狀態其實就是Resolved,只不過Resolved這個單詞太具備迷惑性了,FULFILLED更能體現這個狀態的意義。

根據使用Promise的經驗,其整個生命週期應該是具備狀態的,當開始異步操做,可是尚未結果的時候,應該是掛起狀態PENDING,而後是成功和失敗的狀態。

傳入到構造函數中的函數須要在構造函數中被調用,來開始異步操做。而後經過咱們傳遞進去的兩個函數來分別修改爲功和失敗的狀態以及值。

當咱們調用了封裝爲Promise的函數以後,這個狀態機就啓動了。啓動以後,假設這個異步操做要執行10S,那麼狀態機在執行以後,會由Start變爲PENDING,表示這個異步操做被掛起。

10秒的PENDING狀態在執行異步操做完成了以後,存在兩個分支:

  1. 若是這個異步操做成功,並未拋出錯誤,那麼狀態機跳轉到FULFILLED
  2. 若是異步操做失敗,或者拋出了錯誤,那麼狀態機跳轉到REJECTED

resolve & reject

上面的整個過程是Promise狀態機的最根本的一個過程,可是Promise是能夠進行鏈式調用的,也就是這個狀態機能夠循環往復地進行狀態的改變。

FPromise.prototype.then = function(onFulfilled, onRejected) {
  const self = this;
  return FPromise(function(resolve, reject) {
    const callback = function() {
      // 注意這裏,對於回調函數執行時候的返回值,也須要保存下來,
      // 由於鏈式調用的時候,這個參數應該傳遞給鏈式調用的下一個
      // resolve函數
      const resolveValue = onFulfilled(self[promiseValueSymbol]);
      resolve(resolveValue);
    }
    const errCallback = function() {
      const rejectValue = onRejected(self[promiseValueSymbol]);
      reject(rejectValue);
    }
    // 這裏是對當前Promise狀態的處理,若是上一個Promise在執行then方法以前就已經
    // 完成了,那麼下一個Promise對應的回調應該直接執行
    if (self[promiseStatusSymbol] === STATUS.FULFILLED) {
      return callback();
    } else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
      return errCallback();
    } else if (self[promiseStatusSymbol] === STATUS.PENDING) {
      self.deps.resolver = callback;
      self.deps.rejecter = errCallback;
    }
  })
}
複製代碼

then方法應該是Promise進行鏈式調用的根本。

首先,then方法具備兩個參數,分別是成功和失敗的回調,

而後,其應該返回一個新的Promise對象來給鏈式的下一個節點進行調用,

最後,這裏若是自己Promise對象的狀態已是FULFILLED或者REJECTED了,那麼就能夠直接調用回調函數了,不然須要等待異步操做的完成狀態發生。

狀態轉移函數:鏈式調用

嚴格來講,每次進行狀態的轉移都是根據當前異步操做的執行狀態來進行判斷的。可是每次異步操做的迭代都是依賴Promise的鏈式操做,不然這個狀態機也不會產生如此多的狀態轉移過程。

依賴收集

鏈式調用的根本是依賴收集,通常來講,Promise中的代碼都是異步的,在執行函數的不可能當即執行回調內的函數。

if (self[promiseStatusSymbol] === STATUS.FULFILLED) {
	return callback();
} else if (self[promiseStatusSymbol] === STATUS.REJECTED) {
	return errCallback();
} else if (self[promiseStatusSymbol] === STATUS.PENDING) {
	// 通常都是PENDING狀態,直接收集回調
	self.deps.resolver = callback;
	self.deps.rejecter = errCallback;
}
複製代碼

依賴被收集到一塊兒以後,在狀態發生變化的時候,咱們採用一個setter來對狀態的變化進行響應,而且執行對應的回調。

const transition = function(status) {
  return (value) => {
    this[promiseValueSymbol] = value;
    setStatus.call(this, status);
  }
}
/** 
  * 對於狀態的改變進行控制,相似於存取器的效果。
  * 若是狀態從 PENDING --> FULFILLED,則調用鏈式的下一個onFulfilled函數
  * 若是狀態從 PENDING --> REJECTED, 則調用鏈式的下一個onRejected函數
  *
  * @returns void
  */
const setStatus = function(status) {
  this[promiseStatusSymbol] = status;
  if (status === STATUS.FULFILLED) {
    this.deps.resolver && this.deps.resolver();
  } else if (status === STATUS.REJECTED) {
    this.deps.rejecter && this.deps.rejecter();
  }
}
複製代碼

當第一個異步執行完畢後,會執行其依賴中的resolver或者rejecter。而後咱們會在這個resolver中返回一個新的Promise,那麼這個新的Promisep2就能夠接着p1開始執行,p2的結構和p1是如出一轍的,在其被構造了以後,一樣地,進行依賴收集以及鏈式調用,造成了一個狀態屢次循環的有限狀態機。

完整的狀態機

有限狀態機與Promise

到了這裏,你們應該都能看到有限狀態機和Promise之間的關係了。其實Promise除了依賴收集過程以外,就是一個相似紅綠燈的有限狀態機。

Promise基本上具備一個有限狀態機的全部主要因素。一個Promise的狀態機在其生命週期中經過狀態的轉移,來控制異步函數的同步執行,在必定程度上保證了回調函數的callback hell

除了Promise這個比較簡單的,採用了有限狀態機數學模型的實現以外,前端還有其餘和狀態機相關的實踐。而且還有很是複雜的實踐。下一篇會講一下Redux的實現以及和自動機的關係(雖然不知道這個業務迭代週期內有沒有時間寫了。。。)

相關文章
相關標籤/搜索