react 理解 (2), react 17到18变化

1,362 阅读6分钟

React 17

**有三种模式 **

legacy 模式:ReactDom.render(, rootNode),在17中默认是legacy 模式
blocking 模式:ReactDOM.createBlockingRoot(rootNode).render()
concurrent 模式:ReactDOM.createRoot(rootNode).render(),这个模式开启了所有的新功能

批量处理:

legacy模式 在合成事件中自动开启批量处理的功能, 如果在非react事件,想使用批量处理的功能,必须使用unstable_batchedUpdates

在blocking 和 concurrent中,任何setSate在默认情况下都是批量处理

React 18

  1. 放弃了对ie11的支持, React 17的legacy模式,依旧保留(会有一个warning)

Render APi

  1. 入口,卸载和ReactDOM的引入不一样
  // React 17采用的  
  import ReactDOM from 'react-dom'
  
  
  ReactDom.render(<APP />,  rootNode)
  
  // React 18采用的是
  
  import ReactDOM from 'react-dom/client'
  
  ReactDOM.createRoot(rootNode).render(<App />)
  依然保留了legacy模式
  
  
  // 卸载组件
  React 17 
      ReactDOM.unmountComponentAtNode(root); 
  React 18 
      root.unmount();
  1. 删除了render的回调函数
// React 17
const root = document.getElementById('app')
ReactDOM.render(<App />, root, () => { console.log('渲染完成'); })

// React 18
const root = document.getElementById('app')
ReactDOM.createRoot(root).render(<App />)

3.TypeScript 的类型定义

// React 17 
interface TextProps { color: string; } 
const Text: React.FC<TextProps> = ({ children }) => { 
    // 在 React 17 的 FC 中,默认携带了 children 属性 
    return <div>{children}</div>; 
}; 
export default Text; 


// React 18 
interface TextProps { 
    color: string; 
    children?: React.ReactNode; 
} 
const Text: React.FC<TextProps> = ({ children }) => { 
    // 在 React 18 的 FC 中,不存在 children 属性,需要手动申明 
    return <div>{children}</div>; 
}; 
export default Text;

自动批处理(batchedUpdates)

React 17

legacy模式 在合成事件中自动开启批量处理的功能, 如果在非react事件,想使用批量处理的功能,必须使用unstable_batchedUpdates

非react事件: Promise, setTimeout, 原声事件等

eg.1
    // React 事件
    const App = {
    
        const [num, setNum] = useState(0)
        
        const addNum = () => {
            setNum(num => num + 1)
            setNum(num => num + 1)
        }
        
        console.log('App 组件渲染')
        return (
            <div onClick={addNum}>
                点击:{num}
            </div>
        )
    }
    
    
    // 1.点击1次
    
    // 2,打印1次   ‘App 组件渲染’
    
    // 3.点击2次
    
    // 4,打印2次   ‘App 组件渲染’
    
    // React 事件采用自动批处理
    
    
    
eg.2    
    // 使用setTimeout
    // 修改一下addNum 事件
    
    const App = {
    
        const [num, setNum] = useState(0)
        
        const addNum = () => {
            setTimeout(() => {
                setNum(num => num + 1)
                setNum(num => num + 1)
            })
        }
        
        console.log('App 组件渲染')
        return (
            <div onClick={addNum}>
                点击:{num}
            </div>
        )
    }
    
    
    // 1.点击1次
    
    // 2,打印2次   ‘App 组件渲染’
    
    // 3.点击2次
    
    // 4,打印4次   ‘App 组件渲染’
    
    // setTimeout中,setSate 没有合并
    
    
React 18

注意: 在严格模式下会render 2次

请安装了React DevTools,第二次渲染的日志信息将显示为灰色

image.png

React18的出现 添加了React17 在非React事件(如:setTimiout, 原声事件等)不能批量更新的功能

// 使用以上 eg.2

     const App = {
    
        const [num, setNum] = useState(0)
        
        const addNum = () => {
            setTimeout(() => {
                setNum(num => num + 1)
                setNum(num => num + 1)
            })
        }
        
        console.log('App 组件渲染')
        return (
            <div onClick={addNum}>
                点击:{num}
            </div>
        )
    }
    
    
    // 1.点击1次
    
    // 2,打印1次   ‘App 组件渲染’
    
    // 3.点击2次
    
    // 4,打印2次   ‘App 组件渲染’
    
    // setTimeout 已经采用了自动批量更新

以下有一个特殊的案列, 不会合并


import { useState } from 'react'

function App() {

  const [num, setNum] = useState(0)

  const addNum = async() => {
    await setNum(num => num + 1)
    setNum(num => num + 1)
  }

  console.log('App 渲染')
  return (
    <div onClick={addNum} className="App">
        click: {num}
    </div>
  );
}

export default App;

第一个setNum(num => num + 1) 可以理解为peomise.resolve 的时候执行
第二个setNum(num => num + 1) 可以理解为promise.then 的时候执行

flushSync

如果你还是想使用批量更新,那你就不得不考虑 flushSync

import { useState } from 'react'
import { flushSync } from 'react-dom';

function App() {

  const [num, setNum] = useState(0)

  const addNum = () => {
    flushSync(() => {
      setNum(num => num + 1)
    })
    flushSync(() => {
      setNum(num => num + 1)
    })
  }

  console.log('App 渲染', num)
  return (
    <div onClick={addNum} className="App">
        click: {num}
    </div>
  );
}

export default App;

点击click, 会渲染2次

image.png

注意: 如果flushSync里面有多个setState, setState 依旧会批量更新

Strict Mode

当你使用严格模式时,React 会对每个组件进行两次渲染,以便你观察一些意想不到的结果。在 React 17 中,取消了其中一次渲染的控制台日志,以便让日志更容易阅读。

image.png

为了解决社区对这个问题的困惑,在 React 18 中,官方取消了这个限制。如果你安装了React DevTools,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台

Hook Api

useDeferredValue

useDeferredValue接受一个值并返回该值的新副本,该副本将推迟到更紧急的更新。如果当前渲染是紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值(可以理解成:React 将在其他工作完成后立即进行更新)

如:当前渲染是紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。

本质上和 startTransition 是一个延时更新的任务

没有使用useDeferredValue的执行堆栈图


  const [list, setList] = useState([]);
  useEffect(() => {
    setList(new Array(10000).fill(null));
  }, []);

  return (
    <>
      {list.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );

image.png

使用useDeferredValue的执行堆栈图


  const [list, setList] = useState([]);
  useEffect(() => {
    setList(new Array(10000).fill(null));
  }, []);
  
  const deferredList = useDeferredValue(list);

  return (
    <>
      {deferredList.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
    </>
  );

image.png

通过对比,可以看到任务被拆分到每一帧不同的 task 中,

useDeferredValue的更多信息

useTransition

useTransition和useDeferredValue 作用基本类似,都是标记成了延迟更新任务。

不同:useTransition 是把更新任务变成了延迟更新任务,而 useDeferredValue 是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)

startTransition: 允许您将提供的回调中的更新标记为转换(简单来说,就是被 startTransition、 回调包裹的 setState 触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占)

isPending: 指示转换何时处于活动状态以显示挂起状态

function App() {

  const [isPending, startTransition] = useTransition();
  const [list, setList] = useState([]);

  function handleClick() {
    startTransition(() => setList(new Array(10000).fill(null)))
  }

  console.log(isPending, 'isPending')

  return (
    <>
      {
        !isPending && list.map((_, i) => (
          <div key={i}>{i}</div>
        ))
      }
      
      <button onClick={handleClick}>click</button>
    </>
  );

}

export default App;

点击click, isPending的状态

image.png

useId

useId是一个钩子,用于生成在服务器和客户端之间稳定的唯一 ID,同时避免hydration mismatches

Note: useId不适用于在列表中生成key。key应该从您的数据中生成。

useSyncExternalStore

useSyncExternalStore 是一个新的api,经历了一次修改,由 useMutableSource 改变而来,主要用来解决外部数据tearing问题

(tearing): 假设我们的渲染任务被分成了两部分(注意不是render两次,而是一次render渲染分成两部分),两部分都读取了外部状态A。如果两部分任务之间,浏览器处理了一个事件使得外部状态A发生了变化,那前半部分的任务读取到的是旧值,后半部分读取到的却是新值,这就造成了渲染结果的不一致性

tearing相关文章

useSyncExternalStore 主要对于框架开发者,比如 redux,它在控制状态时可能并非直接使用的 Reactstate,而是自己在外部维护了一个 store 对象,用发布订阅模式实现了数据更新,脱离了 React 的管理,也就无法依靠 React 自动解决撕裂问题。因此 React 对外提供了这样一个 API

useSyncExternalStore相关文章

useInsertionEffect
function useCSS(rule) {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}

function Component() {
  let className = useCSS(rule);
  return <div className={className} />;
}

这个 Hooks 只建议 css-in-js 库来使用。 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 之前,只是此时无法访问 DOM 节点的引用,一般用于提前注入 <style> 脚本。

参考链接

# React18 新特性解读 react-18-discussions