<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          「React進(jìn)階」探案揭秘六種React‘靈異’現(xiàn)象

          共 29456字,需瀏覽 59分鐘

           ·

          2021-05-22 17:27

          前言

          今天我們來(lái)一期不同尋常的React進(jìn)階文章,本文我們通過(guò)一些不同尋常的現(xiàn)象,以探案的流程分析原因,找到結(jié)果,從而認(rèn)識(shí)React,走進(jìn)React的世界,揭開React的面紗,我深信,更深的理解,方可更好的使用。

          我承認(rèn)起這個(gè)名字可能有點(diǎn)標(biāo)題黨了,靈感來(lái)源于小時(shí)候央視有一個(gè)叫做《走進(jìn)科學(xué)》的欄目,天天介紹各種超自然的靈異現(xiàn)象,搞的神乎其神,最后揭秘的時(shí)候原來(lái)是各種小兒科的問題,現(xiàn)在想想都覺得搞笑????。但是我今天介紹的這些React '靈異'現(xiàn)象本質(zhì)可不是小兒科,每一個(gè)現(xiàn)象后都透露出 React 運(yùn)行機(jī)制設(shè)計(jì)原理。(我們講的react版本是16.13.1

          src=http___n.sinaimg.cn_sinacn_w640h360_20180113_9984-fyqrewh6822097.jpg&refer=http___n.sinaimg.jpg

          好的,廢話不多說(shuō),我的大偵探們,are you ready ? 讓我們開啟今天的揭秘之旅把。

          案件一:組件莫名其妙重復(fù)掛載

          接到報(bào)案

          之前的一位同學(xué)遇到一個(gè)詭異情況,他希望在組件更新,componentDidUpdate執(zhí)行后做一些想要做的事,組件更新源來(lái)源于父組件傳遞 props 的改變。但是父組件改變 props發(fā)現(xiàn)視圖渲染,但是componentDidUpdate沒有執(zhí)行,更怪異的是componentDidMount執(zhí)行。代碼如下:

          // TODO: 重復(fù)掛載
          class Index extends React.Component{
             componentDidMount(){
               console.log('組件初始化掛載')
             }
             componentDidUpdate(){
               console.log('組件更新')
               /* 想要做一些事情 */
             }
             render(){
                return <div>《React進(jìn)階實(shí)踐指南》  ?? { this.props.number } +   </div>
             }
          }

          效果如下

          didupdate.gif

          componentDidUpdate沒有執(zhí)行,componentDidMount執(zhí)行,說(shuō)明組件根本沒有走更新邏輯,而是走了重復(fù)掛載。

          逐一排查

          子組件一頭霧水,根本不找原因,我們只好從父組件入手。讓我們看一下父組件如何寫的。

          const BoxStyle = ({ children })=><div className='card' >{ children }</div>

          export default function Home(){
             const [ number , setNumber ] = useState(0)
             const NewIndex = () => <BoxStyle><Index number={number}  /></BoxStyle>
             return <div>
                <NewIndex  />
                <button onClick={ ()=>setNumber(number+1) } >點(diǎn)贊</button>
             </div>

          }

          從父組件中找到了一些端倪。在父組件中,首先通過(guò)BoxStyle做為一個(gè)容器組件,添加樣式,渲染我們的子組件Index,但是每一次通過(guò)組合容器組件形成一個(gè)新的組件NewIndex,真正掛載的是NewIndex,真相大白。

          注意事項(xiàng)

          造成這種情況的本質(zhì),是每一次 render 過(guò)程中,都形成一個(gè)新組件,對(duì)于新組件,React 處理邏輯是直接卸載老組件,重新掛載新組件,所以我們開發(fā)的過(guò)程中,注意一個(gè)問題那就是:

          • 對(duì)于函數(shù)組件,不要在其函數(shù)執(zhí)行上下文中聲明新組件并渲染,這樣每次函數(shù)更新會(huì)促使組件重復(fù)掛載。
          • 對(duì)于類組件,不要在 render 函數(shù)中,做如上同樣的操作,否則也會(huì)使子組件重復(fù)掛載。

          案件二:事件源e.target離奇失蹤

          突發(fā)案件

          化名(小明)在一個(gè)月黑風(fēng)高的夜晚,突發(fā)奇想寫一個(gè)受控組件。寫的什么內(nèi)容具體如下:

          export default class EventDemo extends React.Component{
            constructor(props){
              super(props)
              this.state={
                  value:''
              }
            }
            handerChange(e){
              setTimeout(()=>{
                 this.setState({
                   value:e.target.value
                 })
              },0)
            }
            render(){
              return <div>
                <input placeholder="請(qǐng)輸入用戶名?" onChange={ this.handerChange.bind(this) }  />
              </div>

            }
          }

          input的值受到 statevalue屬性控制,小明想要通過(guò)handerChange改變value值,但是他期望在setTimeout中完成更新。可以當(dāng)他想要改變input值時(shí)候,意想不到的事情發(fā)生了。

          event.jpg

          控制臺(tái)報(bào)錯(cuò)如上所示。Cannot read property 'value' of null 也就是說(shuō)明e.targetnull。事件源 target怎么說(shuō)沒就沒呢?

          線索追蹤

          接到這個(gè)案件之后,我們首先排查問題,那么我們先在handerChange直接打印e.target,如下:

          event1.jpg

          看來(lái)首先排查不是 handerChange 的原因,然后我們接著在setTimeout中打印發(fā)現(xiàn):

          event2.jpg

          果然是setTimeout的原因,為什么setTimeout中的事件源 e.target 就莫名的失蹤了呢? 首先,事件源肯定不是莫名的失蹤了,肯定 React 底層對(duì)事件源做了一些額外的處理,首先我們知道React采用的是事件合成機(jī)制,也就是綁定的 onChange不是真實(shí)綁定的 change事件,小明綁定的 handerChange也不是真正的事件處理函數(shù)。那么也就是說(shuō)React底層幫我們處理了事件源。這一切可能只有我們從 React 源碼中找到線索。經(jīng)過(guò)對(duì)源碼的排查,我發(fā)現(xiàn)有一處線索十分可疑。

          react-dom/src/events/DOMLegacyEventPluginSystem.js


          function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
              const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
              batchedEventUpdates(handleTopLevel, bookKeeping);
          }

          dispatchEventForLegacyPluginEventSystemlegacy模式下,所有事件都必定經(jīng)過(guò)的主要函數(shù),batchedEventUpdates是處理批量更新的邏輯,里面會(huì)執(zhí)行我們真正的事件處理函數(shù),我們?cè)谑录砥轮v過(guò) nativeEvent 就是真正原生的事件對(duì)象 event。targetInst 就是e.target對(duì)應(yīng)的fiber對(duì)象。我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">handerChange里面獲取的事件源是React合成的事件源,那么了解事件源是什么時(shí)候,怎么樣被合成的? 這對(duì)于破案可能會(huì)有幫助。

          事件原理篇我們將介紹React采用事件插件機(jī)制,比如我們的onClick事件對(duì)應(yīng)的是 SimpleEventPlugin,那么小明寫onChange也有專門 ChangeEventPlugin事件插件,這些插件有一個(gè)至關(guān)重要的作用就是用來(lái)合成我們事件源對(duì)象e,所以我們來(lái)看一下ChangeEventPlugin

          react-dom/src/events/ChangeEventPlugin.js

          const ChangeEventPlugin ={
             eventTypes: eventTypes,
             extractEvents:function(){
                  const event = SyntheticEvent.getPooled(
                      eventTypes.change,
                      inst, // 組件實(shí)例
                      nativeEvent, // 原生的事件源 e
                      target,      // 原生的e.target
               );
               accumulateTwoPhaseListeners(event); // 這個(gè)函數(shù)按照冒泡捕獲邏輯處理真正的事件函數(shù),也就是  handerChange 事件
               return event; // 
             }   
          }

          我們看到合成事件的事件源handerChange中的 e,就是SyntheticEvent.getPooled創(chuàng)建出來(lái)的。那么這個(gè)是破案的關(guān)鍵所在。

          legacy-events/SyntheticEvent.js

          SyntheticEvent.getPooled = function(){
              const EventConstructor = this//  SyntheticEvent
              if (EventConstructor.eventPool.length) {
              const instance = EventConstructor.eventPool.pop();
              EventConstructor.call(instance,dispatchConfig,targetInst,nativeEvent,nativeInst,);
              return instance;
            }
            return new EventConstructor(dispatchConfig,targetInst,nativeEvent,nativeInst,);
          }

          番外:在事件系統(tǒng)篇章,文章的事件池感念,講的比較倉(cāng)促,籠統(tǒng),這篇這個(gè)部分將詳細(xì)補(bǔ)充事件池感念。

          getPooled引出了事件池的真正的概念,它主要做了兩件事:

          • 判斷事件池中有沒有空余的事件源,如果有取出事件源復(fù)用。
          • 如果沒有,通過(guò) new SyntheticEvent 的方式創(chuàng)建一個(gè)新的事件源對(duì)象。那么 SyntheticEvent就是創(chuàng)建事件源對(duì)象的構(gòu)造函數(shù),我們一起研究一下。
          const EventInterface = {
            typenull,
            targetnull,
            currentTargetfunction({
              return null;
            },
            eventPhasenull,
            ...
          };
          function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
            this.dispatchConfig = dispatchConfig; 
            this._targetInst = targetInst;    // 組件對(duì)應(yīng)fiber。
            this.nativeEvent = nativeEvent;   // 原生事件源。
            this._dispatchListeners = null;   // 存放所有的事件監(jiān)聽器函數(shù)。
            for (const propName in Interface) {
                if (propName === 'target') {
                  this.target = nativeEventTarget; // 我們真正打印的 target 是在這里
                } else {
                  this[propName] = nativeEvent[propName];
                }
            }
          }
          SyntheticEvent.prototype.preventDefault = function ()/* .... */ }     /* 組件瀏覽器默認(rèn)行為 */
          SyntheticEvent.prototype.stopPropagation = function (/* .... */  }  /* 阻止事件冒泡 */

          SyntheticEvent.prototype.destructor = function ()/* 情況事件源對(duì)象*/
                for (const propName in Interface) {
                     this[propName] = null
                }
              this.dispatchConfig = null;
              this._targetInst = null;
              this.nativeEvent = null;
          }
          const EVENT_POOL_SIZE = 10/* 最大事件池?cái)?shù)量 */
          SyntheticEvent.eventPool = [] /* 綁定事件池 */
          SyntheticEvent.release=function ()/* 清空事件源對(duì)象,如果沒有超過(guò)事件池上限,那么放回事件池 */
              const EventConstructor = this
              event.destructor();
              if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
                 EventConstructor.eventPool.push(event);
              }
          }

          我把這一段代碼精煉之后,真相也就漸漸浮出水面了,我們先來(lái)看看 SyntheticEvent 做了什么:

          • 首先賦予一些初始化的變量nativeEvent等。然后按照 EventInterface 規(guī)則把原生的事件源上的屬性,復(fù)制一份給React 事件源。然后一個(gè)重要的就是我們打印的e.target就是this.target,在事件源初始化的時(shí)候綁定了真正的e.target->nativeEventTarget

          • 然后React事件源,綁定了自己的阻止默認(rèn)行為preventDefault,阻止冒泡stopPropagation等方法。但是這里有一個(gè)重點(diǎn)方法就destructor,這個(gè)函數(shù)置空了React自己的事件源對(duì)象。那么我們終于找到了答案,我們的事件源e.target消失大概率就是因?yàn)檫@個(gè)destructordestructorrelease中被觸發(fā),然后將事件源放進(jìn)事件池,等待下一次復(fù)用。

          現(xiàn)在所有的矛頭都指向了release,那么release是什么時(shí)候觸發(fā)的呢?

          legacy-events/SyntheticEvent.js

          function executeDispatchesAndRelease(){
              event.constructor.release(event);
          }

          當(dāng) React 事件系統(tǒng)執(zhí)行完所有的 _dispatchListeners,就會(huì)觸發(fā)這個(gè)方法 executeDispatchesAndRelease釋放當(dāng)前的事件源。

          真相大白

          回到小明遇到的這個(gè)問題,我們上面講到,React最后會(huì)同步的置空事件源,然后放入事件池,因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">setTimeout是異步執(zhí)行,執(zhí)行時(shí)候事件源對(duì)象已經(jīng)被重置并釋放會(huì)事件池,所以我們打印 e.target = null,到此為止,案件真相大白。

          通過(guò)這個(gè)案件我們明白了 React 事件池的一些概念:

          • React 事件系統(tǒng)有獨(dú)特合成事件,也有自己的事件源,而且還有對(duì)一些特殊情況的處理邏輯,比如冒泡邏輯等。
          • React 為了防止每次事件都創(chuàng)建事件源對(duì)象,浪費(fèi)性能,所以引入了事件池概念,每一次用戶事件都會(huì)從事件池中取出一個(gè)e,如果沒有,就創(chuàng)建一個(gè),然后賦值事件源,等到事件執(zhí)行之后,重置事件源,放回事件池,借此做到復(fù)用。

          用一幅流程圖表示:

          eventloop.jpg

          案件三:真假React

          案發(fā)現(xiàn)場(chǎng)

          這個(gè)是發(fā)生在筆者身上的事兒,之前在開發(fā) React 項(xiàng)目時(shí)候,為了邏輯復(fù)用,我把一些封裝好的自定義 Hooks 上傳到公司私有的 package 管理平臺(tái)上,在開發(fā)另外一個(gè) React 項(xiàng)目的時(shí)候,把公司的包下載下來(lái),在組件內(nèi)部用起來(lái)。代碼如下:

          function Index({classes, onSubmit, isUpgrade}{
             /* useFormQueryChange 是筆者寫好的自定義hooks,并上傳到私有庫(kù),主要是用于對(duì)表單控件的統(tǒng)一管理  */
            const {setFormItem, reset, formData} = useFormQueryChange()
            React.useEffect(() => {
              if (isUpgrade)  reset()
            }, [ isUpgrade ])
            return <form
              className={classes.bootstrapRoot}
              autoComplete='off'
            >

              <div className='btnbox' >
                 { /* 這里是業(yè)務(wù)邏輯,已經(jīng)省略 */ }
              </div>
            </form>

          }

          useFormQueryChange 是筆者寫好的自定義 hooks ,并上傳到私有庫(kù),主要是用于對(duì)表單控件的統(tǒng)一管理,沒想到引入就直接爆紅了。錯(cuò)誤內(nèi)容如下:

          hooks.jpg

          逐一排查

          我們按照 React 報(bào)錯(cuò)的內(nèi)容,逐一排查問題所在:

          • 第一個(gè)可能報(bào)錯(cuò)原因 You might have mismatching versions of React and the renderer (such as React DOM),意思是 ReactReact Dom版本不一致,造成這種情況,但是我們項(xiàng)目中的 ReactReact Dom 都是 v16.13.1,所以排除這個(gè)的嫌疑。

          • 第二個(gè)可能報(bào)錯(cuò)原因 You might be breaking the Rules of Hooks 意思是你打破了Hooks 規(guī)則,這種情況也是不可能的,因?yàn)楣P者代碼里沒有破壞hoos規(guī)則的行為。所以也排除嫌疑。

          • 第三個(gè)可能報(bào)錯(cuò)原因You might have more than one copy of React in the same app 意思是在同一個(gè)應(yīng)用里面,可能有多個(gè) React。目前來(lái)看所有的嫌疑都指向第三個(gè),首先我們引用的自定義 hooks,會(huì)不會(huì)內(nèi)部又存在一個(gè)React 呢?

          按照上面的提示我排查到自定義hooks對(duì)應(yīng)的node_modules中果然存在另外一個(gè)React,是這個(gè)假React(我們姑且稱之為假React)搞的鬼。我們?cè)贖ooks原理 文章中講過(guò),React HooksReactCurrentDispatcher.current 在組件初始化,組件更新階段賦予不同的hooks對(duì)象,更新完畢后賦予ContextOnlyDispatcher,如果調(diào)用這個(gè)對(duì)象下面的hooks,就會(huì)報(bào)如上錯(cuò)誤,那么說(shuō)明了這個(gè)錯(cuò)誤是因?yàn)槲覀冞@個(gè)項(xiàng)目,執(zhí)行上下文引入的React是項(xiàng)目本身的React,但是自定義Hooks引用的是假React Hooks中的ContextOnlyDispatcher

          接下來(lái)我看到組件庫(kù)中的package.json中,

          "dependencies": {
            "react""^16.13.1",
            "react-dom""^16.13.1"
          },

          原來(lái)是React作為 dependencies所以在下載自定義Hooks的時(shí)候,把React又下載了一遍。那么如何解決這個(gè)問題呢。對(duì)于封裝React組件庫(kù),hooks庫(kù),不能用 dependencies,因?yàn)樗鼤?huì)以當(dāng)前的dependencies為依賴下載到自定義hooks庫(kù)下面的node_modules中。取而代之的應(yīng)該用peerDependencies,使用peerDependencies,自定義hooks再找相關(guān)依賴就會(huì)去我們的項(xiàng)目的node_modules中找,就能根本上解決這個(gè)問題。 所以我們這么改

          "peerDependencies": {
              "react"">=16.8",
              "react-dom"">=16.8",
          },

          就完美的解決了這個(gè)問題。

          撥開迷霧

          這個(gè)問題讓我們明白了如下:

          • 對(duì)于一些hooks庫(kù),組件庫(kù),本身的依賴,已經(jīng)在項(xiàng)目中存在了,所以用peerDependencies聲明。

          • 在開發(fā)的過(guò)程中,很可能用到不同版本的同一依賴,比如說(shuō)項(xiàng)目引入了 A 版本的依賴,組件庫(kù)引入了 B 版本的依賴。那么這種情況如何處理呢。在 package.json 文檔中提供了一個(gè)resolutions配置項(xiàng)可以解決這個(gè)問題,在 resolutions 中鎖定同一的引入版本,這樣就不會(huì)造成如上存在多個(gè)版本的項(xiàng)目依賴而引發(fā)的問題。

          項(xiàng)目package.json這么寫

          {
            "resolutions": {
              "react""16.13.1",
              "react-dom""16.13.1"
            },
          }

          這樣無(wú)論項(xiàng)目中的依賴,還是其他庫(kù)中依賴,都會(huì)使用統(tǒng)一的版本,從根本上解決了多個(gè)版本的問題。

          案件四:PureComponet/memo功能失效問題

          案情描述

          在 React 開發(fā)的時(shí)候,但我們想要用 PureComponent 做性能優(yōu)化,調(diào)節(jié)組件渲染,但是寫了一段代碼之后,發(fā)現(xiàn) PureComponent 功能竟然失效了,具體代碼如下:


          class Index extends React.PureComponent{
             render(){
               console.log('組件渲染')
               const { name , type } = this.props
               return <div>
                 hello , my name is { name }
                 let us learn { type }
               </div>

             }
          }

          export default function Home (){
             const [ number , setNumber  ] = React.useState(0)
             const [ type , setType ] = React.useState('react')
             const changeName = (name) => {
                 setType(name)
             }
             return <div>
                 <span>{ number }</span><br/>
                 <button onClick={ ()=> setNumber(number + 1) } >change number</button>
                 <Index type={type}  changeType={ changeName } name="alien"  />
             </div>

          }

          我們本來(lái)期望:

          • 對(duì)于 Index 組件,只有propsnametype改變,才促使組件渲染。但是實(shí)際情況卻是這樣:

          點(diǎn)擊按鈕效果:

          purecomponent.gif

          水落石出

          為什么會(huì)出現(xiàn)這種情況呢? 我們?cè)倥挪橐幌?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Index組件,發(fā)現(xiàn) Index 組件上有一個(gè) changeType,那么是不是這個(gè)的原因呢? 我們來(lái)分析一下,首先狀態(tài)更新是在父組件 Home上,Home組件更新每次會(huì)產(chǎn)生一個(gè)新的changeName,所以IndexPureComponent每次會(huì)淺比較,發(fā)現(xiàn)props中的changeName每次都不相等,所以就更新了,給我們直觀的感覺是失效了。

          那么如何解決這個(gè)問題,React hooks 中提供了 useCallback,可以對(duì)props傳入的回調(diào)函數(shù)進(jìn)行緩存,我們來(lái)改一下Home代碼。

          const changeName = React.useCallback((name) => {
              setType(name)
          },[])

          效果:

          pureComponent1.gif

          這樣就根本解決了問題,用 useCallback對(duì)changeName函數(shù)進(jìn)行緩存,在每一次 Home 組件執(zhí)行,只要useCallbackdeps沒有變,changeName內(nèi)存空間還指向原來(lái)的函數(shù),這樣PureComponent淺比較就會(huì)發(fā)現(xiàn)是相同changeName,從而不渲染組件,至此案件已破。

          繼續(xù)深入

          大家用函數(shù)組件+類組件開發(fā)的時(shí)候,如果用到React.memo React.PureComponent等api,要注意給這些組件綁定事件的方式,如果是函數(shù)組件,那么想要持續(xù)保持純組件的渲染控制的特性的話,那么請(qǐng)用 useCallback,useMemo等api處理,如果是類組件,請(qǐng)不要用箭頭函數(shù)綁定事件,箭頭函數(shù)同樣會(huì)造成失效的情況。

          上述中提到了一個(gè)淺比較shallowEqual,接下來(lái)我們重點(diǎn)分析一下 PureComponent是如何shallowEqual,接下來(lái)我們?cè)谏钊胙芯恳幌?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">shallowEqual的奧秘。那么就有從類租價(jià)的更新開始。

          react-reconciler/src/ReactFiberClassComponent.js

          function updateClassInstance(){
              const shouldUpdate =
              checkHasForceUpdateAfterProcessing() ||
              checkShouldComponentUpdate(
                workInProgress,
                ctor,
                oldProps,
                newProps,
                oldState,
                newState,
                nextContext,
              );
              return shouldUpdate
          }

          我這里簡(jiǎn)化updateClassInstance,只保留了涉及到PureComponent的部分。updateClassInstance這個(gè)函數(shù)主要是用來(lái),執(zhí)行生命周期,更新state,判斷組件是否重新渲染,返回的 shouldUpdate用來(lái)決定當(dāng)前類組件是否渲染。checkHasForceUpdateAfterProcessing檢查更新來(lái)源是否來(lái)源與 forceUpdate , 如果是forceUpdate組件是一定會(huì)更新的,checkShouldComponentUpdate檢查組件是否渲染。我們接下來(lái)看一下這個(gè)函數(shù)的邏輯。

          function checkShouldComponentUpdate(){
              /* 這里會(huì)執(zhí)行類組件的生命周期 shouldComponentUpdate */
              const shouldUpdate = instance.shouldComponentUpdate(
                newProps,
                newState,
                nextContext,
              );
              /* 這里判斷組件是否是 PureComponent 純組件,如果是純組件那么會(huì)調(diào)用 shallowEqual 淺比較  */
              if (ctor.prototype && ctor.prototype.isPureReactComponent) {
                  return (
                  !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
                  );
              }
          }

          checkShouldComponentUpdate有兩個(gè)至關(guān)重要的作用:

          • 第一個(gè)就是如果類組件有生命周期shouldComponentUpdate,會(huì)執(zhí)行生命周期shouldComponentUpdate,判斷組件是否渲染。
          • 如果發(fā)現(xiàn)是純組件PureComponent,會(huì)淺比較新老propsstate是否相等,如果相等,則不更新組件。isPureReactComponent就是我們使用PureComponent的標(biāo)識(shí),證明是純組件。

          接下來(lái)就是重點(diǎn)shallowEqual,以props為例子,我們看一下。

          shared/shallowEqual

          function shallowEqual(objA: mixed, objB: mixed): boolean {
            if (is(objA, objB)) { // is可以 理解成  objA === objB 那么返回相等
              return true;
            }

            if (
              typeof objA !== 'object' ||
              objA === null ||
              typeof objB !== 'object' ||
              objB === null
            ) {
              return false;  
            } // 如果新老props有一個(gè)不為對(duì)象,或者不存在,那么直接返回false

            const keysA = Object.keys(objA); // 老props / 老state key組成的數(shù)組
            const keysB = Object.keys(objB); // 新props / 新state key組成的數(shù)組

            if (keysA.length !== keysB.length) { // 說(shuō)明props增加或者減少,那么直接返回不想等
              return false;
            }

            for (let i = 0; i < keysA.length; i++) { // 遍歷老的props ,發(fā)現(xiàn)新的props沒有,或者新老props不同等,那么返回不更新組件。
              if (
                !hasOwnProperty.call(objB, keysA[i]) ||
                !is(objA[keysA[i]], objB[keysA[i]])
              ) {
                return false;
              }
            }

            return true//默認(rèn)返回相等
          }

          shallowEqual流程是這樣的,shallowEqual 返回 true 則證明相等,那么不更新組件;如果返回false 證明不想等,那么更新組件。is 我們暫且可以理解成 ===

          • 第一步,直接通過(guò) === 判斷是否相等,如果相等,那么返回true。正常情況只要調(diào)用 React.createElement 會(huì)重新創(chuàng)建props,props都是不相等的。
          • 第二步,如果新老props有一個(gè)不為對(duì)象,或者不存在,那么直接返回false。
          • 第三步,判斷新老props,key組成的數(shù)組數(shù)量等不想等,說(shuō)明props有增加或者減少,那么直接返回false。
          • 第四步,遍歷老的props ,發(fā)現(xiàn)新的props沒有與之對(duì)應(yīng),或者新老props不同等,那么返回false。
          • 默認(rèn)返回true。

          這就是shallowEqual邏輯,代碼還是非常簡(jiǎn)單的。感興趣的同學(xué)可以看一看。

          案件五: useState更新相同的State,函數(shù)組件執(zhí)行2次

          接到報(bào)案

          這個(gè)問題實(shí)際很懸,大家可能平時(shí)沒有注意到,引起我的注意的是掘金的一個(gè)掘友問我的一個(gè)問題,問題如下:

          首先非常感謝這位細(xì)心的掘友的報(bào)案,我在 React-hooks 原理 中講到過(guò),對(duì)于更新組件的方法函數(shù)組件 useState 和類組件的setState有一定區(qū)別,useState源碼中如果遇到兩次相同的state,會(huì)默認(rèn)阻止組件再更新,但是類組件中setState如果沒有設(shè)置 PureComponent,兩次相同的state 也會(huì)更新。

          我們回顧一下 hooks 中是怎么樣阻止組件更新的。

          react-reconciler/src/ReactFiberHooks.js -> dispatchAction

          if (is(eagerState, currentState)) { 
               return
          }
          scheduleUpdateOnFiber(fiber, expirationTime); // 調(diào)度更新

          如果判斷上一次的state -> currentState ,和這一次的state -> eagerState 相等,那么將直接 return阻止組件進(jìn)行scheduleUpdate調(diào)度更新。所以我們想如果兩次 useState觸發(fā)同樣的state,那么組件只能更新一次才對(duì),但是事實(shí)真的是這樣嗎?。

          立案調(diào)查

          順著這位掘友提供的線索,我們開始寫 demo進(jìn)行驗(yàn)證。

          const Index = () => {
            const [ number , setNumber  ] = useState(0)
            console.log('組件渲染',number)
            return <div className="page" >
              <div className="content" >
                 <span>{ number }</span><br/>
                 <button onClick={ () => setNumber(1) } >將number設(shè)置成1</button><br/>
                 <button onClick={ () => setNumber(2) } >將number設(shè)置成2</button><br/>
                 <button onClick={ () => setNumber(3) } >將number設(shè)置成3</button>
              </div>
            </div>

          }
          export default class Home extends React.Component{
            render(){
              return <Index />
            }
          }

          如上demo,三個(gè)按鈕,我們期望連續(xù)點(diǎn)擊每一個(gè)按鈕,組件都會(huì)僅此渲染一次,于是我們開始實(shí)驗(yàn):

          效果:

          demo1.gif

          果然,我們通過(guò) setNumber 改變 number,每次連續(xù)點(diǎn)擊按鈕,組件都會(huì)更新2次,按照我們正常的理解,每次賦予 number 相同的值,只會(huì)渲染一次才對(duì),但是為什么執(zhí)行了2次呢?

          可能剛開始會(huì)陷入困境,不知道怎么破案,但是我們?cè)谙?hooks原理中講過(guò),每一個(gè)函數(shù)組件用對(duì)應(yīng)的函數(shù)組件的 fiber 對(duì)象去保存 hooks 信息。所以我們只能從 fiber找到線索。

          順藤摸瓜

          那么如何找到函數(shù)組件對(duì)應(yīng)的fiber對(duì)象呢,這就順著函數(shù)組件的父級(jí) Home 入手了,因?yàn)槲覀兛梢詮念惤M件Home中找到對(duì)應(yīng)的fiber對(duì)象,然后根據(jù) child 指針找到函數(shù)組件 Index對(duì)應(yīng)的 fiber。說(shuō)干就干,我們將上述代碼改造成如下的樣子:

          const Index = ({ consoleFiber }) => {
            const [ number , setNumber  ] = useState(0)
            useEffect(()=>{  
                console.log(number)
                consoleFiber() // 每次fiber更新后,打印 fiber 檢測(cè) fiber變化
            })
            return <div className="page" >
              <div className="content" >
                 <span>{ number }</span><br/>
                 <button onClick={ () => setNumber(1) } >將number設(shè)置成1</button><br/>
              </div>
            </div>

          }
          export default class Home extends React.Component{
            consoleChildrenFiber(){
               console.log(this._reactInternalFiber.child) /* 用來(lái)打印函數(shù)組件 Index 對(duì)應(yīng)的fiber */
            }
            render(){
              return <Index consoleFiber={ this.consoleChildrenFiber.bind(this) }  />
            }
          }

          我們重點(diǎn)關(guān)心fiber上這幾個(gè)屬性,這對(duì)破案很有幫助

          • Index fiber上的 memoizedState 屬性,react hooks 原理文章中講過(guò),函數(shù)組件用 memoizedState 保存所有的 hooks 信息。
          • Index fiber上的 alternate 屬性
          • Index fiber上的 alternate 屬性上的 memoizedState屬性。是不是很繞??,馬上會(huì)揭曉是什么。
          • Index組件上的 useState中的number。

          首先我們講一下 alternate 指針指的是什么?

          說(shuō)到alternate 就要從fiber架構(gòu)設(shè)計(jì)說(shuō)起,每個(gè)React元素節(jié)點(diǎn),用兩顆fiber樹保存狀態(tài),一顆樹保存當(dāng)前狀態(tài),一個(gè)樹保存上一次的狀態(tài),兩棵 fiber 樹用 alternate 相互指向。就是我們耳熟能詳?shù)?strong>雙緩沖。

          初始化打印

          效果圖:

          fiber1.jpg

          初始化完成第一次render后,我們看一下fiber樹上的這幾個(gè)狀態(tài)

          第一次打印結(jié)果如下,

          • fiber上的 memoizedStatebaseState = 0 即是初始化 useState 的值。
          • fiber上的 alternatenull。
          • Index組件上的 number 為 0。

          初始化流程:首先對(duì)于組件第一次初始化,會(huì)調(diào)和渲染形成一個(gè)fiber樹(我們簡(jiǎn)稱為樹A)。樹A的alternate屬性為 null。

          第一次點(diǎn)擊 setNumber(1)

          我們第一次點(diǎn)擊發(fā)現(xiàn)組件渲染了,然后我們打印結(jié)果如下:

          fiber2.jpg
          • 樹A上的 memoizedState 中 **baseState = 0。
          • 樹A上的 alternate 指向 另外一個(gè)fiber(我們這里稱之為樹B)。
          • Index組件上的 number 為 1。

          接下來(lái)我們打印樹B上的 memoizedState

          fiber3.jpg

          結(jié)果我們發(fā)現(xiàn)樹B上 memoizedState上的 baseState = 1。

          得出結(jié)論:更新的狀態(tài)都在樹B上,而樹A上的 baseState還是之前的0。

          我們大膽猜測(cè)一下更新流程:在第一次更新渲染的時(shí)候,由于樹A中,不存在alternate,所以直接復(fù)制一份樹A作為 workInProgress(我們這里稱之為樹B)所有的更新都在當(dāng)前樹B中進(jìn)行,所以 baseState 會(huì)被更新成 1,然后用當(dāng)前的樹B進(jìn)行渲染。結(jié)束后樹A和樹B通過(guò)alternate相互指向。樹B作為下一次操作的current樹。

          第二次點(diǎn)擊 setNumber(1)

          第二次打印,組件同樣渲染了,然后我們打印fiber對(duì)象,效果如下:

          fiber4.jpg
          • fiber對(duì)象上的 memoizedStatebaseState更新成了 1。

          然后我們打印一下 alternatebaseState也更新成了 1。

          fiber5.jpg

          第二次點(diǎn)擊之后 ,樹A和樹B都更新到最新的 baseState = 1

          首先我們分析一下流程:當(dāng)我們第二次點(diǎn)擊時(shí)候,是通過(guò)上一次樹A中的 baseState = 0setNumber(1) 傳入的 1做的比較。所以發(fā)現(xiàn) eagerState !== currentState ,組件又更新了一次。接下來(lái)會(huì)以current樹(樹B)的 alternate指向的樹A作為新的workInProgress進(jìn)行更新,此時(shí)的樹A上的 baseState 終于更新成了 1 ,這就解釋了為什么上述兩個(gè) baseState 都等于 1。接下來(lái)組件渲染完成。樹A作為了新的 current 樹。

          在我們第二次打印,打印出來(lái)的實(shí)際是交替后樹B,樹A和樹B就這樣交替著作為最新狀態(tài)用于渲染的workInProgress樹和緩存上一次狀態(tài)用于下一次渲染的current樹。

          第三次點(diǎn)擊(三者言其多也)

          那么第三次點(diǎn)擊組件沒有渲染,就很好解釋了,第三次點(diǎn)擊上一次樹B中的 baseState = 1setNumber(1)相等,也就直接走了return邏輯。

          揭開謎底(我們學(xué)到了什么)

          • 雙緩沖樹:React 用 workInProgress樹(內(nèi)存中構(gòu)建的樹) 和 current(渲染樹) 來(lái)實(shí)現(xiàn)更新邏輯。我們console.log打印的fiber都是在內(nèi)存中即將 workInProgress的fiber樹。雙緩存一個(gè)在內(nèi)存中構(gòu)建,在下一次渲染的時(shí)候,直接用緩存樹做為下一次渲染樹,上一次的渲染樹又作為緩存樹,這樣可以防止只用一顆樹更新狀態(tài)的丟失的情況,又加快了dom節(jié)點(diǎn)的替換與更新。

          • 更新機(jī)制:在一次更新中,首先會(huì)獲取current樹的 alternate作為當(dāng)前的 workInProgress,渲染完畢后,workInProgress 樹變?yōu)?current 樹。我們用如上的樹A和樹B和已經(jīng)保存的baseState模型,來(lái)更形象的解釋了更新機(jī)制 。 hooks中的useState進(jìn)行state對(duì)比,用的是緩存樹上的state和當(dāng)前最新的state。所有就解釋了為什么更新相同的state,函數(shù)組件執(zhí)行2次了。

          我們用一幅流程圖來(lái)描述整個(gè)流程。

          FFB125E7-6A34-4F44-BB6E-A11D598D0A01.jpg

          此案已破,通過(guò)這個(gè)容易忽略的案件,我們學(xué)習(xí)了雙緩沖和更新機(jī)制。

          案件六:useEffect修改DOM元素導(dǎo)致怪異閃現(xiàn)

          鬼使神差

          小明(化名)在動(dòng)態(tài)掛載組件的時(shí)候,遇到了靈異的Dom閃現(xiàn)現(xiàn)象,讓我們先來(lái)看一下現(xiàn)象。

          閃現(xiàn)現(xiàn)象:

          effect.gif

          代碼:

          function Index({ offset }){
              const card  = React.useRef(null)
              React.useEffect(()=>{
                 card.current.style.left = offset
              },[])
              return <div className='box' >
                  <div className='card custom' ref={card}   >《 React進(jìn)階實(shí)踐指南 》</div>
              </div>

          }

          export default function Home({ offset = '300px' }){
             const [ isRender , setRender ] = React.useState(false)
             return <div>
                 { isRender && <Index offset={offset}  /> }
                 <button onClick={ ()=>setRender(true) } > 掛載</button>
             </div>

          }
          • 在父組件用 isRender 動(dòng)態(tài)加載 Index,點(diǎn)擊按鈕控制 Index渲染。
          • Index的接受動(dòng)態(tài)的偏移量offset。并通過(guò)操縱用useRef獲取的原生dom直接改變偏移量,使得劃塊滑動(dòng)。但是出現(xiàn)了如上圖的閃現(xiàn)現(xiàn)象,很不友好,那么為什么會(huì)造成這個(gè)問題呢?

          深入了解

          初步判斷產(chǎn)生這個(gè)閃現(xiàn)的問題應(yīng)該是 useEffect造成的,為什么這么說(shuō)呢,因?yàn)轭惤M件生命周期 componentDidMount寫同樣的邏輯,然而并不會(huì)出現(xiàn)這種現(xiàn)象。那么為什么useEffect會(huì)造成這種情況,我們只能順藤摸瓜找到 useEffectcallback執(zhí)行時(shí)機(jī)說(shuō)起。

          useEffect ,useLayoutEffect , componentDidMount執(zhí)行時(shí)機(jī)都是在 commit階段執(zhí)行。我們知道 React 有一個(gè) effectList存放不同effect。因?yàn)?React 對(duì)不同的 effect 執(zhí)行邏輯和時(shí)機(jī)不同。我們看一下useEffect被定義的時(shí)候,定義成了什么樣類型的 effect。

          react-reconciler/src/ReactFiberHooks.js

          function mountEffect(create, deps){
            return mountEffectImpl(
              UpdateEffect | PassiveEffect, // PassiveEffect 
              HookPassive,
              create,
              deps,
            );
          }

          這個(gè)函數(shù)的信息如下:

          • useEffect 被賦予 PassiveEffect類型的 effect
          • 小明改原生dom位置的函數(shù),就是 create

          那么 create函數(shù)什么時(shí)候執(zhí)行的,React又是怎么處理PassiveEffect的呢,這是破案的關(guān)鍵。記下來(lái)我們看一 下React 怎么處理PassiveEffect

          react-reconciler/src/ReactFiberCommitWork.js

          function commitBeforeMutationEffects({
            while (nextEffect !== null) {
              if ((effectTag & Passive) !== NoEffect) {
                if (!rootDoesHavePassiveEffects) {
                  rootDoesHavePassiveEffects = true;
                  /*  異步調(diào)度 - PassiveEffect */
                  scheduleCallback(NormalPriority, () => {
                    flushPassiveEffects();
                    return null;
                  });
                }
              }
              nextEffect = nextEffect.nextEffect;
            }
          }

          commitBeforeMutationEffects 函數(shù)中,會(huì)異步調(diào)度 flushPassiveEffects方法,flushPassiveEffects方法中,對(duì)于React hooks 會(huì)執(zhí)行 commitPassiveHookEffects,然后會(huì)執(zhí)行 commitHookEffectListMount 。

          function commitHookEffectListMount(){
               if (lastEffect !== null) {
                    effect.destroy = create(); /* 執(zhí)行useEffect中餓 */
               }
          }

          commitHookEffectListMount中,create函數(shù)會(huì)被調(diào)用。我們給dom元素加的位置就會(huì)生效。

          那么問題來(lái)了,異步調(diào)度做了些什么呢? React的異步調(diào)度,為了防止一些任務(wù)執(zhí)行耽誤了瀏覽器繪制,而造成卡幀現(xiàn)象,react 對(duì)于一些優(yōu)先級(jí)不高的任務(wù),采用異步調(diào)度來(lái)處理,也就是讓瀏覽器才空閑的時(shí)間來(lái)執(zhí)行這些異步任務(wù),異步任務(wù)執(zhí)行在不同平臺(tái),不同瀏覽器上實(shí)現(xiàn)方式不同,這里先姑且認(rèn)為效果和setTimeout一樣。

          雨過(guò)天晴

          通過(guò)上述我們發(fā)現(xiàn) useEffect 的第一個(gè)參數(shù) create,采用的異步調(diào)用的方式,那么閃現(xiàn)就很好理解了,在點(diǎn)擊按鈕組件第一次渲染過(guò)程中,首先執(zhí)行函數(shù)組件render,然后commit替換真實(shí)dom節(jié)點(diǎn),然后瀏覽器繪制完畢。此時(shí)瀏覽器已經(jīng)繪制了一次,然后瀏覽器有空余時(shí)間執(zhí)行異步任務(wù),所以執(zhí)行了create,修改了元素的位置信息,因?yàn)樯弦淮卧匾呀?jīng)繪制,此時(shí)又修改了一個(gè)位置,所以感到閃現(xiàn)的效果,此案已破。

          那么我們?cè)趺礃咏鉀Q閃現(xiàn)的現(xiàn)象呢,那就是 React.useLayoutEffect ,useLayoutEffectcreate是同步執(zhí)行的,所以瀏覽器繪制一次,直接更新了最新的位置。

            React.useLayoutEffect(()=>{
                card.current.style.left = offset
            },[])

          總結(jié)  

          本節(jié)可我們學(xué)到了什么?

          本文以破案的角度,從原理角度講解了 React 一些意想不到的現(xiàn)象,透過(guò)這些現(xiàn)象,我們學(xué)習(xí)了一些 React 內(nèi)在的東西,我對(duì)如上案例總結(jié),

          • 案件一-對(duì)一些組件渲染和組件錯(cuò)誤時(shí)機(jī)聲明的理解
          • 案件二-實(shí)際事件池概念的補(bǔ)充。
          • 案件三-是對(duì)一些組件庫(kù)引入多個(gè)版本 React 的思考和解決方案。
          • 案件四-要注意給 memo / PureComponent 綁定事件,以及如何處理 PureComponent 邏輯,shallowEqual的原理。
          • 案件五-實(shí)際是對(duì)fiber雙緩存樹的講解。
          • 案件六-是對(duì) useEffect create 執(zhí)行時(shí)機(jī)的講解。

          如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
          2. 歡迎加我微信「TH0000666」一起交流學(xué)習(xí)...
          3. 關(guān)注公眾號(hào)「前端Sharing」,持續(xù)為你推送精選好文。



          瀏覽 92
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  性感美女视频一二三 | 影视先锋成人在线 | 亚洲AV成人无码久久精品毛片 | 高清二区三区一区日本 | 天天干天天操天天干 |