深入源碼剖析componentWillXXX為什麼UNSAFE

2021-02-21 奇舞精選

編者按:本文作者蘇暢,奇舞團前端開發工程師。

從v16.3.0開始如下三個生命周期鉤子被標記為UNSAFE。

究其原因,有如下兩點:

本文會從React源碼的角度剖析這兩點。

同時,通過本文的學習你可以掌握React異步狀態更新機制的原理。

被誤用的鉤子

我們先來探討第一點,這裡我們以componentWillRecieveProps舉例。

我們經常在componentWillRecieveProps內處理props改變帶來的影響。有些同學認為這個鉤子會在每次props變化後觸發。

真的是這樣麼?讓我們看看源碼。

這段代碼出自updateClassInstance方法:

if (

  unresolvedOldProps !== unresolvedNewProps ||

  oldContext !== nextContext

) {

  callComponentWillReceiveProps(

    workInProgress,

    instance,

    newProps,

    nextContext,

  );

}

你可以從這裡1看到這段源碼

其中callComponentWillReceiveProps方法會調用componentWillRecieveProps。

可以看到,是否調用的關鍵是比較unresolvedOldProps與 unresolvedNewProps是否全等,以及context是否變化。

其中unresolvedOldProps為組件上次更新時的props,而unresolvedNewProps則來自ClassComponent調用this.render返回的JSX中的props參數。

可見他們的引用是不同的。所以他們全等比較為false。

基於此原因,每次父組件更新都會觸發當前組件的componentWillRecieveProps

想想你是否也曾誤用過?

模式遷移

讓我們再看第二個原因:

React從Legacy模式遷移到Concurrent模式後,這些鉤子的表現會和之前不一致。

我們先了解下什麼是模式?不同模式有什麼區別?

從Legacy到Concurrent

從React15升級為React16後,源碼改動如此之大,說React被重構可能更貼切些。

正是由於變動如此之大,使得一些特性在新舊版本React中表現不一致,這裡就包括上文談到的三個生命周期鉤子。

為了讓開發者能平穩從舊版本遷移到新版本,React推出了三個模式:

legacy模式 -- 通過ReactDOM.render創建的應用會開啟該模式。這是當前React使用的方式。這個模式可能不支持一些新功能。

blocking模式 -- 通過ReactDOM.createBlockingRoot創建的應用會開啟該模式。開啟部分concurrent模式特性,作為遷移到concurrent模式的第一步。

concurrent模式 -- 通過ReactDOM.createRoot創建的應用會開啟該模式。面向未來的開發模式。

你可以從這裡2看到不同模式的特性支持情況

concurrent模式相較我們當前使用的legacy模式最主要的區別是將同步的更新機制重構為異步可中斷的更新

接下來我們來探討React如何實現異步更新,以及為什麼異步更新情況下鉤子的表現和同步更新不同。

同步更新

我們可以用代碼版本控制類比更新機制。

在沒有代碼版本控制前,我們在代碼中逐步疊加功能。一切看起來井然有序,直到我們遇到了一個緊急線上bug(紅色節點)。

為了修復這個bug,我們需要首先將之前的代碼提交。

在React中,所有通過ReactDOM.render創建的應用都是通過類似的方式更新狀態。

即所有更新同步執行,沒有優先級概念,新來的高優更新(紅色節點)也需要排在其他更新後面執行。

異步更新

當有了代碼版本控制,有緊急線上bug需要修復時,我們暫存當前分支的修改,在master分支修復bug並緊急上線。

bug修復上線後通過git rebase命令和開發分支連接上。開發分支基於修復bug的版本繼續開發。

在React中,通過ReactDOM.createBlockingRoot和ReactDOM.createRoot創建的應用在任務未過期情況下會採用異步的方式更新狀態。

高優更新(紅色節點)中斷正在進行中的低優更新(藍色節點),先完成渲染流程。

待高優更新完成後,低優更新基於高優更新的部分或者完整結果重新更新。

深入源碼

在React源碼中,每次發起更新都會創建一個Update對象,同一組件的多個Update(如上圖所示的A -> B -> C)會以鍊表的形式保存在updateQueue中。

首先了解下他們的數據結構。

Update有很多欄位,當前我們關注如下三個欄位:

const update: Update<*> = {

  // ...省略當前不需要關注的欄位

  lane,

  payload: null,

  next: null

};

Update由createUpdate方法返回,你可以從這裡3 看到 createUpdate 的源碼

updateQueue結構如下:

const queue: UpdateQueue<State> = {

    baseState: fiber.memoizedState,

    firstBaseUpdate: null,

    lastBaseUpdate: null,

    shared: {

      pending: null,

    },

    // 其他參數省略...

};

UpdateQueue由initializeUpdateQueue方法返回,你可以從這裡4看到initializeUpdateQueue的源碼

baseState:更新基於哪個state開始。上圖中版本控制的例子中,高優bug修復後提交master,其他commit基於master分支繼續開發。這裡的master分支就是baseState。

firstBaseUpdate與lastBaseUpdate:更新基於哪個Update開始,由firstBaseUpdate開始到lastBaseUpdate結束形成鍊表。這些Update是在上次更新中由於優先級不夠被留下的,如圖中A B C。

shared.pending:本次更新的單或多個Update形成的鍊表。

其中baseUpdate + shared.pending會作為本次更新需要執行的Update。

例子

了解了數據結構,接下來我們模擬一次異步中斷更新,來揭示本文探尋的秘密 —— componentWillXXX為什麼UNSAFE。

在某個組件updateQueue中存在四個Update,其中字母代表該Update要更新的字母,數字代表該Update的優先級,數字越小優先級越高。

baseState = '';

A1 - B2 - C1 - D2

首次渲染時,優先級1。B D優先級不夠被跳過。

為了保證更新的連貫性,第一個被跳過的Update(B)及其後面所有Update會作為第二次渲染的baseUpdate,無論他們的優先級高低,這裡為B C D。

baseState: ''

Updates: [A1, C1]

Result state: 'AC'

接著第二次渲染,優先級2。

由於B在第一次渲染時被跳過,所以在他之後的C造成的渲染結果不會體現在第二次渲染的baseState中。所以baseState為A而不是上次渲染的Result state AC。這也是為了保證更新的連貫性。

baseState: 'A'

Updates: [B2, C1, D2]

Result state: 'ABCD'

我們發現,C同時出現在兩次渲染的Updates中,他代表的狀態會被更新兩次。

如果有類似的代碼:

componentWillReceiveProps(nextProps) {

    if (!this.props.includes('C') && nextProps.includes('C')) {

        // ...do something

    }

}

則很有可能被調用兩次,這與同步更新的React表現不一致!

基於以上原因,componentWillXXX被標記為UNSAFE。

總結

由於篇幅有限,本次我們只聚焦了React源碼的冰山一角。

如果想深入學習React源碼,在此向你推薦開源、嚴謹、易懂的React源碼電子書 —— React技術揭秘5

Github地址:https://github.com/BetaSu/just-react

文內連結

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1034

https://zh-hans.reactjs.org/docs/concurrent-mode-adoption.html#why-so-many-modes

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactUpdateQueue.old.js#L189

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactUpdateQueue.new.js#L157

https://react.iamkasong.com/

關於奇舞周刊

《奇舞周刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公眾號後,直接發送連結到後臺即可給我們投稿。

相關焦點

  • 深入理解 HttpSecurity【源碼篇】
    因此我們有必要從源碼的角度來理解下 HttpSecurity 到底幹了啥?這一點,小夥伴們可以回憶前面【深入理解 FilterChainProxy【源碼篇】】一文。Spring Security 過濾器鏈中的所有過濾器對象都是由 xxxConfigure 來進行配置的,這裡就是獲取這個 xxxConfigure 對象。removeConfigurer 移除一個配置對象。setSharedObject/getSharedObject 配置/獲取由多個 SecurityConfigurer 共享的對象。
  • 【Vue原理】Props - 源碼版
    專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助於理解工作原理,源碼版助於了解內部詳情,讓我們一起學習吧研究基於 Vue版本2.5.17今天記錄 Props 源碼流程,哎,這東西,就算是研究過了,也真是會隨著時間慢慢忘記的。幸好我做了詳細的文章,忘記了什麼的,回憶起來必然是很快的。
  • 類java.awt.Component
    add(Component component)          添加一個 component,其選項卡的默認值為調用 component.getName 返回的組件的名稱。add(String title, Component component)          添加具有指定選項卡標題的 component。
  • 終於,他還是對C# Span 下手了,源碼解讀和應用實踐
    ReadOnlySpan<char> value);    }StringBuilder 對 Span / ReadOnlySpan 的支持    public sealed class StringBuilder : ISerializable    {        public unsafe
  • 為什麼成功的人中意開邁銳寶XL?這篇文章為你深入剖析!
    各位好,我是邁銳寶XL駕駛者,目前行駛9個月,今天給大家帶來《為什麼成功的人中意開邁銳寶XL?這篇文章為你深入剖析!》。現今的汽車界可以說是此起彼伏,汽車企業相互競爭,互不相讓,想盡辦法討好用戶需求,車友對於購車變得斤斤計較,汽車品牌僅利用一項長處已不可保住自身的市場地位。
  • Windows XP源碼洩露丨大東話安全
    源碼是在哪裡洩露的?洩露的內容裡都有什麼?大東:別著急,我先回答你的第一個問題吧,事件起源於論壇,前幾天4chan 論壇的一名用戶發帖稱 Windows XP 源碼已被洩露,並在帖子裡面附上了一張正在解壓 Windows NT 內核源碼的截圖。
  • Android-Automotive之源碼編譯過中APK是如何打包和籤名的
    如果接觸到安卓源碼,我們更想弄清楚當我們編譯源碼過程中APK應用是如何默默的打包,使用了什麼秘鑰來籤名。本篇文章就為大家揭開這個面紗。安卓源碼編譯的三個步驟:source build/envsetup.sh;lunchxxxproject;make。籤名兩個步驟所做的事情,大家有興趣可以自行研究。這裡重點介紹make這個過程做了什麼。
  • 深度剖析github上15.1k Star項目:redux-thunk
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫日益忙碌的一周又過去了,是時候開始每周一次的總結復盤了,今天筆者就來剖析一下github中star數15.1k的開源項目redux-thunk。
  • 直播逐漸滲透各行各業,直播源碼發展趨勢如何?
    隨著直播應用和技術的演進升級,各大直播平臺通過直播源碼的方式和其他領域如電商、教育、體育、醫療等行業進行融合發展。二、直播源碼盈利模式將多樣化直播產品雖然要支出高昂的帶寬成本,但是和傳統的c端產品不一樣,直播源碼的變現速度很快,觀眾購買道具,順其自然地綁定了銀行卡或支付寶,然後向主播打賞,主播和平臺分成,並且連接了用戶的支付手段,這是現在和未來直播平臺最大的盈利方式,也是吸引大量創業者和投資人最大的引爆點。
  • 源碼資本完成人民幣四期38億新基金募集
    出資人對源碼的認可一方面來自於基金持續優秀的業績表現,人民幣基金累計退出超過60億,七家成員企業IPO,包括近期上市的理想汽車 (NASDAQ:LI)、貝殼找房(NYSE:BEKE)等;同時也源於和源碼在底層「創造持久真實價值」的價值觀層面互相認同,未來將與源碼資本一起同更多優秀的新經濟企業家一道推動產業全面升級。
  • 直播交友APP源碼開發,直播交友系統源碼搭建,直播軟體源碼開發
    直播市場競爭十分的激烈,直播行業正朝著多元化方向發展,對於很多中小型運營商來講,沒有強大的技術支持,是很難擠進這塊大市場的,於是直播系統源碼成為打造一個優質平臺的基石。那麼開發一套直播系統源碼以及搭建該怎麼做?
  • 《拜託了冰箱》:深入剖析明星的生活狀態、個人習慣
    《拜託了冰箱》作為中國的最強下飯綜藝,由何炅、王嘉爾擔任兩位MC主持,通過讓明星大咖展示冰箱並挑選大廚為其烹飪的流程,來深入剖析明星的生活狀態、個人習慣甚至是價值觀、處事觀等。這檔真人秀綜藝節目前4季的口碑也一直非常好,而《拜託了冰箱 第5季》也終於上線與大家見面了。
  • MAKE IT源夢十年源碼時代十周年勇立潮頭再出發
    據悉,2020年是源碼時代砥礪前行的第十個年頭,十載風雨創業路,源碼時代致力於打造中國高端IT教育培訓品牌,將勇於擔當、樂於奉獻、善於創新、勇往直前作為行事準則,把「讓每一名學員高薪就業」當做奮鬥目標,力求讓每一位對IT具有熱情的年輕人都有所收穫,源碼時代也希望成為中國IT教育培訓領跑者。從公司成立到而今開拓出驕人成績,源碼時代以十年奮進布局了一個「讓每一名學員高薪就業」的發展目標。
  • 鴻蒙內核源碼分析:調度機制篇
    為什麼要學這麼多的相關概念?鴻蒙的內核中 Task 和 線程 在廣義上可以理解為是一個東西,但狹義上肯定會有區別,區別在於管理體系的不同,Task是調度層面的概念,線程是進程層面概念。狹義上的後續有 鴻蒙內核源碼分析(啟動過程篇) 來說明。不知道大家有沒有這種體會,學一個東西的過程中要接觸很多新概念,尤其像 Java/android 的生態,概念賊多,很多同學都被繞在概念中出不來,痛苦不堪。那問題是為什麼需要這麼多的概念呢?舉個例子就明白了:假如您去深圳參加一個面試老闆問你哪裡人?你會說是江西人,湖南人...
  • 手機直播系統源碼搭建功能
    如今各大直播平臺為了爭奪全國已經接近4億的用戶群體可謂是使盡了渾身解數,直播系統源碼開發更是經歷了前所未有的挑戰。想把直播系統源碼搭建開發出來,絕對不是很容易的,因為直播系統源碼開發中運用到的技術難點非常多。我們在開發直播系統源碼時需要注意哪些功能呢?
  • 分析2021年一對一直播系統源碼持續火爆的優勢
    通過一對一直播系統源碼可以快速搭建和部署一對一的視頻聊天系統平臺,且一對一視頻聊天系統平臺的功能性多樣化,社交性更強,私密的直播體驗度更高。針對一對一直播系統源碼的闡述,這裡有一些源碼的優勢及火爆的原因來分享給大家。一、一對一直播系統源碼的優勢1.超低延遲直播私聊流暢,相對於一對多直播不易卡頓,為你營造零距離感。
  • 直播軟體源碼,直播中最離不開的兩個設備你知道嗎
    直播軟體源碼中為什麼要加入那麼多適配用的接口,就是為了給各個設備的順利接入做準備。直播軟體源碼1.攝像頭對女主播來說,攝像頭可以說是必備的直播設備,開啟攝像頭能有效增加女主播的關注度。目前的攝像頭以高清攝像頭為主,這類攝像頭最大的優點就是清晰度高,細節還原好,畫面看上去更真實說道圖像的處理,圖像的美顏交給直播軟體源碼來做,直播軟體源碼自帶美顏功能,能直接對圖像進行美化處理,大大增加了主播的顏值,同時也減少了主播購買美顏設備的錢
  • 搞定HashMap面試,深入講解HashMap的工作原理
    源碼終於來了,本文默認分析JDK1.8的源碼,因為網上分析1.7的已經很多了,只在必要時用1.7作為對比。這個數組在源碼中的表示是Node<K,V>[] table。*/staticfinalintTREEIFY_THRESHOLD=8;HashMap中需要剖析的內容很多,但篇幅有限,讀者耐心有限,鄙人水平有限。下面僅從hash函數、put方法、擴容過程3個最具代表性的點深入展開講解。這3點基本涵蓋了日常使用和面試的所有場面。
  • 深入剖析建模工具UML中有哪些UML圖
    深入剖析建模工具UML中有哪些UML圖 UML有哪些圖你是否熟悉,本文就向大家簡單介紹一下,UML圖形可分為五類,共有九種圖形,希望通過本文的學習你對UML圖有一定的認識。
  • vue的$nextTick的使用+源碼分析
    但是這裡有一個很有趣的事,大家先想一想,我們說了vue是響應式的,那當我們this.a做重新賦值的時候是不是就把a的值進行修改了,那修改了是不是就應該要觸發頁面的更新,把最新的值顯示到頁面上去,按理來說應該會更新三次對吧,因為我們先修改了a,然後修改b,最後修改的是c,但是結果告訴我們頁面只更新了一次,為什麼呢?