Skip to content

React

JSX.Element 与 React.ReactNode 与 React.ReactElement

jsx 文件就是实现 React.createElement() 的一个语法糖。 React.createElement 方法会根据传入 type 的类型来区分返回普通的 HTMLElement 或者是 React.ReactElement 实例

React.ReactElement 在使用上就是一个含有 typepropskey 的实例对象,由 React 最终渲染成真实的 DOM 元素

ts
// index.d.ts
interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
  type: T
  props: P
  key: Key | null
}

// Playground.tsx
const element: React.ReactElement = React.createElement("div", { className: "test", id: "unique" }, "hello world")
ReactDOM.render(element, document.getElementById("render-container"))
console.log(element)

实际的 ReactElement 对象则具有以下元素:ReactElement

React.ReactNode 是一种联合类型,并且继承了 React.ReactElement

ts
type ReactText = string | number
type ReactChild = ReactElement | ReactText
// ...
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined

JSX.Element 就是通过继承 React.ReactNode 实现的,没有任何区别

区分 class 组件和 function 组件

How Does React Tell a Class from a Function?

为什么要区分

箭头函数的组件没有 this,new 会报错

怎么区分

class 组件 会继承 React.ComponentMyComponent instanceof React.Component

js
// Inside React
class Component {}
Component.prototype.isReactComponent = {}

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent)

ref

class 组件

jsx
import React from "react"

export default class Test extends React.Component {
  ref = React.createRef()

  componentDidUpdate() {
    this.ref.current.focus()
  }

  render() {
    return <input ref={this.ref} />
  }
}

函数组件

jsx
import { useRef } from "react"

export default function Test() {
  const ref = useRef()

  useEffect(() => {
    this.ref.current.focus()
  }, [])

  return <input ref={this.ref} />
}

函数组件不会暴露出自己的实例,需要使用 forwardRef 来包装组件

jsx
import { forwardRef, useImperativeHandle } from "react"

function Child(props, ref) {
  useImperativeHandle(ref, () => {
    return {
      onXXX,
      onResetXXX,
      XXX,
    }
  })

  return <>...</>
}

export default forwardRef(Child)

Fiber

tree

由于 react 和 vue 的响应式实现原理不同,react 每次更新都需要渲染一颗更大的虚拟 DOM 树

fiber tree

在 fiber 出现之前,react 的虚拟 DOM 树只有指向子节点的指针,所以中断渲染,暂存当前的 DOM 节点信息就会丢失父节点和兄弟节点的信息,无法完成遍历
通过 requestIdleCallback 来控制遍历的进度条,决定是否让出线程给其他操作

React 中最多会存在两颗 Fiber 树

  • currentFiber:页面中显示的内容
  • workInProgressFiber:内存中正在重新构建的 Fiber 树

双缓存 Fiber 树

在 React 中最多会同时存在两棵 Fiber 树。当前屏幕上显示内容对应的 Fiber 树称为 current Fiber 树,正在内存中构建的 Fiber 树称为 workInProgress Fiber 树
React 应用的根节点通过 current 指针在不同 Fiber 树的 rootFiber 间切换来实现 Fiber 树的切换
当 workInProgress Fiber 树构建完成交给 Renderer 渲染在页面上后,应用根节点的 current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为 current Fiber 树

Hooks 链表

hooks mount 阶段

当函数式组件初始化时,会调用 renderWithHooks 函数,初始化走 HooksDispatcherOnMount,后续更新走 HooksDispatcherOnUpdate

初始化时,会调用 mountWorkInProgressHook 创建 hook 链表节点,挂载到 fiber 节点的 memoizedState 上(Fiber 上的 memoizedState 指向 hooks 链表,hook 身上的 memoizedState 存储他们自己的状态)

hooks update 阶段

调用 updateWorkInProgressHook 克隆 hook 节点,进行新旧比较

  1. nextCurrentHook 中间变量 记录旧的 hooks 链表
  2. nextWorkInProgressHook 中间变量 克隆旧的 hook 节点形成新的 hooks 链表
  3. currentHook 指向旧 hooks 链表;workInProgressHook 指向新的 hooks 链表,返回 workInProgressHook

合成事件

React 事件系统工作原理

v16 之前所有事件统一绑定在 document 上
v17 绑定在 ReactDOM.render(app, container) 的 container 上

事件绑定

  1. diff 时判断 dom 的 props 是否有合成事件(onXxx)
  2. 判断事件是否有对应的原生事件
  3. 调用 addTrappedEventListener 将真正的事件绑定在顶层

事件触发

  1. 任意一个事件触发,执行 dispatchEvent 函数
  2. 检测到自己需要处理的事件类型时,处理该事件
  3. 找到对应 DOM 节点
  4. 反向触发合成事件链,捕获
  5. 正向触发合成事件链,冒泡

彻底搞懂 React 18 并发机制的原理

异步获取 state

ts
// function
function fn1() {
  setCount(count + 1)
}
function fn2() {
  setTimeout(() => {
    alert(count)
  }, 3000)
}

// class
fn1 = () => {
  this.setState({ count: this.state.count + 1 })
}
fn2 = () => {
  setTimeout(() => {
    alert(this.state.count)
  }, 3000)
}

在 function 中,每次页面更新都会重新触发 function,生成新的函数,异步的 alert 只能拿到那个时候执行的 state。
在 class 中,始终是同一个 class 实例,每次页面更新只会重新出发 render 函数,通过 this 拿到的是最新的 state。
useState 通过链表的结构存在对应的 fiber 节点上,按顺序区分哪个 state 是哪个,所以不允许 if

useEffect 和 useMemo 区别

  • useEffect 是在 DOM 改变之后触发,useMemo 在 DOM 渲染之前就触发,类似 shouldComponentUpdate 生命周期
  • 在 useMemo 中使用 setState 会产生死循环,并且会有警告,因为 useMemo 是在渲染中进行的,你在其中操作 DOM 后,又会导致触发 memo

key

在 diff 算法的实现过程中,如果 key 和 element type 都是相同的话,则通过 useFiber() 函数,基于旧 fiber 节点和新 element 的 props 来复用旧 fiber 节点,否则直接删除旧 fiber 节点,创建新的 fiber 节点。而复用或者创建新的 fiber 节点就意味着复用旧的 DOM 对象或者创建新的 DOM 对象(HostComponent 类型的 fiber 节点所关联的 DOM 对象存放在它的 stateNode 属性上)。

useMemoizedFn 替代 useCallback

ts
function useMemoizedFn(func) {
  if (typeof func !== "function") return

  // 通过 useRef 保持其引用地址不变,并且值能够保持值最新
  const funcRef = useRef(func)
  funcRef.current = useMemo(() => {
    return func
  }, [func])

  const memoizedFn = useRef()

  if (!memoizedFn.current) {
    // 返回的持久化函数,调用该函数的时候,调用原始的函数
    memoizedFn.current = function (...args) {
      return funcRef.current.apply(this, args)
    }
  }

  return memoizedFn.current
}