網站首頁 編程語言 正文
熱身準備
這里不再講useLayoutEffect
,它和useEffect
的代碼是一樣的,區別主要是:
- 執行時機不同;
-
useEffect
是異步,useLayoutEffect
是同步,會阻塞渲染;
初始化 mount
mountEffect
在所有hook
初始化時都會通過下面這行代碼實現hook
結構的初始化和存儲,這里不再講mountWorkInProgressHook
方法
var hook = mountWorkInProgressHook();
在mountEffect
方法中,只有這幾行代碼。先來解讀下幾個參數:
- fiberFlags:有副作用的更新標記,用來標記hook所在的
fiber
; - hookFlags:副作用標記;
- create:使用者傳入的回調函數;
- deps:使用者傳入的數組依賴;
function mountEffectImpl(fiberFlags, hookFlags, create, deps) { // hook初始化 var hook = mountWorkInProgressHook(); // 判斷是否有傳入deps,如果有會作為下次更新的deps var nextDeps = deps === undefined ? null : deps; // 給hook所在的fiber打上有副作用的更新的標記 currentlyRenderingFiber$1.flags |= fiberFlags; // 將副作用操作存放到fiber.memoizedState.hook.memoizedState中 hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps); }
上面代碼中都有注釋,接下來我們看看React
是如何存放副作用更新操作的,主要就是pushEffect
方法
function pushEffect(tag, create, destroy, deps) { // 初始化副作用結構, var effect = { tag: tag, create: create, // 回調函數 destroy: destroy, // 回調函數里的return(mount時是undefined) deps: deps, // 依賴數組 // 閉環鏈表 next: null }; // 下面的一大段代碼看著復雜,但是有沒有很熟悉的感覺? var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue; if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber$1.updateQueue = componentUpdateQueue; // effect.next = effect形成環形鏈表 componentUpdateQueue.lastEffect = effect.next = effect; } else { var lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { var firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
上面這段代碼除了初始化副作用的結構代碼外,都是我們前面講過的操作閉環鏈表,向鏈表末尾添加新的effect
,該effect.next
指向fisrtEffect
,并且鏈表當前的指針指向最新添加的effect
。
useEffect
的初始化就這么簡單,簡單總結一下:給hook
所在的fiber
打上副作用更新標記,并且fiber.memoizedState.hook.memoizedState
和fiber.updateQueue
存儲了相關的副作用,這些副作用通過閉環鏈表的結構存儲。
相關參考視頻講解:傳送門
更新 update
updateEffect
updateWorkInProgressHook
在上篇文章也已講過,不再詳述,主要功能就是創建一個帶有回調函數的newHook
去覆蓋之前的hook
。
function updateEffectImpl(fiberFlags, hookFlags, create, deps) { var hook = updateWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var destroy = undefined; if (currentHook !== null) { var prevEffect = currentHook.memoizedState; destroy = prevEffect.destroy; if (nextDeps !== null) { var prevDeps = prevEffect.deps; // 比較兩次依賴數組中的值是否有變化 if (areHookInputsEqual(nextDeps, prevDeps)) { // 和之前初始化時一樣 pushEffect(hookFlags, create, destroy, nextDeps); return; } } } // 和之前初始化時一樣 currentlyRenderingFiber$1.flags |= fiberFlags; hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps); }
相信眼眼尖的看官已經注意到上面代碼中有兩個pushEffect
,一個沒有賦值給hook.memoizedState
,一個賦值了,這兩者有什么區別呢?
先保留著這個疑問,先來了解下下面這行代碼都做了些什么,因為它造就了兩個pushEffect
。
if (areHookInputsEqual(nextDeps, prevDeps)){...}
function areHookInputsEqual(nextDeps, prevDeps) { // 沒有傳deps的情況返回false if (prevDeps === null) { return false; } // deps不是[],且其中的值有變動才會返回false for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (objectIs(nextDeps[i], prevDeps[i])) { continue; } return false; } // deps = [],或者deps里面的值沒有變化會返回true return true; }
它會判斷兩次依賴數組中的值是否有變化以及deps
是否是空數組來決定返回true
和false
,返回true
表明這次不需要調用回調函數。
現在我們明白了兩次pushEffect
的異同,if
內部的pushEffect
是不需要調用的回調函數, 外面的pushEffect
是需要調用的。再來仔細看下這兩行代碼:
// if內部的,第一個參數是hookFlags = 4 pushEffect(hookFlags, create, destroy, nextDeps); // if外部的,第一個參數是HasEffect | hookFlags = 5 hook.memoizedState = pushEffect(HasEffect | hookFlags, create, destroy, nextDeps);
這兩行代碼的區別是傳入的第一個參數不同,而第一個參數就是effect.tag
的值,effect.tag = 4
不會添加到副作用執行隊列,而effect.tag = 5
可以。沒有添加到副作用執行隊列的effect
就不會執行。這樣就巧妙的實現了useEffect
基于deps
來判斷是否需要執行回調函數。
到這里, 我們搞明白了,不管useEffect
里的deps
有沒有變化都會為回調函數創建effect
并添加到effect
鏈表和fiber.updateQueue
中,但是React
會根據effect.tag
來決定該effect
是否要添加到副作用執行隊列中去執行。
執行副作用
我們現在知道了,useEffect
是異步執行的。那么這個回調函數副作用會在什么時候執行呢?useEffect
回調函數會在layout
階段之后執行。現在我們來了解下具體調用執行的流程。
我畫了一個簡單的流程圖,大致描述了下調用流程。首先在mutation
之前階段,基于副作用創建任務并放到taskQueue
中,同時會執行requestHostCallback
,這個方法就涉及到了異步了,它首先考慮使用MessageChannel
實現異步,其次會考慮使用setTimeout
實現。使用MessageChannel
時,requestHostCallback
會馬上執行port.postMessage(null);
,這樣就可以在異步的第一時間執行workLoop
,workLoop
會遍歷taskQueue
,執行任務,如果是useEffect
的effect
任務,會調用flusnPassiveEffects
。
Q:可能有人會疑惑為什么優先考慮MessageChannel
?
A: 首先我們要明白React
調度更新的目的是為了時間分片,意思是每隔一段時間就把主線程還給瀏覽器,避免長時間占用主線程導致頁面卡頓。使用MessageChannel
和SetTimeout
的目的都是為了創建宏任務,因為宏任務會在當前微任務都執行完后,等到瀏覽器主線程空閑后才會執行。不優先考慮setTimeout
的原因是,setTimeout
執行時間不準確,會造成時間浪費,即使是setTimeout(fn, 0)
,感興趣的可以去自己了解下,本文不做贅述了。
在schedulePassiveEffects
中,會決定是否執行effect
鏈表中的effect
,判斷的依據就是每個effect
上的effect.tag
:
function schedulePassiveEffects(finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; // 遍歷effect鏈表 do { var _effect = effect, next = _effect.next, tag = _effect.tag; // 基于effect.tag決定是否添加到副作用執行隊列 if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) { enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); enqueuePendingPassiveHookEffectMount(finishedWork, effect); } effect = next; } while (effect !== firstEffect); } }
在flushPassiveEffects
中,會先執行上次更新動作的銷毀函數,然后再執行本次更新動作的回調函數,并且會把回調函數的return
作為下次更新動作的銷毀函數。
function flushPassiveEffectsImpl() { // 執行上次更新動作的銷毀函數 var unmountEffects = pendingPassiveHookEffectsUnmount; pendingPassiveHookEffectsUnmount = []; for (var i = 0; i < unmountEffects.length; i += 2) { ...destroy() } // 執行本次更新動作的回調函數 var mountEffects = pendingPassiveHookEffectsMount; pendingPassiveHookEffectsMount = []; for (var _i = 0; _i < mountEffects.length; _i += 2) { ...create() } }
上面代碼中的這兩行就是來自副作用執行隊列,已經過濾掉了不需要執行的effect
,只執行該隊列上的副作用函數
var unmountEffects = pendingPassiveHookEffectsUnmount; var mountEffects = pendingPassiveHookEffectsMount;
總結
看完這篇文章, 我們可以弄明白下面這幾個問題:
-
useEffect
和useLayoutEffect
的區別? -
useEffect
是怎么判斷回調函數是否需要執行的? -
useEffect
是同步還是異步? -
useEffect
是通過什么實現異步的? -
useEffect
為什么要要優先選用MessageChannel
實現異步?
原文鏈接:https://blog.csdn.net/It_kc/article/details/127547634
相關推薦
- 2022-02-20 Ubuntu18.04更改apt源為阿里云源的詳細過程_Linux
- 2022-07-29 Linux磁盤管理方法介紹_linux shell
- 2023-12-23 mybatis的selectOne()方法使用記錄
- 2022-04-24 Redis三種特殊數據類型的具體使用_Redis
- 2024-01-16 URLClassLoader詳解
- 2023-11-17 Python如何使用matlibplot繪制3D柱形圖
- 2022-06-11 C#實現DataTable轉TXT、CSV文件_C#教程
- 2022-10-18 shell腳本批量將文件復制到指定的文件夾下_linux shell
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支