1. 程式人生 > >Javascript splice的遭坑小記

Javascript splice的遭坑小記

在 Javascript 中 Array.splice 方法是個很強大的方法,很多時候我們可以用它來刪除陣列中的某一個元素(有別於 delete,delete刪除會在陣列中留下一個空洞而splice不會,用 splice 更像是從連結串列中移除某個節點)。

今天遇到的坑簡單來說是這樣的,在 UI 相關的程式設計中如果有某處的互動很多,互動的組合關係也很複雜的時候,我們往往會採用“資料驅動”的方式來進行程式碼編寫,通過合理的資料結構設計加上“被動檢視”來降低複雜度和提高程式碼質量。

今天的問題也是由此引出的,我們有一塊業務也是類似的方式處理的,具體的行駛為trigger-on 的模型,view 去監聽資料的變化,資料有變化的時候通知所有的觀察者。簡化後的業務程式碼如下:

function Info() {
    var events = {}
    var _this = this

    this.on = function (evt, handler) {
        events[evt] || (events[evt] = [])
        events[evt].push(handler)
    }

    this.once = function (evt, handler) {
        events[evt] || (events[evt] = [])
        function _once() {
            handler()
            _this.
off(evt, _once) } events[evt].push(_once) } this.off = function (evt, handler) { var handlers = events[evt] handlers.forEach(function (_handler, i) { if (_handler === handler) { handlers.splice(i, 1) } }) } this.
trigger = function (evt) { var i = 0, handlers = events[evt], len = handlers.length for (; i < len; i++) { handlers[i] && handlers[i]() } } }

程式碼的邏輯很簡單,我就不多做說明了,但是測試的過程中,發現了一件詭異的事情,某些時候監聽了 change 事件的函式並沒有被觸發,但並不是每次都能復現(大家都知道,修復不能 100% 復現的 bug 是多麼痛苦),沒有辦法,問題上報到我這,我只能去翻看了一下內部實現,剝去所有業務程式碼後,抽出了上面的核心程式碼,然後寫了一個正常和不正常的版本的 case 給小夥伴。

// 正常的程式碼
var info = new Info()
info.on('change', function () {
    console.log('change')
})
info.once('change', function () {
    console.log('change')
})
info.trigger('change')
info.trigger('change')
// 不正常的程式碼
var info = new Info()
info.once('change', function () {
    console.log('change')
})
info.on('change', function () {
    console.log('change')
})
info.trigger('change')
info.trigger('change')

以上的程式碼,按照邏輯來說,會列印三次 change,兩次由 on 分別列印,一次由 once 列印,但不正常的程式碼只會列印兩次。

如果單獨看每個函式,似乎都沒有問題,但留意 once 這個方法,它和 on 唯一的區別在於它自己就會呼叫一次 off,而 off 方法使用的 splice 會改變原陣列的長度。再結合 trigger 的實現,你會發現,只要 once 方法先觸發,陣列的長度就會減1,被刪除的元素就會“掉落”到刪除元素的位置,而 for 迴圈是以 index 為準的,自然就跳過了該位置,那麼緊跟著 once 後面的 on 是不會被觸發的。

所以,使用 splice 的過程中,一定要反覆提醒自己,splice 可能會改變陣列長度,而那些依賴陣列長度的程式碼都應該要仔細檢查。

修復的方法其實也很簡單,就是把 for 迴圈倒過來寫~

for (i = 0; i < len; i++) ===> for (i = len; i >=0; i--)

當然了,雖然 splice 有這樣的坑,坑也不一定就不能用來做好事。例如,我們可以利用 splice 的特性來為陣列增加一個 half 方法,可以指定是移除奇數位還是偶數位的元素

Array.prototype.half = function (even) {
    var i = even ? 0 : 1
    var len = this.length

    for (;i < len; i++) {
        this.splice(i, 1)
    }
}

當然,可能還有其它的用處,我這裡就拋磚引玉,歡迎大家補充。