從一次重寫原生方法遇到的坑,總結一下Web中的事件系統
寫在前面
前段時間,我寫過一篇文章 前端開發中的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() { return11111}; ReactDOM.render(, 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 ,
這裡只有乾貨,符合你的預期。
原文 : SegmentFault博客