從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

收藏待读

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

寫在前面

前段時間,我寫過一篇文章 前端開發中的Error以及異常捕獲 。 在文章中,我提到了這個問題:

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

經過不斷探索(不想再噴自己了),我找到了原因。下面一一道來。本文主要講解自己找問題原因的思路,如果想看結論和總結,請直接跳到文末。

問題復現

我是在自己以前的項目中測試 addEventListener 的重寫的。這裡直接上精簡後的問題代碼:

import React from 'react';
import ReactDOM from 'react-dom';

const nativeAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, func, options) {
    const wrappedFunc = function (...args) {
        try {
             return func.apply(this, args);
        } catch (e) {
             const errorObj = {
                 error_msg: e.message || '',
                 error_stack: e.stack || (e.error && e.error.
                 error_native: e
             };
        }
    }
    return self.nativeAddEventListener.call(this, type, wrappedFunc, options);
};

const App = function() {
    return 
11111
}; ReactDOM.render(, document.body);

運行這段代碼,瀏覽器上一片空白,但是卻沒有任何報錯。我一臉懵逼。

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

問題初探索

刪掉那一點重寫 addEventListener 的代碼後,表現符合預期了。應該是重寫那兒的問題。但是仔細看了過後,那段代碼並沒有什麼問題。並且這段代碼我在其他地方也試過,表現一直是正常的。是不是和 React 哪裡衝突了?我使用的 React 版本是

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

我搜索了 react-dom 源碼中的 addEventListener 關鍵字,總共出現了四次。初步看了一下,並沒有什麼問題,只是註冊了一些事件而已。沒有具體分析這些代碼的含義,我選擇了先更換 React 的版本試一試,於是,我換成了 15.6.2 的版本。令人吃驚的是,表現符合預期了。難道真的和 React 的版本有關係? 在我的認知中,兩個版本中最大的不同就是: React v16 採用了全新的 Fiber 架構,而我對 Fiber 的理解大概就是:重新設計了 react node 的數據結構,模擬實現了自己的任務堆棧,結合時間分片來進行任務的調度,從而更新整個系統。另外, React 有自己的一套任務系統, addEventListener 和任務也是緊密相關的,難道影響到了這個?

繼續探索

我決定從 ReactDOM.render() 這個方法入手,調試一下 ReactDOM 的源代碼。之前並沒有研究過 React 的源碼,壓力有點大。調試了一翻之後,我並沒有發現什麼問題,並且已經有點懵逼了。我準備同時調試 react v15react v16 的代碼,看看有什麼不同。為了方便,我將問題代碼全部抽了出來,全部寫到了一個 html 文件中,並且直接引用 React 的cdn地址。這個時候,我發現了一個神奇的問題:直接引用cdn地址後,不管 React 是什麼版本,就算是 v16 版本,也不會出現之前問題,表現都是符合預期的。我更加懵逼了。

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

發現問題

靜下心來仔細觀察後,我發現了,我cdn引用的都是 reactproduction 版本,而我在項目中使用的 react 代碼,卻是 development 版本的,難道是 developmentproduction 的diff代碼,導致了上面的問題。於是我重新仔細看了一下 v16development 的代碼,找到了代碼中一段長長的注釋:

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

大意就是:在開發版本中, react 不會採用 try{}catch(){} 的方式來捕獲錯誤,而是會把所有開發者定義的 callback 用一個叫做 invokeGuardedCallback 的函數包裹起來,然後使用一個假的 dom ,監聽、觸發自定義事件來執行 invokeGuardedCallback ,並且通過一個全局的錯誤捕捉函數來捕獲錯誤。

在這段注釋的下面,就是注釋中提到的 invokeGuardedCallback 的代碼。

我仔細研究了這個 invokeGuardedCallback 的代碼,其核心就是:

function invokeGuardedCallback(name, func, context, a, b, c, d, e, f){
     ...
     var fakeNode = document.createElement('react');
     var evt = document.createEvent('Event');
     var evtType = 'react-' + (name ? name : 'invokeguardedcallback');
     var callCallback = function(){
        ...
        fakeNode.removeEventListener(evtType, callCallback, false); // 這裡很重要!!!
        ...
        func.apply(context, funcArgs); // 這裡是真正執行react中的邏輯代碼
     } 
     fakeNode.addEventListener(evtType, callCallback, false);
     evt.initEvent(evtType, false, false);
     fakeNode.dispatchEvent(evt);
     ...
}

react 將所有容易出錯的函數,都用這個 invokeGuardedCallback 包了起來。每一次都重新造一個虛擬的 element ,然後監聽其自定義事件,並且立即觸發這個自定義事件。調試了這個 invokeGuardedCallback 後,我發現在 react v16 中,發現很多函數被多次執行。

為什麼會多次執行呢? 終於,我找到了問題的原因:

我重寫了 addEventListener , 在函數外包了一層 try{}catch(){} ,返回的是一個新的函數,所以,最終註冊在事件監聽器上的,並不是我傳入的那個函數。這個時候,調用 removeEventListener 時,無法移除我傳入 addEventListener 的函數。

invokeGuardedCallback 中, removeEventListener 的邏輯相當於並沒有生效。於是,在 Fiber 的調度中,某個函數被多次重複執行了,而被重複執行的函數並不是冪等的,問題便產生了。

問題的總結與思考

問題終於定位了,一句總結,就是:

重寫了 addEventListener ,卻並沒有考慮到與之對應的 removeEventListener ,導致 removeEventListener 無法正常工作。

下面是一些思考:

  1. 一開始,如果我仔細看一下 react 源碼中 addEventListener 周圍的代碼,或許能更早發現這個問題,就不用繞這麼大一個圈了。
  2. 自己對於第三方庫的 development 版本和 production 版本,並沒有一個很強烈的認知、意識,以前上線的不少項目,線上竟然還是用的第三方庫的 development 版本,這個毛病,一定得改掉。
  3. 分析問題的能力還很欠缺,不夠敏感。考慮問題的全面性需要提高。
  4. 真的不要隨便重寫原生方法。。。

寫在後面

在探索這個問題的過程中,我看到了 react 巧妙應用 自定義事件 來捕獲錯誤。於是,我全面總結一下了Web中的事件系統,也算是對基礎的鞏固。由於篇幅已經不夠了,這裡就直接放文章鏈接吧:

歡迎關注我的公眾號: 符合預期的CoyPan

這裡只有乾貨,符合你的預期。

從一次重寫原生方法遇到的坑,總結一下Web中的事件系統

原文 : SegmentFault博客

相關閱讀

免责声明:本文内容来源于SegmentFault,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。