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

LucasTwilight發表於2018-10-26

寫在前面

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

有人說為什麼這麼簡單的一個東西要用看起來很複雜的數學模型來表示呢?因為我們平時接觸到的很多程式設計相關的知識都是從這個數學模型上發展出來的。可能在你自己不知道的時候,已經使用了這個理論來coding。數學抽象能夠讓人更加系統的瞭解這個理論,並且進行推導。

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

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

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

有限狀態機

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

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

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

有限在這裡是定語,表示狀態是有限的,也就是一個正確的有限狀態機只有有限個狀態,但是允許這個狀態機不間斷地執行下去。

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

構成一個狀態機的最簡單的幾個因素(這裡以紅綠燈作為例子):

  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的實現以及和自動機的關係(雖然不知道這個業務迭代週期內有沒有時間寫了。。。)

相關文章