掘金小课《React进阶实战指南》笔记

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了掘金小课《React进阶实战指南》笔记相关的知识,希望对你有一定的参考价值。

阅读了掘金小课《React进阶实战指南》,做的笔记加自己的一些总结。部分资源来自掘金小课《React进阶实战指南》

认识JSX

Jsx统统被转为React.createElement,

createElement参数:

  • 第一个参数:如果是组件类型,会传入组件对应的类或函数;如果是 dom 元素类型,传入 div 或者 span 之类的字符串。
  • 第二个参数:一个对象,在 dom 类型中为标签属性,在组件类型中为 props 。
  • 其他参数:依次为 children,根据顺序排列。

<div>
   <TextComponent />
   <div>hello,world</div>
   let us learn React!
</div>

转化成

 React.createElement("div", null,
        React.createElement(TextComponent, null),
        React.createElement("div", null, "hello,world"),
        "let us learn React!"
    )

vdom样子:

JSX转化规则,经过createElement转化后

  • element类型,如div => reactElement类型,type为’div’

  • 文本类型,如’aaa’=> ‘aaa’,直接转为字符串

  • 数组类型,如[<div>,<div>] => [type: div…, type: div…],转化后是element的数组

  • 组件类型,转为react element,type是函数或者类本身

  • 三元运算 / 表达式,先运算,再按照上述规则转化

  • 函数执行,先执行,再按照上述规则转化

Fiber

针对不同的react element,Fiber的tag也不同

export const FunctionComponent = 0;       // 函数组件
export const ClassComponent = 1;          // 类组件
export const IndeterminateComponent = 2;  // 初始化的时候不知道是函数组件还是类组件 
export const HostRoot = 3;                // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4;              // 对应  ReactDOM.createPortal 产生的 Portal 
export const HostComponent = 5;           // dom 元素 比如 <div>
export const HostText = 6;                // 文本节点
export const Fragment = 7;                // 对应 <React.Fragment> 
export const Mode = 8;                    // 对应 <React.StrictMode>   
export const ContextConsumer = 9;         // 对应 <Context.Consumer>
export const ContextProvider = 10;        // 对应 <Context.Provider>
export const ForwardRef = 11;             // 对应 React.ForwardRef
export const Profiler = 12;               // 对应 <Profiler/ >
export const SuspenseComponent = 13;      // 对应 <Suspense>
export const MemoComponent = 14;          // 对应 React.memo 返回的组件

Component

  • 如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢?

    • 答:绑定 props 是在父类 Component 构造函数中,执行 super 等于执行 Component 函数,此时 props 没有作为第一个参数传给 super() ,在 Component 中就会找不到 props 参数,从而变成 undefined ,在接下来 constructor 代码中打印 props 为 undefined 。
  • 函数组件和类组件本质的区别是什么呢

    • 对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。
  • 组件通信方式:

    • props
    • eventbus
    • redux
    • Localstorage
    • context
    • ref

玄学State

  • state同步异步
    • 在18之前,对于setState,react会采用批量更新的方式更新state,通过一个开关来实现批量更新。所以处于微任务或者红任务的setState无法被合并。
    • 在18之后,对于setState,react批量更新的方式不再是通过开关,而是通过事件的优先级,同一优先级的事件会被统一处理,处于微任务和红任务之间的setState也会被合并。
  • 类组件中的 setState 和函数组件中的 useState 有什么异同?
    • 首先从原理角度出发,setState和 useState 更新视图,底层都调用了scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。
    • 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。
    • 但是useState的setState,在创建第一个update的时候,会先执行得到新的state,并且进行比较,若值相同,不会开启新的一轮调度
    • setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。

深入Props

  • props能做什么?
    • 父组件 props 可以把数据层传递给子组件去渲染消费。另一方面子组件可以通过 props 中的 callback ,来向父组件传递信息。还有一种可以将视图容器作为 props 进行渲染。
    • 对于更新机制,在vue中,基于劫持数据的变化并且通知订阅者通知其更新,但是在react中,无法检测到数据更新波动的范围,所以props成为了作为组件是否更新的重要准则,变化及更新,所以后来出现了PureCompoennt和memo等优化手段。

理解生命周期

mount阶段
  • constructor
  • getDerivedStateFromProps(nextProps,prevState)
  • componentWillMount(没有getDerviedStateFromProps或者没有getSnapShotBeforeUpdate才会执行),不再推荐
  • render函数执行(beginWork)
  • componentDidMount执行(layout阶段)
update阶段
  • componentWillReceiveProps(没有getDerviedStateFormProps的时候执行)

  • getDerviedStateFormProps

  • shouldComponentUpdate

  • componentWillUpdate

  • render

  • getSnapShotBeforeUpdate(在dom更新前执行,before-mutation阶段)

  • componentDidUpdate

unmount阶段
  • componentWillUnMount(mutation阶段)

getDerivedStateFormProps(nextProps,prevState)

类的静态属性,接受新的props和老的state。返回值会合并到最新的state去。

  • 在初始化和更新阶段,接受父组件的 props 数据, 可以对 props 进行格式化,过滤等操作,返回值将作为新的 state 合并到 state 中,供给视图渲染层消费。
  • 只要组件更新,就会执行 getDerivedStateFromProps,不管是 props 改变,还是 setState ,或是 forceUpdate 。
  • getDerivedStateFromProps作用:
    • 代替 componentWillMount 和 componentWillReceiveProps
    • 组件初始化或者更新时,将 props 映射到 state。
    • 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。
componentWillMount & componnetWillReceiveProps
  • 16.3之后,componentWillMount和componentWillUpdate和ComponentWillReceiveProps都带上了UNSAFE标识。因为这三个函数,都是在类组件render之前执行的,对于render,可以通过shouldUpdate控制是否执行,而这三个却没有限制,有着多次调用的风险。
  • componnetWillReceiveProps是在组件更新阶段,因为props的更新触发。但是只要组件触发更新,调用render,那么React.createElement就会重新执行,而props就会被重新创建,导致props没变,却会重复执行。
  • 当 props 不变的前提下, PureComponent 组件能否阻止 componentWillReceiveProps 执行?不能,组件跟生命周期执行没有关系,PureComponent相当于实现了shouldUpdate函数,但不能阻止componentWillReceiveProps执行。
  • componentWillUpdate被getSnapShotBeforeUpdate替代。

getSnapshotBeforeUpdate(prevProps,preState)更新前的props和更新前的preState

  • 返回值作为componentDidUpdate的第三个参数,代替componentWillUpdate,在before-mutaiton阶段执行。getSnapshotBeforeUpdate 这个生命周期意义就是配合componentDidUpdate 一起使用,计算形成一个 snapShot 传递给 componentDidUpdate 。保存一次更新前的信息。

componentDidUpdate(prevProps, prevState, snapshot) & componentDidMount

  • 在layout阶段执行,此时dom更新完毕。

shouldComponentUpdate(newProps,newState,nextContext)

  • 在beginWork阶段执行,用于优化手段。如果有getDerivedStateFromProps,返回值也会合并到最新的state,传给shouldComponentUpdate。

componentWillUnmount

  • mutaiton阶段执行,在dom即将销毁之前执行,清除延时器,定时器。

函数hooks执行阶段对应生命周期

useEffect & useLayoutEffect

一个是同步执行,一个是异步调用。useLayoutEffect会在layout阶段执行,useEffect会在layout阶段之后异步调用。

多功能Ref

创建ref
  • useRef
  • createRef
  • 两者的区别就是函数组件useRef是绑定在fiber上的,因为函数组件每次执行都会重新执行useRef。而update时候的useRef是直接从fiber上吗获取ref对象返回;而类有实例,它可以保存ref信息。
类组件创建ref的三种方式
  • ref是一个字符串, this.refs
  • ref是一个函数 ref=(node)=> this.currentDom = node
  • 通过createRef创建ref对象
高阶用法
  • 通过forwardRef转发ref

    const NewFather = React.forwardRef((props,ref)=> <Father grandRef=ref  ...props />)
    在Father里面就可以使用this.props.grandRef来绑定获取ref对象,也可以给ref对象赋值其他的东西
    
  • 组件通信,父组件通过ref控制子组件类实例,或者函数子组件的一些方法,操控子组件,如form的resetFields和setFieldsValue。函数组件没有实例,通过forwardRef + useImperativeHandle控制子组件的一些方法

    function Son(props, ref)
        const iptRef = useRef<htmlInputElement>(null)
        useImperativeHandle(ref,()=>
            const onSonFocus = () =>
                iptRef.current.focus()
            
            const onChangeValue = () =>
                iptRef.current.value = 'hahahah'
            
    
            return 
                onSonFocus,
                onChangeValue
            
        ,[])
        
        return <input type="text"  ref=iptRef/>
    
    const ForSon = React.forwardRef(Son)
    const App = () =>
        const sonRef = useRef<any>(null)
        return <div>
            <button onClick=()=>sonRef.current.onSonFocus()>focus</button>
            <button onClick=()=>sonRef.current.onChangeValue()>change</button>
            <ForSon ref=sonRef/>
        </div>
    
    (ReactDOM as any).createRoot(document.getElementById('root')).render(<App/>)
    

Context

createContext

const ThemeContext = React.createContext(null) //
const ThemeProvider = ThemeContext.Provider  //提供者
const ThemeConsumer = ThemeContext.Consumer // 订阅消费者

Provider使用

const ThemeProvider = ThemeContext.Provider  //提供者
export default function ProviderDemo()
    const [ contextValue , setContextValue ] = React.useState(  color:'#ccc', background:'pink' )
    return <div>
        <ThemeProvider value= contextValue  > 
            <Son />
        </ThemeProvider>
    </div>

消费者一共有三种方式消费:

  • 类组件静态属性contextType

    const ThemeContext = React.createContext(null)
    // 类组件 - contextType 方式
    class ConsumerDemo extends React.Component
       render()
           const  color,background  = this.context
           return <div style= color,background   >消费者</div> 
       
    
    ConsumerDemo.contextType = ThemeContext
    
    const Son = ()=> <ConsumerDemo />
    
  • 函数组件useContext

    const ThemeContext = React.createContext(null)
    // 函数组件 - useContext方式
    function ConsumerDemo()
        const  contextValue = React.useContext(ThemeContext) /*  */
        const  color,background  = contextValue
        return <div style= color,background   >消费者</div> 
    
    const Son = ()=> <ConsumerDemo />
    
  • 订阅者-Consumer模式

    const ThemeConsumer = ThemeContext.Consumer // 订阅消费者
    
    function ConsumerDemo(props)
        const  color,background  = props
        return <div style= color,background   >消费者</div> 
    
    const Son = () => (
        <ThemeConsumer>
            /* 将 context 内容转化成 props  */ 
            (contextValue)=> <ConsumerDemo  ...contextValue  /> 
        </ThemeConsumer>
    ) 
    

在 Provider 里 value 的改变,会使引用contextType,useContext 消费该 context 的组件重新 render ,同样会使 Consumer 的 children 函数重新执行,与前两种方式不同的是 Consumer 方式,当 context 内容改变的时候,不会让引用 Consumer 的父组件重新更新。

问题:

提供者的组件的setState目的是为了触发调度,影响消费者组件获取到的context改变从而引起消费者render。但是可能会造成提供者的子组件无意义的渲染。

解决:

  • 使用React.memo或者PureComponent,防止重复渲染。

  • 使用useMemo缓存vdom。jsx会被转为React.createElement,返回一个vdom。重复渲染是因为React.createElement被重复调用导致生成新的vdom。

    <ThemeProvider value= contextValue  >
         React.useMemo(()=>  <Son /> ,[]) 
    </ThemeProvider>
    

高阶组件

属性代理&反向继承

属性代理
function HOC(WrapComponent)
    return class Advance extends React.Component
       state=
           name:'alien'
       
       render()
           return <WrapComponent   ...this.props   ...this.state   />
       
    

缺点:转发ref需要forwardRef;无法获取组件的原始状态,需要使用ref;无法直接继承静态属性

反向继承
class Index extends React.Component
  render()
    return <div> hello,world  </div>
  

function HOC(Component)
    return class wrapComponent extends Component /* 直接继承需要包装的组件 */
        
    

export default HOC(Index) 
经典例子:
  • 强化props:react-router的withRouter,将react-router的context作为props传入。

  • 劫持渲染, 动态加载 React.lazy,接受一个promise,如()=>import(‘…/pages/index’),promise的返回值是一个组件,通过componentDidMount,等到react渲染该组件执行componentDidMount的时候,才会去接受promise.then取得组件,进行渲染。

  • 组件赋能,通过ref控制类实例。

  • 事件监控,在外层包裹一层div,监控点击事件等等。

注意事项
  • 谨慎修改原型链上的属性,如Component.prototype.componentDidMount
  • 不要在函数组件内部或类组件render函数中使用HOC,HOC每次都会生成新的组件,不满足diff优化.

React事件系统

原因:

  • 不同浏览器,事件存在不同的兼容性,所以必须统一兼容。
  • 其二,react的事件在17之前统统绑定在document,在17之后绑定在了容器上面,防止过多事件绑定在原生dom上,由于不是绑定在真实dom上,react需要自己实现一套机制,事件俘获,到事件冒泡。

事件合成

三部分:

  • 事件合成,初始化事件插件
  • 渲染过程中,收集事件,注册到container。
  • 一次用户交互,事件触发,到事件执行一系列过程。

事件合成的概念:React中,事件并不是绑定原声事件,而是通过合成,如onClick由click合成,onChange由blur,change,focus等合成。

事件插件机制

React 有一种事件插件机制,比如上述 onClick 和 onChange ,会有不同的事件插件 SimpleEventPlugin ,ChangeEventPlugin 处理,如

const registrationNameModules = 
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...


    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    onMouseEnter: ['mouseout', 'mouseover'],
    onMouseLeave: ['mouseout', 'mouseover'],
    ...

registrationNameDependencies对象,保存了合成事件和原生事件的联系。

事件绑定

onChange等这些事件,保存在了dom对应的fiber.memoizedProps。

绑定一个onChange,实际上绑定了取出了registrationNameDependencies的onChange的[‘blur’, ‘change’, ‘click’, ‘focus’, ‘input’, ‘keydown’, ‘keyup’, ‘selectionchange’],依次进行addEvenListener注册

事件触发

  • 第一步 批量更新,

首先,通过dom找到fiber。然后开启批量更新,有批量更新开关。

export function batchedEventUpdates(fn,a)
    isBatchingEventUpdates = true; //打开批量更新开关
    try
       fn(a)  // 事件在这里执行
    finally
        isBatchingEventUpdates = false //关闭批量更新开关
    

  • 第二步 合成事件源

通过onClick找到事件插件SimpleEventPlugin,合成新的事件源e,里面就包含了 preventDefault 和 stopPropagation 等方法。

  • 第三部 形成时间队列,模拟俘获冒泡

通过一个数组收集事件,从target开始根据return指针往上找,遇到俘获事件就unshift插入到数组头部,遇到冒泡事件就push到数组尾部。一个循环下来,就收集完所有的事件了。然后根据数组依次执行回调。

React如何模拟阻止事件冒泡
function runEventsInBatch()
    const dispatchListeners = event._dispatchListeners;
    if (Array.isArray(dispatchListeners)) 
    for (let i = 0; i < dispatchListeners.length; i++) 
      if (event.isPropagationStopped())  /* 判断是否已经阻止事件冒泡 */
        break;
          
      dispatchListeners[i](event) /* 执行真正的处理函数 及handleClick1... */
    
  

如果有一个调用了e.stopPropagation(),那么事件源里将有状态证明此次事件已经停止冒泡,下一次循环的时候,event.isPropagationStopped()为ture,,直接跳出当前循环,不再执行接下去的事件。

React调度

v15的react面临着js执行过久的问题,如何解决呢?对比vue

  • vue 有template 模版收集依赖的过程,通过代理可以轻松构建响应式,使得更新的时候,vue可以迅速找到依赖的ui,以组件化的粒度更新组件,渲染视图
  • 而在react中,一次更新无法具体知道波及的范围,只能选择通过root从根节点开始找不同。所以一次更新会花费大量js计算事件。既然如此,react选择了时间分片,让浏览器每一帧只给react一点时间去执行j s,剩余时间作为绘制的时间,防止页面卡顿。与 vue 更快的响应,更精确的更新范围,React 选择更好的用户体验。
Schedule原理。

react通过MessageChannel创建宏任务,并通过最小堆队列,和赋予不同任务优先级,以及过期时间,通过while循环,每执行一段js就判断是否到时间,如果到了,就继续调度宏任务,等下一帧再执行js,这一帧剩余的时间交给浏览器进行其他工作。

为啥不用requestIdleCallback?
  • requestIdleCallback 目前只有谷歌浏览器支持 ,为了兼容每个浏览器,React需要自己实现一个 requestIdleCallback
具备条件
  • 1 实现的这个 requestIdleCallback ,可以主动让出主线程,让浏览器去渲染视图。
  • 2 一次事件循环只执行一次,因为执行一个以后,还会请求下一次的时间片。

满足这两点的只有宏任务了,

  • setTimeout(fn, 0) 可以满足创建宏任务,让出主线程,但是递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒左右,而不是最初的 1 毫秒

Scheduler:

异步调度+调和:

调和

为什么要用fiber?
  • 更新fiber的过程叫做调和器,Reconciler。每一个 fiber 都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间expirationTime( v17 版本叫做优先级 lane )来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染
  • 在浏览器空余时间执行宏任务的时候,通过Schedule调度器,再次恢复执行单元,本质上中断了js执行,提高用户体验。
fiber和React element和dom
  • element 是 React 视图层在代码层级上的表象,也就是开发者写的 jsx 语法,写的元素结构,都会被创建成 element 对象的形式。上面保存了 props , children 等信息。
  • DOM 是元素在浏览器上给用户直观的表象。
  • fiber 可以说是是 element 和真实 DOM 之间的交流枢纽站,一方面每一个类型 element 都会有一个与之对应的 fiber 类型,element 变化引起更新流程都是通过 fiber 层面做一次调和改变,然后对于元素,形成新的 DOM 做视图渲染

双缓存树

  • 如同canvas,优先在内存构建好下一桢的动画,绘制完毕后直接替换,省去了白屏时间,这种在内存中构建并直接替换的技术叫做双缓存。

  • React 用 workInProgress 树(内存中构建的树) 和 current (渲染树) 来实现更新逻辑。双缓存一个在内存中构建,一个渲染视图,两颗树用 alternate 指针相互指向,在下一次渲染的时候,直接复用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样可以防止只用一颗树更新状态的丢失的情况,又加快了 DOM 节点的替换与更新。

两大阶段,render+commit

render阶段包括beginWork和completeWork。

beginWork:是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同effectTag。

completeUnitOfWork:是向上归并的过程,如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等。

BeginWork阶段
  • 对于组件,执行部分生命周期,执行 render ,得到最新的 children (只会处理第一层的儿子,孙子不处理)。
  • 向下遍历调和 children ,复用 oldFiber ( diff 算法)。
  • 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新。

核心函数:reconcileChildren

function reconcileChildren(current,workInProgress)
   if(current === null)  /* 初始化子代fiber  */
        workInProgress.child = mountChildFibers(workInProgress,null,nextChildren,renderExpirationTime)
   else  /* 更新流程,diff children将在这里进行。 */
        workInProgress.child = 				reconcileChildFibers(workInProgress,current.child,nextChildren,renderExpirationTime)
   

对于第一次mount,会调度children,生成子节点的fiber,并通过child和return指针关联起来。

对于第二次update,会diff children,打上对应的effectTag

effectTag的常用例子
export const Placement = /*             */ 0b0000000000010;  // 插入节点
export const Update = /*                */ 0b0000000000100;  // 更新fiber
export const Deletion = /*              */ 0b0000000001000;  // 删除fiebr
export const Snapshot = /*              */ 0b0000100000000;  // 快照
export const Passive = /*               */ 0b0001000000000;  // useEffect的副作用
export const Callback = /*              */ 0b0000000100000;  // setState的 callback
export const Ref = /*                   */ 0b0000010000000;  // ref
completeUnitOfWork
  • 首先 completeUnitOfWork 会将带有 effectTag 的 Fiber 节点会被保存在一条被称为 effectLists 的单向链表中。在 commit 阶段,将不再需要遍历每一个 fiber ,只需要执行更新 effectList 就可以了。
  • completeWork 阶段对于组件处理 context ;对于元素标签初始化,会创建真实 DOM ,将子孙 DOM 节点插入刚生成的 DOM 节点中;会触发 diffProperties 处理 props ,比如事件收集,style,className 处理。

Commit阶段

commit可以分为:

  • Before-mutation之前

  • before-mutation(更新dom之前)

  • mutation(更新dom)

  • Layout(更新dom之后)

  • layout之后

Before-mutation
function commitBeforeMutationEffects() 
  while (nextEffect !== null) 
    const effectTag = nextEffect.effectTag;
    if ((effectTag & Snapshot) !== NoEffect) 
      const current = nextEffect.alternate;
      // 调用getSnapshotBeforeUpdates
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    
    if ((effectTag & Passive) !== NoEffect)  //如果有useEffect的effectTag
       scheduleCallback(NormalPriority, () => 
          flushPassiveEffects(); //异步调用useEffect,这里只是注册。
          return null;
        );
    
    nextEffect = nextEffect.nextEffect;
  

  • 因为 Before mutation 还没修改真实的 DOM ,是获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate ,那么会执行这个生命周期。
  • 会异步调用 useEffect ,在生命周期章节讲到 useEffect 是采用异步调用的模式,其目的就是防止同步执行时阻塞浏览器做视图渲染。
Mutation
function commitMutationEffects()
    while (nextEffect !== null) 
        if (effectTag & Ref)  /* 置空Ref */
            const current = nextEffect.alternate;
            if (current !== null) 
                commitDetachRef(current);
            
        
        switch (primaryEffectTag) 
            case Placement:  //  新增元素
            case Update:     //  更新元素
            case Deletion:   //  删除元素
        
     

  • 置空 ref 。
  • 对新增元素,更新元素,删除元素。进行真实的 DOM 操作。
  • 调用对应的生命周期,比如函数组件的useLayoutEffect的销毁函数。或者类组件的componentWillUnMount等。

Layout

function commitLayoutEffects(root)
     while (nextEffect !== null) 
          const effectTag = nextEffect.effectTag;
          commitLayoutEffectOnFiber(root,current,nextEffect,committedExpirationTime)
          if (effectTag & Ref) 
             commitAttachRef(nextEffect);
          
     

  • commitLayoutEffectOnFiber 对于类组件,会执行生命周期(componentDIdMount,componentDidUpdate),setState 的callback,对于函数组件会执行 useLayoutEffect 钩子。
  • 如果有 ref ,会重新赋值 ref 。

Hooks

Hooks 出现本质上原因是:

  • 1 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
  • 2 解决逻辑复用难的问题。
  • 3 放弃面向对象编程,拥抱函数式编程。

hooks作为函数组件fiber和函数组件之间沟通的桥梁

Hooks对象本质

思考一个问题 React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?

React hooks以三种处理策略存在 React 中:

  • 1 ContextOnlyDispatcher: 第一种形态是防止开发者在函数组件外部调用 hooks ,所以第一种就是报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。
  • 2 HooksDispatcherOnMount: 第二种形态是函数组件初始化 mount ,因为之前讲过 hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooks 与 fiber 之间的关系。
  • 3 HooksDispatcherOnUpdate:第三种形态是函数组件的更新,既然与 fiber 之间的桥已经建好了,那么组件再更新,就需要 hooks 去获取或者更新维护状态。

所有函数组件的触发都在renderWIthHooks执行,可以看这个函数的逻辑。

let currentlyRenderingFiber
function renderWithHooks(current,workInProgress,Component,props)
    currentlyRenderingFiber = workInProgress; //赋值当前fiber,hooks通过这个获取fiber
    workInProgress.memoizedState = null; /* 每一次执行函数组件之前,先清空状态 (用于存放hooks列表)*/
    workInProgress.updateQueue = null;    /* 清空状态(用于存放effect list) */
  
    ReactCurrentDispatcher.current =  current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate /* 判断是初始化组件还是更新组件 */
  
    let children = Component(props, secondArg); /* 执行我们真正函数组件,所有的hooks将依次执行。 */
  
    ReactCurrentDispatcher.current = ContextOnlyDispatcher; /* 将hooks变成第一种,防止hooks在函数组件外部调用,调用直接报错。 */

从上面可以看到,

  • 对于类组件,memoizedState保存着state的信息。对于函数组件,memoizedState保存着hooks列表。

  • 对于类组件,updateQueue存放着update链表等更新信息。而对于函数组件,updateQueue保存着useEffect/useLayoutEffect 产生的副作用组成的链表

  • 在函数真正执行之前,React hooks对象被赋予了真正的Hooks对象,而当函数组件执行完毕之后,hooks对象被重新赋值了报错的对象。这也是解释了为什么hooks只能在函数中执行,因为。引用的 React hooks都是从 ReactCurrentDispatcher.current 中的, React 就是通过赋予 current 不同的 hooks 对象达到监控 hooks 是否在函数组件内部调用

  • 每个Hooks内部可以读取到fiber的原因是因为在函数执行之前,fiber被赋值到了currentlyRenderingFiber,hooks通过currentlyRenderingFiber读取到fiber

为什么hooks不能出现在if else语句。

答:可能会破坏hooks的顺序。

每一个hooks执行的时候,会创建一个hooks对象,hooks对象通过next指针关联。等到更新阶段执行hooks的时候,会复用第一次创建的hooks对象。假设有一个存在条件语句下的hooks,在第一次更新的时候,他执行了,创建了hooks对象。,到第二次更新的时候,他不执行了,而这个时候却少一个hooks来消费hooks对象,会导致出现如下结果:

第二次

hook2服用了hook1的hooks对象,而useRef执行的时候,指向的hooks.next却是useState,也就是hook2的hooks对象,因为useState!== useRef,所以就会报错。这也是为什么hooks不能出现在条件语句的原因,会破坏hooks的结构顺序。

处理useEffect

对于mount的useEffect,执行mountEffect, mountEffect调用mountEffectImpl

// useEffect的mount
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void 
    return mountEffectImpl(
      PassiveEffect |以上是关于掘金小课《React进阶实战指南》笔记的主要内容,如果未能解决你的问题,请参考以下文章

React进阶(十一)实战演练之Button组件(1)

写给初中级前端的高级进阶指南(JSTSVueReact性能学习规划)

登峰造极迎接实战!绝版Spring全家桶阿里高工私享面试进阶笔记,就这还不香?

接口测试框架实战与自动化进阶指南!

Swift:WKWebView 进阶填坑指南

useEffect进阶指南(下)