React 18新特性

React 18 新特性
2022年3月29日发布

1. 新的Root API:createRoot

  • 旧的Root API:即ReactDOM.render.
import React from 'react'
// 18
// import ReactDOM from 'react-dom/client'
// 17
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import ReactDOM from 'react-dom'

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

// 卸载
 ReactDOM.unmountComponentAtNode(document.getElementById('root'))

平滑升级:如果在18中使用这个API,会有一个警告,表明已经被弃用并且换到新的 API,意味着如果直接把项目版本升级到18版本不会造成break change,在整个18版 本中都为可用兼容状态,并保持版本的特性。
在这里插入图片描述

  • 新的Root API:即createRoot.
// 18
import ReactDOM from 'react-dom/client'

const root = ReactDOM.createRoot(document.getElementById('root'))
// 装载
root.render(
 <React.StrictMode>
  <App />
 </React.StrictMode>
)

// 卸载
 root.unmount()

目的:React组件生态很庞大,很多组件会用到ReactDOM.render直接渲染,比如UI库 中的Model.confirm类似的API,这时就需要一个版本的周期让这些生态组件升级上来。

2. useEffect()

 useEffect(() => {
   console.log('组件挂载了')
   return () => {
     console.log('组件卸载了')
   }
 }, [])

在这里插入图片描述
React 18新特性_第1张图片

更新到18后,在开发环境下会挂载两次,避免这种行为的方法是去掉StrictMode标签,但也会把其他提示去掉。

root.render(
// 
 	 <App />
// 
)

还可以用ref解决

 const firstRenderRef = useRef(true)

  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false
      return
    }
    console.log('组件挂载了')
    return () => {
      console.log('组件卸载了')
    }
  }, [])
  return <div></div>

React 18新特性_第2张图片

3. 自动批处理

在React 18以前,React 只能在组件的生命周期函数或者合成事件函数中进行批处理。默认情况下,Promise、setTimeout 以及原生事件中是不会对其进行批处理的。

function App() {
	const [count, setCount] = useState(0)
	const [flag, setFlag] = useState(false)
	console.log('count = ' + count, 'flag = ' + flag)
	function handleClickOne() {
	  setCount(1)
	  setFlag(true)
	  // 批处理:会合并为一次 render
	}
	function handleClickTwo() {
	  Promise.resolve().then(() => {
	    setCount(2)
	  })
	  setFlag(false)
	  // 17 会执行两次 render
	  // 18 合并为一次 render
	}
	 return (
    <div style={{ textAlign: 'center' }}>
      <h3>React17</h3>
      <button onClick={handleClickOne}>批处理</button>
      <button onClick={handleClickTwo}>同步模式</button>
    </div>
  )
}
export default App

React 18新特性_第3张图片

在React 18中,任何情况下都可以合并渲染,上面第二个例子只会有一次的render,因为所有的更新都将自动批处理。但在下面情况会执行两次render:

 async function handleClickThree() {
     await setCount(3)
     setFlag(true)
  // React 18:会执行两次 render
}

React 18新特性_第4张图片

React 18的批处理是安全的,是框架层面自动处理,如果想要退出批处理,可以使用flushSync方法。
flushSync(fn: () => R): R 它接收一个函数作为参数,并且允许有返回值。
:flushSync 具有函数为作用域,函数内部的多个 setState时 仍然为批量更新,这样可以精准控制哪些不需要的批量更新:

const App = () => {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)
  const [loading, setLoading] = useState(true)
  console.log('count = ' + count, 'flag = ' + flag, 'loading = ' + loading)

  function handleClickFlush() {
    flushSync(() => {
      setCount(3)
      setFlag(true)
    })
    // 在setCount 和 setFlag 为批量更新结束后执行
    setLoading(false)
    // 此方法会触发两次 render
  }
  return (
    <div style={{ textAlign: 'center' }}>
      <h3>React18</h3>
      <button onClick={handleClickFlush}>flushSync</button>
    </div>
  )
 }
 export default App

React 18新特性_第5张图片

4. 并发模式

Transition更新分为两类:

  1. 紧急更新:比如打字、点击、拖动等,在直觉上需要立即响应的行为,如果不立即响应会给人感觉出错。
  2. 过渡更新:将UI从一个视图过渡到另一个视图,不需要及时响应,有点延迟但在预期范围内、可接受的。

在React 18之前,所有的更新都没有优先级之分,都是紧急的,意味着两种状态更新会被同时render。有时用户希望第一个更新更加及时,第二个更新可以稍微延迟一会。
startTransition可以解决这个问题。

import React, { startTransition, useState } from 'react'

const [content, setContent] = useState('')
  const [inputValue, setInputValue] = useState('')
  console.log('inputValue = ' + inputValue, 'content = ' + content)

  const onChange = (e) => {
    // 紧急更新
    setInputValue('紧急')

    // 标记回调函数内的更新为 非紧急更新
    startTransition(() => {
      setContent('非紧急')
    })
  }

React 18新特性_第6张图片

简单来说,被startTransition包裹的setState触发的渲染,被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。

我们可以使用 startTransition 包装任何要移至后台的更新,通常,这些类型的更新分为两类:

  1. 渲染缓慢:这些更新需要时间,因为 React 需要执行大量工作才能转换 UI 以显示结果。
  2. 网络慢:这些更新需要时间,因为 React 正在等待来自网络的一些数据。

useTransition:用来降低渲染优先级,减少重复渲染次数。
例如:搜索引擎的关键词联想。一般来说用户在输入框输入都希望是实时更新,如果此时联想词比较多,也要实时更新的话,可能会导致用户输入卡顿,这样使用户的体验感变差。对于用户输入的更新和联想词更新,前者的紧急程度大于后者。
为此提供了一个带有 两个返回值的useTransition,第一个isPending是一个布尔值,这是 react通知我们是否正在等待过度的完成。第二个startTransition是一个接收回调的函数,用来告诉react需要推迟的state。React 将在状态转换期间提供视觉反馈,并在转换发生时保持浏览器响应。

import React, { useState, useTransition } from 'react'

  const [value, setValue] = useState('')
  const [searchQuery, setSearchQuery] = useState([])
  const [loading, startTransition] = useTransition(2000)

  const handleChange = (e) => {
    setValue(e.target.value)
    // 延迟更新
    startTransition(() => {
      setSearchQuery(Array(20000).fill(e.target.value))
    })
    return (
    <div style={{ textAlign: 'center' }}>
	     <h3>React18</h3>
		 <input value={value} onChange={handleChange} />
	     {loading ? (
	     	<p>loading...</p>
	     ) : (
	     	searchQuery.map((item,index) => <p key={index}>{item}</p>
	     )}
    </div>
  )
  }

React 18新特性_第7张图片

useDeferredValue:支持变量延迟更新,同时接受一个可选的延迟更新的最大值。React尝试尽快更新延迟值,如果在给定的timeoutMs期限内未能完成,将强制更新。
优点:能很好地展现并发渲染时优先级调整的特性,可以用于延迟计算逻辑比较复杂的状态,让其他组件优先渲染,等待这个状态更新完后再更新。

import { useState, useDeferredValue } from 'react'

function App() {
  const [text, setText] = useState('111')
  const deferredText = useDeferredValue(text, {
    timeoutMs: 5000
  })

  let arr = []
  for (let i = 0; i < 30000; i++) {
    arr.push(i)
  }

  function handleChange(e) {
    setText(e.target.value)
  }

  return (
    <div style={{ textAlign: 'center' }}>
      <h3>React18</h3>
      <input value={text} onChange={handleChange} />
      <br />
      {deferredText}
      <ul>
        {arr.map((item) => (
          <li key={item}>{deferredText}</li>
        ))}
      </ul>
    </div>
  )
}

export default App

React 18新特性_第8张图片

useDeferredValue 与 useTransition 相同和不同:
相同:useDeferredValue 本质上的内部实现与 useTransition 一样,都是标记成延迟更新任务。
不同:useTransition 是把更新任务变成了延迟更新任务,而 useDeferredValue 是产生一个新的值,这个值作为延时状态。(一个用来包装方法,一个用来包装值)

5. userId()

我们可以使用 React 进行服务端渲染(SSR),在开发模式上,我们可以在客户端与服务端共享同一个 React 组件,但服务端渲染必须保证客户端与服务端生成的HTML结构相匹配,如Math.random()在SSR 面前是没法保证客户端与服务端之间的 id 唯一性。

const id = Math.random();

export default function App() {
  return <div id={id}>Hello</div>
}

如果应用是CSR(客户端渲染),id是稳定的,App组件没有问题。
如果应用是SSR(服务端渲染),那么会经历:
React 18新特性_第9张图片

  1. 脱水(dehydrate):React在服务端渲染,生成随机id(假设为0.1234)
  2. Hello
    作为HTML内容传递给客户端
  3. 注水(hydrate):React在客户端渲染,生成随机id(假设为0.5678)
    客户端、服务端生成id并不匹配,无法简单生成稳定且唯一的id,所以React18推出useId,解决这个问题。
function Checkbox() {
  // 生成唯一、稳定id
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input type="checkbox" name="react" id={id} />
    </>
  );
);

userId可以生成客户端与服务端唯一的id,并返回一个字符串,这样就可以将其作为当前组件的唯一身份。
如果在同一个组件中,我们需要多个 id,那么一定不要重复的使用 useId,而是基于一个 id 来创建不同的身份标识。额外添加不同的字符串即可。

function NameFields() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id + '-firstName'}>First Name</label>
      <div>
        <input id={id + '-firstName'} type="text" />
      </div>
      <label htmlFor={id + '-lastName'}>Last Name</label>
      <div>
        <input id={id + '-lastName'} type="text" />
      </div>
    </div>
  );
}

6.流式服务端渲染

在React18之前,服务器端渲染(SSR),客户端必须一次性等待HTML数据加载到服务器上,并且等待所有JavaScript加载完毕后再开始。不支持客户端渲染常用的代码分割组合为React.lazy和Suspense。
在React 18中,基于全新的 Suspense,支持了流式 SSR,也就是允许服务端一点一点的返回页面。

假设有一个页面,包含了 NavBar、Sidebar、Post、Comments 等几个部分,在传统的 SSR 模式下,我们必须请求到 Post 数据,请求到 Comments 数据后,才能返回完整的 HTML。

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section>
    <!-- Comments -->
    <p>First comment</p>
    <p>Second comment</p>
  </section>
</main>

React 18新特性_第10张图片
但如果 Comments 数据请求很慢,会拖慢整个流程。

在 React 18 中,我们通过 Suspense 包裹,可以告诉 React,我们不需要等这个组件,可以先返回其它内容,等这个组件准备好之后,单独返回。

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

我们通过 Suspense 包裹了 Comments 组件,那服务器首次返回的 HTML 是下面这样的, 组件处通过 loading 进行了占位。

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

React 18新特性_第11张图片
组件准备好之后,React 会通过同一个流(stream)发送给浏览器(res.send 替换成 res.socket),并替换到相应位置。

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

你可能感兴趣的:(react.js,前端)