历史演进中的 react

前言

react 版本更新中,主要有几个具有代表性的版本,分别是 react16react17react18。在日常的开发中可能会不同的项目会遇到使用不同的版本的情况,所以我们需要了解不同版本的 react 的特性,以及在使用不同版本的 react 时需要注意的地方。

react16

react16 中,更新加了很多新特性,其中最大的更新就是更改了 react 类组件的生命周期。那到底是什么原因导致需要更新类组件的生命周期呢?

其实最大原因是 react 内部的架构做了更新,改成了 fiber 架构,这个架构的目的是为了解决 react 在渲染大量 dom 时,会出现卡顿的问题。在 react16 之前,react 的渲染是同步的,也就是说在渲染的过程中,如果遇到了耗时的任务,那么就会导致页面卡顿,用户体验不好。而在 react16 中,react 的渲染是异步的,也就是说在渲染的过程中,如果遇到了耗时的任务,那么就会先把这个任务挂起,等到浏览器空闲的时候再去执行这个任务,这样就不会导致页面卡顿了。但是这样的话,就会导致 react 的生命周期的执行顺序发生变化,所以 react16 就更新了生命周期的执行顺序。

另一个最大的更新就是在这个版本中引入了 hooks 概念,让函数组件也能拥有类组件的功能,这样就可以让函数组件也能拥有状态了。

接下来看下具体更新了哪些内容和 api 以及使用的时候需要注意的地方。

render 支持返回数组和字符串

react16 之前,render 函数只能返回一个元素,如果需要返回多个元素,那么就需要用一个 div 包裹起来,但是在 react16 中,render 函数支持返回数组和字符串了,所以就不需要用 div 包裹起来了。

jsx 复制代码
// react16之前
render() {
  return (
    <div>
      <div>1</div>
      <div>2</div>
    </div>
  )
}

// react16
render() {
  return [
    <div>1</div>,
    <div>2</div>
  ]
}

新增了 componentDidCatch 生命周期

react16 版本之前,如果在组件内部出现了错误,那么就会导致整个页面崩溃,而在 react16 中,新增了 componentDidCatch 生命周期,可以捕获组件内部的错误,从而避免页面崩溃。

jsx 复制代码
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true }
  }
  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo)
  }
  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>
    }
    return this.props.children
  }
}

createPortal

在 react16 中新增了 createPortal 方法,可以将组件渲染到指定的 dom 节点上。

jsx 复制代码
class Modal extends React.Component {
  constructor(props) {
    super(props)
    this.el = document.createElement('div')
  }
  componentDidMount() {
    modalRoot.appendChild(this.el)
  }
  componentWillUnmount() {
    modalRoot.removeChild(this.el)
  }
  render() {
    return ReactDOM.createPortal(this.props.children, this.el)
  }
}

Fragment

在 react16 中新增了 Fragment,可以用来代替 div,但是不会在页面上渲染出来。不会添加额外的 dom 节点。

jsx 复制代码
render() {
  return (
    <React.Fragment>
      <div>1</div>
      <div>2</div>
    </React.Fragment>
  )
}

也可以简写成

jsx 复制代码
render() {
  return (
    <>
      <div>1</div>
      <div>2</div>
    </>
  )
}

新增了 ref 转发的 api createRef / forwardRef

在 react16 中,可以通过 ref 转发,将 ref 传递给子组件。

jsx 复制代码
function FancyButton(props) {
  return (
    <button ref={props.ref} className="FancyButton">
      {props.children}
    </button>
  )
}

const ref = React.createRef()
;<FancyButton ref={ref}>Click me!</FancyButton>

新增了 context api

在 react16 中,新增了 context api,可以用来跨层级传递数据。

jsx 复制代码
const ThemeContext = React.createContext('light')

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 "dark" 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    )
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  )
}

function ThemedButton(props) {
  // ThemedButton 组件从 context 接收 theme
  return (
    <ThemeContext.Consumer>{(theme) => <Button {...props} theme={theme} />}</ThemeContext.Consumer>
  )
}

更改了生命周期的执行顺序

在 react16 之前,react 的生命周期的执行顺序是这样的:

而在 react16 中,react 的生命周期的执行顺序是这样的:

Strict Mode 严格模式

在 react16 中,新增了 Strict Mode 严格模式,可以用来检测项目中的问题。

jsx 复制代码
import React from 'react'

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  )
}

memo api

在 react16 中,新增了 memo api,可以用来优化函数组件的性能。

jsx 复制代码
const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
})

lazy/suspense api

在 react16 中,新增了 lazy/suspense api,可以用来实现组件的懒加载。

jsx 复制代码
const OtherComponent = React.lazy(() => import('./OtherComponent'))

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  )
}

getDerivedStateFromError

在 react16 中,新增了 getDerivedStateFromError api,可以用来捕获组件内部的错误。

jsx 复制代码
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true }
  }
  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo)
  }
  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>
    }
    return this.props.children
  }
}

hooks

在 react16 中,新增了 hooks,可以让函数组件也能拥有类组件的功能,这样就可以让函数组件也能拥有状态了。

jsx 复制代码
import React, { useState } from 'react'

function Example() {
  // 声明一个叫 "count" 的 state 变量。
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

react17

在 react17 中,并没有新增一些新的 api,主要是对 react16 中的 api 做了一些调整,主要是对 react 本身的一次优化升级,为了更好的支持 react18。

去除事件池机制

在 react16 中,事件处理函数的执行是异步的,也就是说在事件处理函数中,如果需要获取到事件对象,那么就需要通过 event.persist()来获取,因为在事件处理函数执行完之后,事件对象就会被回收,所以需要通过 event.persist()来获取。而在 react17 中,事件处理函数的执行是同步的,也就是说在事件处理函数中,可以直接获取到事件对象,不需要通过 event.persist()来获取了。

ts 复制代码
// react16
function handleClick(event) {
  event.persist()
  setTimeout(function () {
    console.log(event.target)
  }, 1000)
}

// react17
function handleClick(event) {
  setTimeout(function () {
    console.log(event.target)
  }, 1000)
}

更改了事件委托的根节点

在 react16 中,事件委托的根节点是 document,而在 react17 中,事件委托的根节点是 root 根节点。

jsx 复制代码
import { useState, useEffect } from 'react'

function App() {
  const [isShowText, setIsShowText] = useState(false)

  const handleShowText = (e: React.MouseEvent) => {
    // e.stopPropagation() // v16无效
    // e.nativeEvent.stopImmediatePropagation() // 阻止监听同一事件的其他事件监听器被调用
    setIsShowText(true)
  }

  useEffect(() => {
    document.addEventListener('click', () => {
      setIsShowText(false)
    })
  }, [])

  return (
    <div className="App">
      <button onClick={handleShowText}>事件委托变更</button>

      {isShowText && <div>展示文字</div>}
    </div>
  )
}

如上代码,在 react16 和 v17 版本,点击按钮时,都不会显示文字。这是因为 react 的合成事件是基于事件委托的,有事件冒泡,先执行 React 事件,再执行 document 上挂载的事件。

v16:出于对冒泡的了解,我们直接在按钮事件上加 e.stopPropagation(),这样就不会冒泡到 document,isShowText 也不会被置为 false 了。但由于 v16 版本的事件委托是绑在 document 上的,它的事件源跟 document 就是同级了,而不是上下级,所以 e.stopPropagation()并没有起作用。如果要阻止冒泡,可以使用原生的 e.nativeEvent.stopImmediatePropagation()阻止同级冒泡,这样文字就可以显示了。

v17:由于事件委托到根目录 root 节点,与 document 属于上下级关系,所以可以直接使用 e.stopPropagation()阻止

stopImmediatePropagation() 方法可以阻止监听同一事件的其他事件监听器被调用 这种更新不仅方便了局部使用 React 的项目,还可以用于项目的渐进式升级,解决不同版本的 React 组件嵌套使用时,e.stopPropagation()无法正常工作的问题

JSX 转换器

在 react17 中,新增了 JSX 转换器,可以让我们在不引入 react 的情况下,使用 jsx 语法。

jsx 复制代码
function App() {
  return (
    <div>
      <h1>hello world</h1>
    </div>
  )
}

副作用清理时机

jsx 复制代码
useEffect(() => {
  // This is the effect itself.
  return () => {
    // This is its cleanup.
  }
})

v17 前,组件被卸载时,useEffect 的清理函数都是同步运行的;对于大型应用程序来说,同步会减缓屏幕的过渡(如切换标签)

v17 后,useEffect 副作用清理函数是异步执行的,如果要卸载组件,则清理会在屏幕更新后运行

此外,v17 将在运行任何新副作用之前执行所有副作用的清理函数(针对所有组件),v16 只对组件内的副作用保证这种顺序。

不过需要注意

jsx 复制代码
useEffect(() => {
  someRef.current.someSetupMethod()
  return () => {
    someRef.current.someCleanupMethod()
  }
})

问题在于 someRef.current 是可变的且因为异步的,在运行清除函数时,它可能已经设置为 null。

jsx 复制代码
// 用一个变量量在 ref 每次变化时,将 someRef.current 保存起来,放到副作用清理回调函数的闭包中,来保证不可变性。
useEffect(() => {
  const instance = someRef.current
  instance.someSetupMethod()

  return () => {
    instance.someCleanupMethod()
  }
})

或者用 useLayoutEffect

jsx 复制代码
useLayoutEffect(() => {
  someRef.current.someSetupMethod()
  return () => {
    someRef.current.someCleanupMethod()
  }
})

useLayoutEffect 可以保证回调函数同步执行,这样就能确保 ref 此时还是最后的值。

返回一致的 undefined 错误

在 v17 以前,组件返回 undefined 始终是一个错误。但是有漏网之鱼,React 只对类组件和函数组件执行此操作,但并不会检查 forwardRef 和 memo 组件的返回值。

jsx 复制代码
function Button() {
  return // Error: Nothing was returned from render
}

function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  ;<button />
}

在 v17 中修复了这个问题,forwardRef 和 memo 组件的行为会与常规函数组件和类组件保持一致,在返回 undefined 时会报错

jsx 复制代码
let Button = forwardRef(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  ;<button />
})

let Button = memo(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  ;<button />
})

react18

在 react18 中,新增了很多新特性,最重要的更新就是新增了 concurrent mode 并发模式,可以让 react 的渲染更加的流畅。

concurrent mode 并发模式下的 render api

在 react18 中,新增了 concurrent mode 并发模式,可以让 react 的渲染更加的流畅。

jsx 复制代码
// v17
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))

// v18
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(<App />)

自动批处理

批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能

在 v17 的批处理只会在事件处理函数中实现,而在 Promise 链、异步代码、原生事件处理函数中失效。而 v18 则所有的更新都会自动进行批处理。

jsx 复制代码
// v17
const handleBatching = () => {
  // re-render 一次,这就是批处理的作用
  setCount((c) => c + 1)
  setFlag((f) => !f)
}

// re-render两次
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
}, 0)

// v18
const handleBatching = () => {
  // re-render 一次
  setCount((c) => c + 1)
  setFlag((f) => !f)
}
// 自动批处理:re-render 一次
setTimeout(() => {
  setCount((c) => c + 1)
  setFlag((f) => !f)
}, 0)

如果在某些场景不想使用批处理,可以使用 flushSync 退出批处理,强制同步执行更新。

flushSync

flushSync 会以函数为作用域,函数内部的多个 setState 仍然是批量更新

jsx 复制代码
const handleAutoBatching = () => {
  // 退出批处理
  flushSync(() => {
    setCount((c) => c + 1)
  })
  flushSync(() => {
    setFlag((f) => !f)
  })
}

Suspense 支持 SSR

SSR 一次页面渲染的流程:

  • 服务器获取页面所需数据
  • 将组件渲染成 HTML 形式作为响应返回
  • 客户端加载资源
  • (hydrate)执行 JS,并生成页面最终内容
  • 上述流程是串行执行的,v18 前的 SSR 有一个问题就是它不允许组件"等待数据",必须收集好所有的数据,才能开始向客户端发送 HTML。如果其中有一步比较慢,都会影响整体的渲染速度。

v18 中使用并发渲染特性扩展了 Suspense 的功能,使其支持流式 SSR,将 React 组件分解成更小的块,允许服务端一点一点返回页面,尽早发送 HTML 和选择性的 hydrate, 从而可以使 SSR 更快的加载页面

jsx 复制代码
<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

startTransition

Transitions 是 React 18 引入的一个全新的并发特性。它允许你将标记更新作为一个 transitions(过渡),这会告诉 React 它们可以被中断执行,并避免回到已经可见内容的 Suspense 降级方案。本质上是用于一些不是很急迫的更新上,用来进行并发控制

在 v18 之前,所有的更新任务都被视为急迫的任务,而 Concurrent Mode 模式能将渲染中断,可以让高优先级的任务先更新渲染。

React 的状态更新可以分为两类:

  • 紧急更新:比如点击按钮、搜索框打字是需要立即响应的行为,如果没有立即响应给用户的体验就是感觉卡顿延迟

  • 过渡/非紧急更新:将 UI 从一个视图过渡到另一个视图。一些延迟可以接受的更新操作,不需要立即响应 startTransition API 允许将更新标记为非紧急事件处理,被 startTransition 包裹的会延迟更新的 state,期间可能被其他紧急渲染所抢占。因为 React 会在高优先级更新渲染完成之后,才会渲染低优先级任务的更新

React 无法自动识别哪些更新是优先级更高的。比如用户的键盘输入操作后,setInputValue 会立即更新用户的输入到界面上,是紧急更新。而 setSearchQuery 是根据用户输入,查询相应的内容,是非紧急的。

jsx 复制代码
const [inputValue, setInputValue] = useState()

const onChange = (e) => {
  setInputValue(e.target.value) // 更新用户输入值(用户打字交互的优先级应该要更高)
  setSearchQuery(e.target.value) // 更新搜索列表(可能有点延迟,影响)
}

return <input value={inputValue} onChange={onChange} />

React 无法自动识别,所以它提供了 startTransition 让我们手动指定哪些更新是紧急的,哪些是非紧急的,从而让我们改善用户交互体验。

jsx 复制代码
// 紧急的更新
setInputValue(e.target.value)
// 开启并发更新
startTransition(() => {
  setSearchQuery(input) // 非紧急的更新
})

useTransition

当有过渡任务(非紧急更新)时,我们可能需要告诉用户什么时候当前处于 pending(过渡) 状态,因此 v18 提供了一个带有 isPending 标志的 Hook useTransition 来跟踪 transition 状态,用于过渡期。

useTransition 执行返回一个数组。数组有两个状态值:

  • isPending: 指处于过渡状态,正在加载中
  • startTransition: 通过回调函数将状态更新包装起来告诉 React 这是一个过渡任务,是一个低优先级的更新
jsx 复制代码
function TransitionTest() {
  const [isPending, startTransition] = useTransition()
  const [count, setCount] = useState(0)

  function handleClick() {
    startTransition(() => {
      setCount((c) => c + 1)
    })
  }

  return (
    <div>
      {isPending && <div>spinner...</div>}
      <button onClick={handleClick}>{count}</button>
    </div>
  )
}

直观感觉这有点像 setTimeout,而防抖节流其实本质也是 setTimeout,区别是防抖节流是控制了执行频率,让渲染次数减少了,而 v18 的 transition 则没有减少渲染的次数。

useDeferredValue

useDeferredValue 和 useTransition 一样,都是标记了一次非紧急更新。useTransition 是处理一段逻辑,而 useDeferredValue 是产生一个新状态,它是延时状态,这个新的状态则叫 DeferredValue。所以使用 useDeferredValue 可以推迟状态的渲染

useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到紧急更新之后。如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。

jsx 复制代码
function Typeahead() {
  const query = useSearchQuery('')
  const deferredQuery = useDeferredValue(query)

  // Memoizing 告诉 React 仅当 deferredQuery 改变,
  // 而不是 query 改变的时候才重新渲染
  const suggestions = useMemo(() => <SearchSuggestions query={deferredQuery} />, [deferredQuery])

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading results...">{suggestions}</Suspense>
    </>
  )
}

这样一看,useDeferredValue 直观就是延迟显示状态,那用防抖节流有什么区别呢?

如果使用防抖节流,比如延迟 300ms 显示则意味着所有用户都要延时,在渲染内容较少、用户 CPU 性能较好的情况下也是会延迟 300ms,而且你要根据实际情况来调整延迟的合适值;但是 useDeferredValue 是否延迟取决于计算机的性能。

useId

useId 支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级结构。

jsx 复制代码
function Checkbox() {
  const id = useId()
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react" />
    </>
  )
}

useSyncExternalStore

useSyncExternalStore 一般是第三方状态管理库使用如 Redux。它通过强制的同步状态更新,使得外部 store 可以支持并发读取。它实现了对外部数据源订阅时不再需要 useEffect

jsx 复制代码
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

useInsertionEffect

useInsertionEffect 仅限于 css-in-js 库使用。它允许 css-in-js 库解决在渲染中注入样式的性能问题。 执行时机在 useLayoutEffect 之前,只是此时不能使用 ref 和调度更新,一般用于提前注入样式。

jsx 复制代码
useInsertionEffect(() => {
  console.log('useInsertionEffect 执行')
}, [])
相关推荐
hong_zc5 分钟前
初始 html
前端·html
小小吱10 分钟前
HTML动画
前端·html
Bio Coder27 分钟前
学习用 Javascript、HTML、CSS 以及 Node.js 开发一个 uTools 插件,学习计划及其周期
javascript·学习·html·开发·utools
糊涂涂是个小盆友31 分钟前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
浮华似水1 小时前
Javascirpt时区——脱坑指南
前端
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i1 小时前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠1 小时前
前端(1)——快速入门HTML
前端·html
凹凸曼打不赢小怪兽2 小时前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar2 小时前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes