面試官眼中的Promise

Maxliu發表於2018-12-28

說明

本文假設你有一定的Promise基礎知識,不涉及api的講解,但是對你深入理解Promise有一定益處。

寫在前面

在公司頂過幾天面試官,一道手寫Promise就卡主了不少人(受困於這道題的人別打我。。。我是不會告訴你我就職的公司的),其實這道題的主要目的是考察對Promise的理解,順便的才是考察js邏輯,寫出來是加分項,能表達出你對Promise的理解才是最重要的。

但是現實情況是有挺多人直接白卷,把加分項變成了減分項。寫不寫是態度問題,寫出幾個點已經足以讓面試官高看你一眼。下面我就把面試官希望看到幾個點拆解出來,以面試題的方式去理解Promise,希望對你們有所幫助。

不想看長篇大論的可以直接檢視總結:總結


Promise 特性

Promise捕獲錯誤與 try catch 等同

  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    throw Error('sync error')
})
    .then(res => {
        console.log(res)
    })
    .catch(err => {
        console.log(err)
    })
複製程式碼

2.請寫出下列程式碼的輸出

var p1 = new Promise(function(resolve, reject) {
    setTimeout(() => {
        throw Error('async error')   
    })
})
    .then(res => {
        console.log(res)
    })
    .catch(err => {
        console.log(err)
    })
複製程式碼
  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    resolve()
})
    .then(res => {
        throw Error('sync error') 
    })
複製程式碼

錯誤三連,你知道正確答案嗎??

正確答案是:

  1. Error被catch到,最後console.log輸出
  2. 錯誤無法被catch,控制檯報錯
  3. promise沒有catch,錯誤被捕獲後又被丟擲,控制檯報錯

這裡考查的主要是Promise的錯誤捕獲,其實仔細想想js中能用的錯誤捕獲也只能是try catch了,而try catch只能捕獲同步錯誤,並且在沒有傳入錯誤監聽的時候會將捕獲到的錯誤丟擲。

所以在手寫promise中,你至少要寫出try catch包裹回撥代調

    function Promise(fn) {
        ...
        doResolve(fn, this)
    }
    
    function doResolve(fn, self) {
        try {
            fn(function(value) {
                ...
            },
            function(reason) {
                ...
            })
        } catch(err) {
            reject(self, err)
        }
    }
    
    Promise.prototype.then = function(onFulfilled, onRejected) {
        try {
            ...
            onFulfilled(value)
        } catch(err) {
            reject(err)
        }
    };
    
    function reject(self, newValue) {
        ...
        if (!self._handled) {
            Promise._unhandledRejectionFn(self._value);
        }
    }

複製程式碼

Promise 擁有狀態變化

把上面的面試題改寫一下:

  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    resolve(1)
    throw Error('sync error')
})
    .then(res => {
        console.log(res)
    })
    .catch(err => {
        console.log(err)
    })
複製程式碼
  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    reject(2)
    resolve(1)
})
    .then(res => {
        console.log(res)
    })
    .catch(err => {
        console.log(err)
    })
複製程式碼
  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    resolve(1)
})
    .then(res => {
        throw Error('sync error')
        console.log(res)
    })
    .catch(err => {
        console.log(err)
    })
複製程式碼

正確答案是:

  1. 輸出 1
  2. 輸出 2
  3. console.log輸出錯誤

Promise是一個有狀態的容器,當狀態被凝固了,後面的resolve或reject就不會被觸發。簡單的說就是同一個Promise只能觸發一個狀態監聽(onFulfilled或onRejected)。所以在手寫Promise中需要有一個狀態標記:

    function Promise(fn) {
        ...
        this._state = 0 // 狀態標記
        doResolve(fn, this)
    }
    
    function doResolve(fn, self) {
        var done = false // 保證只執行一個監聽
        try {
            fn(function(value) {
                if (done) return
                done = true
                resolve(self, value)
            },
            function(reason) {
                if (done) return;
                done = true
                reject(self, value)
            })
        } catch(err) {
            if (done) return
            done = true
            reject(self, err)
        }
    }
    
    function resolve(self, newValue) {
        try {
            self._state = 1;
            ...
        }
        catch(err) {
            reject(self, err)
        }
    }
    
    function reject(self, newValue) {
        self._state = 2;
        ...
        if (!self._handled) {
            Promise._unhandledRejectionFn(self._value);
        }
    }
複製程式碼

Promise 方法中的回撥是非同步的

  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    resolve()
    setTimeout(() => {
        console.log(1)
    })
    console.log(2)
})
    .then(res => {
        console.log(3)
    })
console.log(4)
複製程式碼

正確答案是:

依次輸出:

2 
4
3
1
複製程式碼

2 
4
1
3
複製程式碼

首先 promise 中then、catch、finally中的回撥都是非同步執行的,所以前面輸出2 4 的同步程式碼是沒有疑問的。

那為什麼兩種答案都認為是對的呢,其實是因為polyfill的鍋。正確的Promise輸出應該是 2 4 3 1,原因在於Promise.then是微任務執行的,微任務優先於巨集任務執行(setTimeout就是巨集任務)。

但是在polyfill中,瀏覽器環境是沒法主動註冊微任務的,所以同樣是使用setTimeout呼叫then中的fn,同樣是巨集任務的情況下就只是佇列的先進先出原則了,那麼在promise-polyfill環境中輸出 2 4 1 3也認為是正確的。

那麼手寫Promise中,應該將resolve,reject回撥設為非同步:


function handle(self, deferred) {
    ...
    setTimeout(function() {
        var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
        if (cb === null) {
            (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
            return;
        }
        var ret;
        try {
            ret = cb(self._value);
        } catch (e) {
            reject(deferred.promise, e);
            return;
        }
        resolve(deferred.promise, ret);
    }, 0)
    ...
}

複製程式碼

Promise 會儲存返回值

  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    reject(1)
})
    .catch(err => {
        console.log(err)
        return 2
    })

setTimeout(() => {
    p1
        .then(res => console.log(res))
}, 1000)
複製程式碼

正確答案是:

先輸出 1

1秒後輸出 2

Promise會將最後的值儲存起來,如果在下次使用promise方法的時候回直接返回該值的promise。

所以手寫一個Promise,你應該儲存返回值:

    function Promise(fn) {
        ...
        this._state = 0 // 狀態標記
        this._value = undefined; // 儲存返回值
        doResolve(fn, this)
    }
    
    function resolve(self, newValue) {
        try {
            ...
            if (newValue instanceof Promise) {
                self._state = 3;
                self._value = newValue;
                finale(self);
                return;
            } else if (typeof then === 'function') {
                doResolve(bind(then, newValue), self);
                return;
            }
            self._state = 1;
            self._value = newValue;
            ...
        }
        catch(err) {
            reject(self, err)
        }
    }
    
    function reject(self, newValue) {
        self._state = 2;
        self._value = newValue;
        ...
        if (!self._handled) {
            Promise._unhandledRejectionFn(self._value);
        }
    }
複製程式碼

Promise 方法每次都返回一個新的Promise

  1. 請寫出下列程式碼的輸出
var p1 = new Promise(function(resolve, reject) {
    reject(1)
})
    .then(
        res => {
            console.log(res)
            return 2
        },
        err => {
            console.log(err)
            return 3
        }
    )
    .catch(err => {
        console.log(err)
        return 4
    })
    .finally(res => {
        console.log(res)
        return 5
    })
    .then(
        res => console.log(res),
        err => console.log(err)
    )
複製程式碼

正確答案是:

依次輸出:

1
undefined
3
複製程式碼

Promise能夠鏈式呼叫的原因是它的每一個方法都返回新的promise,哪怕是finally方法,特殊的是finlly會返回上一個promise的值包裝成的新promise,並且finally也不接收引數,因為無論Promise是reject還是fulfill它都會被呼叫。

所以你需要在promise方法中返回新的promise:

function bind(fn, thisArg) {
  return function() {
    fn.apply(thisArg, arguments);
  };
}

function resolve(self, newValue) {
    ...
    try {
        if (newValue instanceof Promise) {
            self._state = 3;
            self._value = newValue;
            finale(self);
            return;
        } else if (typeof then === 'function') {
            doResolve(bind(then, newValue), self);
            return;
        }
        self._state = 1;
        ...
    } catch (e) {
        reject(self, e);
    }
}
複製程式碼

總結

上述總共表達了五個Promise知識點:

  1. Promise捕獲錯誤與 try catch 等同
  2. Promise 擁有狀態變化
  3. Promise 方法中的回撥是非同步的
  4. Promise 方法每次都返回一個新的Promise
  5. Promise 會儲存返回值

文中案例皆取自 promise-polyfill,有美玉在前,作者就不亮出自己的板磚了,同時也提醒各位面試者多看優秀作品的原始碼,何必看那些不太正規的第三方的實現。

畢竟公司的目標不是造重複的輪子,如果你已經能清晰明瞭地表述出上述部分知識,我們就能相信你已經是一個能夠正確並靈活使用Promise的開發者了,及格分雙手奉上(以我們公司的招聘目標為例,相信大部分公司要求也是如此)。

最後:

馬上快到2019年了,祝大家都能找到稱心如意的工作!???

-- The End

相關文章