函数式编程之Promise的奇幻漂流
上一篇我們講了同步鏈式處理數據函子的概念。這一節,我們來講異步。用到的概念很簡單,不需要有函數式編程的基礎。當然如果你看了那篇 《在你身邊你左右 --函數式編程別煩惱》 會更容易理解。這一篇我們會完成一個Promise代碼的編寫。本文會從實現一個只有十幾行代碼能夠解決異步鏈式調用問題的簡單的Promise開始。然后逐漸完善增加功能。
- 實現簡單的異步Promise函子
- 能夠同時調用同一Promise函子
- 增加reject回調函數
- 增加Promise狀態
本文代碼在我的github
1 實現簡單的Promise函子
我們先來回顧一下同步鏈式調用。
class Functor{constructor (value) {this.value = value ;} map (fn) {return Functor.of(fn(this.value))}} Functor.of = function (val) {return new Functor(val); }Functor.of(100).map(add1).map(add1).map(minus10)// var a = Functor.of(100); // var b = a.map(add1); // var c = b.map(add1); // var d = c.map(minus10);復制代碼- 函子的核心就是每個functor都是一個新的對象
- 通過map中傳遞進去的函數fn去處理數據
- 用得到的值去生成新的函子
那么如果當a的值是異步產生的,我們該何如傳入this.value值呢?
function executor(resolve){setTimeout(()=>{resolve(100)},500) } 復制代碼我們模擬一下通過setTimeout500毫秒后拿到數據100。其實也很簡單,我們可以傳進去一個resolve回調函數去處理這個數據。
class Functor {constructor (executor) {let _this = this;this.value = undefined;function resolve(value){_this.value = value;}executor(resolve)} }var a = new Functor(executor);復制代碼- 我們講executor傳入并立即執行
- 在resolve回調函數中我們能夠拿到value值
- 我們定義resolve回調函數講value的值賦給this.value
這樣我們就輕松的完成了a這個對象的賦值。那么我們怎么用方法去處理這個數據呢?
- 顯然在拿到回調函數值之后,我們應該能讓map里的fn去繼續處理數據
- 處理完這個數據,我們交給下一個函數的resolve去繼續處理
- 所以我們定義了一個callback函數,
- 在調用map時,將就包含fn處理數據,和執行下一個對象的resolve的函數賦值給它
- 然后在自己的resolve拿到值之后,我們執行這個callback
現在我們已經實現了異步的鏈式調用,我們來具體分析一下,都發生了什么。
- (1)a = new Functor(executor)的時候,我們進行了初始化, executor(resolve)開始執行
- (2)b =a.map(add1)的時,先進行了初始化 new Functor(),然后執行 executor(resolve)
- (3)b中executor(resolve)執行結束,將一個函數賦值a中的callback
注意:這時map中this指向的是a函子,但是 new Functor((resolve) => {}中resolve是B的
- (4)最后return 一個新的函子b
- (5)c =b.map(add1)的時,同樣,給b中的callback賦值
- (6)然后返回一個新的函子c,此時沒有map的調用,c中的callback就是null
我們再來分析一下異步結束之后,回調函數中的resolve是如何執行的。
- (1)resolve 先_this.value = value;把a中的value進行修改
- (2)在執行_this.callback(),先let data = fn(self.value) 計算出處理后的data
- (3)調用b中的resolve函數繼續處理
- (4)b中也是,先給value賦值,然后處理數據
- (5)再調用c中的resolve,并把處理好的數據傳給他
- (6)先給C中value賦值,然后再處理數據,最后調用callback時因為不是函數會報錯,之后我們會解決
本節代碼:promise1.js
嗯,這就是promise作為函子實現的處理異步操作的基本原理。它已經能夠解決了簡單的異步調用問題。雖然代碼不多,但這是promise處理異步調用的核心。接下來我們會不斷繼續實現其他功能。
2 同時調用同一個Promise函子
如果我們像下面同時調用a這個函子。你會發現,它實際上只執行了c。
var a = new Functor(executor); var b = a.map(add); var c = a.map(minus); 復制代碼原因很簡單,因為上面我們學過,b先給a的callback賦值,然后c又給a的callback賦值。所以把b給覆蓋掉了就不會執行啦。解決這個問題很簡單,我們只需要讓callback變成一個數組就解決啦。
class MyPromise {constructor (executor) {let _this = this;this.value = undefined;this.callbacks = [];function resolve(value){_this.value = value;_this.callbacks.forEach(item => item())}executor(resolve)} then (fn) {return new MyPromise((resolve) => {this.callbacks.push (()=>{let data = fn(this.value) console.log(data) resolve(data)})})} }var a = new MyPromise(executor); var b = a.then(add).then(minus); var c = a.then(minus);復制代碼- 我們定義了callbacks數組,每次的調用a的then方法時。都將其存到callbacks數組中。
- 當回調函數拿到值時,在resolve中遍歷執行每個函數。
- 如果callbacks是空,forEach就不會執行,這也解決了之前把錯的問題
- 然后我們進一步改了函子的名字(MyPromise),將map改成then
- 簡化了return中,let self = this;
3 增加reject回調函數
我們都知道,在異步調用的時候,我們往往不能拿到數據,返回一個錯誤的信息。這一小節,我們對錯誤進行處理。
function executor(resolve,reject){fs.readFile('./data.txt',(err, data)=>{if(err){ console.log(err)reject(err)}else {resolve(data)}}) } 復制代碼- 我們現在用node異步讀取一個文件
- 成功執行 resolve(data),失敗執行 reject(err)
現在我們定義出這個reject
class MyPromise {constructor (executor) {let _this = this;this.value = undefined;this.reason = undefined;this.onResolvedCallbacks = [];this.onRejectedCallbacks = [];function resolve(value){_this.value = value;_this.onResolvedCallbacks.forEach(item => item())}function reject(reason){_this.reason = reason;_this.onRejectedCallbacks.forEach(item => item());}executor(resolve, reject);} then (fn,fn2) {return new MyPromise((resolve,reject) => {this.onResolvedCallbacks.push (()=>{let data = fn(this.value) console.log(data) resolve(data)})this.onRejectedCallbacks.push (()=>{let reason = fn2(this.reason) console.log(reason) reject(reason)})})} } 復制代碼- 其實很簡單,就是我們就是在executor多傳遞進去一個reject
- 根據異步執行的結果去判斷執行resolve,還是reject
- 然后我們在MyPromise為reject定義出和resolve同樣的方法
- 然后我們在then的時候應該傳進去兩個參數,fn,fn2
本節代碼:promise3.js
這時候將executor函數封裝到asyncReadFile異步讀取文件的函數
function asyncReadFile(url){return new MyPromise((resolve,reject) => {fs.readFile(url, (err, data) => {if(err){ console.log(err)reject(err)}else {resolve(data)}})}) } var a = asyncReadFile('./data.txt'); a.then(add,mismanage).then(minus,mismanage); 復制代碼這就是我們平時封裝異步Promise函數的過程。但這是過程有沒有覺得在哪見過。如果之前executor中的'./data.txt'我們是通過參數傳進去的那么這個過程不就是上一節我們提到的柯里化。
本節代碼:promise4.js
我們再來總結一下上面的過程。
- 我們先進行了初始化,去執行傳進來的 executor函數,并把處理的函數push進入callback數組中
- 在reslove或reject執行時,我們去執行callback中的函數
- 我們可以看到同樣一個函子a在不同時期有著不一樣的狀態。
- 顯然如果在reslove()或者 reject( )之后我們再添加then()方法是不會有作用的
那么我們如何解決reslove之后a函子的then調用問題呢,其實reslove之后,我們已經有了value值,那不就是我們最開始講的普通函子的鏈式調用嗎?所以現在我們只需要標記出,函子此時的狀態,再決定如何調用then就好啦
4 增加Promise狀態
- 我們定義進行中的狀態為pending
- 已成功執行后為fulfilled
- 失敗為rejected
我們分析一下上面這個過程
其實就多了一個參數,然后判斷了一下,很簡單。那么我們現在來分析一下,當我們調用fulfilled狀態下的a的執行過程
setTimeout(()=>{ d = a.then(add);} ,2000) value:"1" 復制代碼- (1)先執行new MyPromise(),初始化d
- (2)然后執行 executor(resolve, reject);fn開始執行,算出新的值x
- (3)傳給d的resolve執行,
- (4)修改stauts和value的狀態
- (5)return 出新的函子,可以繼續鏈式調用
我們來想一個問題,如果(2)中fn是一個異步操作,d后邊繼續調用then方法,此刻pending狀態就不會改變,直到resolve執行。那么then的方法就會加到callback上。就又回到我們之前處理異步的狀態啦。所以這就是為什么Promise能夠解決回調地獄
參考代碼:promise5.js
好了,我們現在來看傳進去的方法fn(this.value) ,我們需要用上篇講的Maybe函子去過濾一下。
5 Maybe函子優化
then (onResolved,onRejected) {onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}return new MyPromise((resolve,reject) => {if(this.status === 'pending'){this.onResolvedCallbacks.push (()=>{let x = onResolved(this.value) resolve(x)})this.onRejectedCallbacks.push (()=>{let x = onRejected(this.reason)reject(x)})}if(this.status === 'fulfilled'){let x = onResolved(this.value)resolve(x)}if(this.status === 'rejected'){let x = onRejected(this.value)reject(x)}})} 復制代碼- Maybe函子很簡單,對onResolved和onRejected進行一下過濾
參考代碼:promise6.js
這一篇先寫到這里吧。最后總結一下,Promise的功能很強大,就是少年派的奇幻漂流一樣。雖然旅程絢爛多彩,但始終陪伴你的只有那只老虎。Promise也是一樣,只要掌握其核心函子的概念,其他問題就比較好理解啦。這里只實現了一個簡單的Promise,更強大的功能,我們慢慢加吧。
總結
以上是生活随笔為你收集整理的函数式编程之Promise的奇幻漂流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 上市公司9月23日晚间公告速递
- 下一篇: jpa+hibernate整合达梦数据库