寫在前面
前段時間,我寫過一篇文章前端開發中的Error以及異常捕獲。 在文章中,我提到了這個問題:
經過不斷探索(不想再噴自己了),我找到了原因。下面一一道來。本文主要講解自己找問題原因的思路,如果想看結論和總結,請直接跳到文末。
問題復現
我是在自己以前的項目中測試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 <div>11111</div>
};
ReactDOM.render(<App/>, document.body);
運行這段代碼,瀏覽器上一片空白,但是卻沒有任何報錯。我一臉懵逼。
問題初探索
刪掉那一點重寫addEventListener
的代碼後,表現符合預期了。應該是重寫那兒的問題。但是仔細看了過後,那段代碼並沒有什麼問題。並且這段代碼我在其他地方也試過,表現一直是正常的。是不是和React
哪裏衝突了?我使用的React
版本是
我搜索了react-dom
源碼中的addEventListener
關鍵字,總共出現了四次。初步看了一下,並沒有什麼問題,只是註冊了一些事件而已。沒有具體分析這些代碼的含義,我選擇了先更換React
的版本試一試,於是,我換成了15.6.2
的版本。令人吃驚的是,表現符合預期了。難道真的和React
的版本有關係? 在我的認知中,兩個版本中最大的不同就是:React v16
採用了全新的Fiber
架構,而我對Fiber
的理解大概就是:重新設計了react node
的數據結構,模擬實現了自己的任務堆棧,結合時間分片來進行任務的調度,從而更新整個系統。另外,React
有自己的一套任務系統,addEventListener
和任務也是緊密相關的,難道影響到了這個?
繼續探索
我決定從ReactDOM.render()
這個方法入手,調試一下ReactDOM
的源代碼。之前並沒有研究過React
的源碼,壓力有點大。調試了一翻之後,我並沒有發現什麼問題,並且已經有點懵逼了。我準備同時調試react v15
和react v16
的代碼,看看有什麼不同。爲了方便,我將問題代碼全部抽了出來,全部寫到了一個html
文件中,並且直接引用React
的cdn地址。這個時候,我發現了一個神奇的問題:直接引用cdn地址後,不管React
是什麼版本,就算是v16
版本,也不會出現之前問題,表現都是符合預期的。我更加懵逼了。
發現問題
靜下心來仔細觀察後,我發現了,我cdn引用的都是react
的production
版本,而我在項目中使用的react
代碼,卻是development
版本的,難道是development
和production
的diff代碼,導致了上面的問題。於是我重新仔細看了一下v16
的development
的代碼,找到了代碼中一段長長的註釋:
大意就是:在開發版本中,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
無法正常工作。
下面是一些思考:
- 一開始,如果我仔細看一下
react
源碼中addEventListener
周圍的代碼,或許能更早發現這個問題,就不用繞這麼大一個圈了。 - 自己對於第三方庫的
development
版本和production
版本,並沒有一個很強烈的認知、意識,以前上線的不少項目,線上竟然還是用的第三方庫的development
版本,這個毛病,一定得改掉。 - 分析問題的能力還很欠缺,不夠敏感。考慮問題的全面性需要提高。
- 真的不要隨便重寫原生方法。。。
寫在後面
在探索這個問題的過程中,我看到了react
巧妙應用自定義事件來捕獲錯誤。於是,我全面總結一下了Web中的事件系統,也算是對基礎的鞏固。由於篇幅已經不夠了,這裏就直接放文章鏈接吧:
歡迎關注我的公衆號: 符合預期的CoyPan,
這裏只有乾貨,符合你的預期。