掘金小课《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’,直接转为字符串

  • 数组类型,如[

    ,
    ] => [{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 元素 比如 
export const HostText = 6; // 文本节点 export const Fragment = 7; // 对应 export const Mode = 8; // 对应 export const ContextConsumer = 9; // 对应 export const ContextProvider = 10; // 对应 export const ForwardRef = 11; // 对应 React.ForwardRef export const Profiler = 12; // 对应 export const SuspenseComponent = 13; // 对应 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里面就可以使用this.props.grandRef来绑定获取ref对象,也可以给ref对象赋值其他的东西
    
  • 组件通信,父组件通过ref控制子组件类实例,或者函数子组件的一些方法,操控子组件,如form的resetFields和setFieldsValue。函数组件没有实例,通过forwardRef + useImperativeHandle控制子组件的一些方法

    function Son(props, ref){
        const iptRef = useRef(null)
        useImperativeHandle(ref,()=>{
            const onSonFocus = () =>{
                iptRef.current.focus()
            }
            const onChangeValue = () =>{
                iptRef.current.value = 'hahahah'
            }
    
            return {
                onSonFocus,
                onChangeValue
            }
        },[])
        
        return 
    }
    const ForSon = React.forwardRef(Son)
    const App = () =>{
        const sonRef = useRef(null)
        return 
    } (ReactDOM as any).createRoot(document.getElementById('root')).render()

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 
}

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

  • 类组件静态属性contextType

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

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

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

在 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。

    
        { React.useMemo(()=>   ,[]) }
    
    

高阶组件

属性代理&反向继承

属性代理
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注册

事件触发

  • 第一步 批量更新,

掘金小课《React进阶实战指南》笔记_第1张图片

首先,通过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:

掘金小课《React进阶实战指南》笔记_第2张图片

异步调度+调和:

掘金小课《React进阶实战指南》笔记_第3张图片

调和

为什么要用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 做视图渲染

掘金小课《React进阶实战指南》笔记_第4张图片

双缓存树

  • 如同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 。

掘金小课《React进阶实战指南》笔记_第5张图片

Hooks

Hooks 出现本质上原因是:

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

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

掘金小课《React进阶实战指南》笔记_第6张图片

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对象,会导致出现如下结果:

掘金小课《React进阶实战指南》笔记_第7张图片

第二次

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 | PassiveStaticEffect, //effectTag标志
      HookPassive, //useEffect的标志
      create,
      deps,
    );
}
// fiberFlags表示effectTag的标志,而hookFlags表示是useEffect的标志
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = mountWorkInProgressHook(); //创建hooks
  const nextDeps = deps === undefined ? null : deps; //依赖
  currentlyRenderingFiber.flags |= fiberFlags; //添加effectTag
  hook.memoizedState = pushEffect( //创建effect保存在hook.memoizedState
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

可以看到,对于useEffect,第一次会创建一个hooks,然后打上标记。最后调用puseEffect创建一个effect存放到hook.memoizedState上。

这里插一嘴,对于useEffect,标志是HookPassive,对于useLayoutEffect,他的执行是mountLayoutEffect

function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  let fiberFlags: Flags = UpdateEffect;
  if (enableSuspenseLayoutEffectSemantics) {
    fiberFlags |= LayoutStaticEffect;
  }
  return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}

这里可以看到,useLayoutEffect的标志是HookLayout,他跟useEffect一样都是调用同一个方法,只不过给了不同的标记。

接着看pushEffect,pushEffect不只会创建effect,如

// 创建effect, tag是effectTag, create是执行函数, deps是依赖项
function pushEffect(tag, create, destroy, deps) {
  // 创建effect
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    // Circular
    next: (null: any),
  };
  // 获取当前fiber的updateQueue,函数组件执行前,fiber已经赋予了currentlyRenderingFiber
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  
  // 通过环状链表将effect存放在fiber.updateQueue上
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

pushEffect创建effect之后,还会将effect以环状链表的形式,存放在fiber.updateQueue上。

这样设计是因为函数组件可能存在多个useEffect和useLayoutEffect,将这些effect收集起来,在commit阶段的时候统一处理。

比如

React.useEffect(()=>{
    console.log('第一个effect')
},[ props.a ])
React.useLayoutEffect(()=>{
    console.log('第二个effect')
},[])
React.useEffect(()=>{
    console.log('第三个effect')
    return () => {}
},[])

updateQueue就是这样的

掘金小课《React进阶实战指南》笔记_第8张图片

更新阶段的useEffect,调用updateEffect,updateEffect调用updateEffectImpl

function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
// useEffect的Update阶段
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  const hook = updateWorkInProgressHook(); //获取hooks
  const nextDeps = deps === undefined ? null : deps; //获取依赖项
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState; //获取上一次的effect
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps; //获取之前的依赖项
      if (areHookInputsEqual(nextDeps, prevDeps)) { //如果依赖项一样,更新当前effect对象就行,无需打上新的标记。
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  // 打上新的标记HookHasEffect,这样commit阶段的时候就会再执行一次。
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

如上,主要做了:

  • 判断依赖项是否一样,一样的话,更新副作用链表就可以了
  • 如果不一样,就会打上新的标记HookHasEffect,在 commit 阶段就会根据这些标签,重新执行副作用。

对于useLayoutEffect,更新阶段调用的函数也是跟useEffect一样。

function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

可以看到,也是调用了updateEffectImpl,只不过标记不同。

effectTag的思考
  • React 会用不同的 EffectTag 来标记不同的 effect,对于useEffect 会标记 UpdateEffect | PassiveEffect, UpdateEffect 是证明此次更新需要更新 effect ,HookPassive 是 useEffect 的标识符,对于 useLayoutEffect 第一次更新会打上 HookLayout 的标识符。

  • React 就是在 commit 阶段,通过标识符,证明是 useEffect 还是 useLayoutEffect ,接下来 React 会同步处理 useLayoutEffect ,异步处理 useEffect

  • 如果函数组件需要更新副作用,会标记 UpdateEffect,至于哪个effect 需要更新,那就看 hooks 上有没有 HookHasEffect 标记,所以初始化或者 deps 不相等,就会给当前 hooks 标记上 HookHasEffect ,所以会执行组件的副作用钩子。

  • useEffect和useLayoutEffect的处理函数是一样的,只不过入参不同。

你可能感兴趣的:(react源码学习解析专栏,前端)