useEffect和useLayoutEffect是React官方推出的两个hooks,都是用来执行反作用的钩子函数,名字相似,功能相近,惟一不一样的就是执行的时机有差别,今天这篇文章主要是从这两个钩子函数的执行时机入手,来剖析一下React的运行原理和浏览器的渲染流程。javascript
useLayoutEffect
其函数签名与 useEffect
相同,但它会在全部的 DOM 变动以后同步调用 effect。可使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制以前, useLayoutEffect
内部的更新计划将被同步刷新,尽量使用标准的 useEffect
以免阻塞视觉更新。java
简单来说,就是:useEffect是异步的,useLayoutEffect是同步的,异(同)步是相对于浏览器执行刷新屏幕Task来讲的。react
下面将经过一个简单的demo示例来讲明具体的执行过程,其中React是16.13.1版本,首先是示例代码:web
import React, { useState, useEffect, useLayoutEffect } from 'react'; const EffectDemo = () => { const [count, setCount] = useState(0); useEffect(function useEffectDemo() { console.log('useEffect:', count); }, [count]); useLayoutEffect(function useLayoutEffectDemo() { console.log('useLayoutEffect:', count); }, [count]); return ( <div> <button onClick={() => { setCount(count + 1); }} >click me</button> </div> ); }; export default EffectDemo;
功能很简单,就不作界面展现,这里主要是看一下浏览器控制台Performance的监控图:
经过两个hooks的执行图能够看出,useLayoutEffect发生在页面渲染到屏幕(用户可见)以前,useEffect发生在那以后,中间还经历了DCL,FCP,FMP,LCP阶段,除开DCL(DomContentLoaded)以外,这些指标是RAIL模型衡量页面性能的标准,总的来讲,渲染到屏幕的阶段是一个分水岭,那么渲染包含什么呢,仍是看图吧:
此阶段完成了样式的计算(Recalculate Style)和布局(Layout),紧接着是一个Task,完成Update Layer Tree,Paint,Composite Layers,通过这一系列的任务后,页面最终呈现给用户,能够用一张图来表示浏览器的渲染过程:
后面会有相关学习资料,这里就不展开细说了。浏览器
在深刻了解React的运行以前,首先在本地写一个简单的示例,大体模拟文章开始的例子:数据结构
<body> <div id="app"></div> <script type="text/javascript"> (function iife(){ function render() { var appNode = document.querySelector('#app'); var textNode = document.createElement('span'); textNode.id = 'tip'; textNode.textContent = 'hello'; appNode.appendChild(textNode); } function useLayoutEffectDemo() { console.log('useLayoutEffectDemo', document.querySelector('#tip')); } function useEffectDemo() { console.log('useEffectDemo'); } render(); useLayoutEffectDemo(); setTimeout(useEffectDemo, 0); })(); </script> </body>
而后启用Performance监控渲染状况:
架构
总结一下:
1.首先运行render,完成后当即执行useLayoutEffectDemo函数(虽然已经插入DOM,可是界面尚未渲染出来);
2.注册异步回调函数useEffectDemo,该函数将在0ms事后加入EventLoop中的宏任务队列;
3.页面开始渲染:Recalculate Style->Layout->Update Layer Tree->Paint->Composite Layers->GPU绘制;
4.取出宏任务useEffectDemo,执行回调;app
React的执行比这个模拟示例复杂不少,可是抽象出的流程节点大同小异,了解以后,咱们能够继续深刻挖掘React的运行机制了。frontend
React渲染页面分为两个阶段:
1.调度阶段(reconciliation):找出须要更新的节点元素
2.渲染阶段(commit):将须要更新的元素插入DOM
接下来就跟着React的运行流程来具体看下不一样阶段的执行状况:dom
简单总结一下:
1.react-dom负责Fiber节点的建立,最终造成一个Fiber节点树,其中每一个Fiber包含须要执行的反作用和渲染到屏幕的DOM对象;
2.调用scheduler暴露的方法注册须要调度的事件;
3.执行DOM插入;
4.执行useLyaoutEffect或者ClassComponent的生命周期函数;
5.浏览器接过控制权,执行渲染;
6.scheduler执行调度任务,执行useEffectDemo;
以上就是总体流程,接下来再深刻一点,看看useEffect和useLayoutEffect是怎么解析和执行的:
从上图可知,uesEffect和useLayoutEffect最终都会调用mountEffectImpl函数,而后初始化/更新Fiber的updateQueue,能够看一下mountEffectImpl函数是怎样的:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; currentlyRenderingFiber$1.effectTag |= fiberEffectTag; hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps); }
都认识,可是不知道是干吗的,好吧,仍是用一张图来讲明吧:
这个函数的功能以下:
1.建立hook对象,放入到workInProgressHook链表中;
2.Fiber的updateQueue和上一步建立的hook关联,这样每个Fiber对象上就知道要执行Effect了;
那么workInProgressHook是干吗的呢,看下源代码的解释吧:
var workInProgressHook = null; // Whether an update was scheduled at any point during the render phase. This // does not get reset if we do another render pass; only when we're completely // finished evaluating this component. This is an optimization so we know // whether we need to clear render phase updates after a throw.
上面说到updateQueue,最终咱们写的useEffectDemo和useLayoutEffectDemo都会放在这里,那么是怎么一个结构存储的呢,能够打印看一下:
其实就是一个收尾相连的环形结构,为何要这么设计呢,你们看下commitHookEffectListMount执行函数的遍历方式就知道了:
function commitHookEffectListMount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Mount var create = effect.create; effect.destroy = create(); { var destroy = effect.destroy; if (destroy !== undefined && typeof destroy !== 'function') { var addendum = void 0; if (destroy === null) { addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).'; } else if (typeof destroy.then === 'function') { addendum = '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching'; } else { addendum = ' You returned: ' + destroy; } error('An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s%s', addendum, getStackByFiberInDevAndProd(finishedWork)); } } } effect = effect.next; } while (effect !== firstEffect); } }
这里根据effect的tag不一样决定执行哪种effect,这里咱们的useEffectDemo和useLayoutEfectDemo的tag分别是5和3,所以须要执行useEffect中的反作用函数时,commitHookEffectListMount的tag确定就是5了,执行useLayoutEffect中的反作用函数时,commitHookEffectListMount的tag确定就是3。
总的来讲全部的useEffect和useLayoutEffect的反作用函数都是在这里执行的,经过tag来控制他们的执行时机。
其实上面已经讲了commitHookEffectListMount的执行,这里再看下具体的执行过程:
执行useEffect的入口:
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork); return; } ...... }
执行useLayoutEffect的入口:
function commitPassiveHookEffects(finishedWork) { if ((finishedWork.effectTag & Passive) !== NoEffect) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { ...... commitHookEffectListMount(Passive$1 | HasEffect, finishedWork); break; } } } }
能够看出两个执行入口传入的第一个入参tag是不同的,最终执行的反作用函数就区分开来了。
如今你们应该对useEffect和useLayoutEffect的执行有了一个大体的了解,那么还有一个关于scheduler异步调度的小问题,本文最开始模拟的一个例子里是经过setTimeout来完成的,React中则是经过MessageChannel来实现的,若是不熟悉能够查查使用方式,这里来看下异步执行的过程:
浏览器的渲染是一个十分复杂的过程,若是不是很了解,能够浏览谷歌提供的介绍文章,连接以下:https://developers.google.cn/web/fundamentals/performance/rendering
了解了浏览器的基本渲染以后,能够更加深刻窥探浏览器的运行,首先上一张图:
上面这幅图是来源于https://aerotwist.com/blog/the-anatomy-of-a-frame
这里还给你们推荐一篇讲解浏览器渲染的文章:https://juejin.im/entry/6844903476506394638
在学习Hooks的时候,不免会和class组件中的生命周期作比较,这里咱们只关注useEffect,useEffect在某些程度上至关于componentDidMount
、 componentDidUpdate
、 componentWillUnmount
三个钩子函数的集合,由于这些函数都会阻塞浏览器的渲染,其中componentDidMount
、 componentDidUpdate
的执行是在哪里呢,看一下上面提到的commitLifeCycles函数就清楚了(componentWillUnmount你们有兴趣本身找找吧);
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { commitHookEffectListMount(Layout | HasEffect, finishedWork); return; } case ClassComponent: { var instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { // 初次渲染 ...... instance.componentDidMount(); stopPhaseTimer(); } else { // 更新渲染 ...... instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate); stopPhaseTimer(); } }