乐趣区

关于前端:一文解读-React-17-与-React-18-的更新变化

前言

我的项目目前 react17 和 react18 都有应用,但在开发者角度绝大部分场景还是感知不到多大变动,但也要具体理解分明具体更新了什么。本文就来一次性梳理下 react17 与 react18 的变动。

React 17 更新

首先,官网公布日志称 react17 最大的特点就是无新个性,这个版本次要指标是让 React 能渐进式降级,它容许多版本混用共存,能够说是为更远的将来版本做筹备了。

去除事件池

在 React17 之前,如果应用异步的形式来获取事件 e 对象,会发现合成事件对象被销毁,如下:

function App() {const handleClick = (e: React.MouseEvent) => {console.log('间接打印 e', e.target) // <button>React 事件池 </button>
    // v17 以下在异步办法拿不到事件 e,必须先调用 e.persist()
    // e.persist()

    // 异步形式获取事件 e
    setTimeout(() => {console.log('setTimeout 打印 e', e.target) // null
    })
  }
  return (
    <div className="App">
      <button onClick={handleClick}>React 事件池 </button>
    </div>
  )
}

如果你须要在事件处理函数运行之后获取事件对象的属性,你须要调用 e.persist(),它会将以后的合成事件从事件池中删除,并容许保留对事件的援用。

事件池:合成事件对象会被放入池中对立治理。这意味着合成事件对象能够被复用,当所有事件处理函数被调用之后,其所有属性都会被回收开释置空。

事件池的益处是在较旧浏览器中重用了不同事件的事件对象以进步性能,但它对古代浏览器的性能优化微不足道,反而给开发者带来困惑,因而去除了事件池,因而也没有了事件复用机制。

function App() {// v17 去除了 React 事件池,异步形式应用 e 不再须要 e.persist()
  const handleClick = (e: React.MouseEvent) => {console.log('间接打印 e', e.target) // <button>React 事件池 </button>

    setTimeout(() => {console.log('setTimeout 打印 e', e.target) // <button>React 事件池 </button>
    })
  }
  return (
    <div className="App">
      <button onClick={handleClick}>React 事件池 </button>
    </div>
  )
}

事件委托到根节点

reactv17 前,React 将事件委托到 document 上,在 react17 中,则委托到根节点

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
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()无奈失常工作的问题

更贴近原生浏览器事件

对事件零碎进行了一些较小的更改:

  • onScroll 事件不再冒泡,以防止出现常见的混同
  • React 的 onFocusonBlur 事件已在底层切换为原生的 focusinfocusout 事件。它们更靠近 React 现有行为,有时还会提供额定的信息。

blur、focus 和 focusin、focusout 的区别:blur、focus 不反对冒泡,focusin、focusout 反对冒泡

  • 捕捉事件(例如,onClickCapture)当初应用的是理论浏览器中的捕捉监听器。

这些更改会使 React 与浏览器行为更靠近,并进步了互操作性。

只管 React 17 底层已将 onFocus 事件从 focus 切换为 focusin,但请留神,这并未影响冒泡行为。在 React 中,onFocus 事件总是冒泡的,在 React 17 中会持续放弃,因为通常它是一个更有用的默认值

全新的 JSX 转换器

总结下来就是两点:

  • jsx() 函数替换 React.createElement()
  • 运行时主动引入 jsx() 函数,无需手写引入react

在 v16 中,咱们写一个 React 组件,总要引入

import React from 'react'

这是因为在浏览器中无奈间接应用 jsx,所以要借助工具如 @babel/preset-react 将 jsx 语法转换为 React.createElement 的 js 代码,所以须要显式引入 React,能力失常调用 createElement。

通过React.createElement() 创立元素是比拟频繁的操作,自身也存在一些问题,无奈做到性能优化,具体可见官网优化的 动机

v17 之后,React 与 Babel 官网进行单干,间接通过将 react/jsx-runtime 对 jsx 语法进行了新的转换而不依赖 React.createElement,因而 v17 应用 jsx 语法能够不引入 React,应用程序仍然能失常运行。

function App() {return <h1>Hello World</h1>;}

// 新的 jsx 转换为
// 由编译器引入(禁止本人引入!)import {jsx as _jsx} from 'react/jsx-runtime';

function App() {return _jsx('h1', { children: 'Hello world'});
}

如何降级至新的 JSX 转换

  • 更新至反对新转换的 React 版本(v17)

如果你还在应用 v16,也可降级至 React v16.14.0 的版本,官网在该版本也反对这个个性。

  • 批改配置
  1. @babel/preset-react编译减少 runtime: 'automatic'配置
// 如果你应用的是 @babel/preset-react
{
  "presets": [
    ["@babel/preset-react", {"runtime": "automatic"}]
  ]
}

// 如果你应用的是 @babel/plugin-transform-react-jsx
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {"runtime": "automatic"}]
  ]
}
  1. 批改 tsconfig.json 配置,具体配置可见 TS 官网文档
{
    "compilerOptions": {
        // "jsx": "react",
        "jsx": "react-jsx",
    },
}

从 Babel 8 开始,”automatic” 会将两个插件默认集成在 rumtime 中

副作用清理机会

useEffect(() => {
  // This is the effect itself.
  return () => {// This is its cleanup.}
})
  • v17 前,组件被卸载时,useEffect的清理函数都是同步运行的;对于大型应用程序来说,同步会减缓屏幕的过渡(如切换标签)
  • v17 后,useEffect 副作用清理函数是 异步执行 的,如果要卸载组件,则清理会在屏幕更新后运行

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

不过须要留神

useEffect(() => {someRef.current.someSetupMethod();
  return () => {someRef.current.someCleanupMethod();
  };
});

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

// 用一个变量量在 ref 每次变动时,将 someRef.current 保存起来,放到副作用清理回调函数的闭包中,来保障不可变性。useEffect(() => {
  const instance = someRef.current
  instance.someSetupMethod()
  
  return () => {instance.someCleanupMethod()
  }
})

或者用 useLayoutEffect

useLayoutEffect(() => {someRef.current.someSetupMethod();
  return () => {someRef.current.someCleanupMethod();
  };
});

useLayoutEffect 能够保障回调函数同步执行,这样就能确保 ref 此时还是最初的值。

返回统一的 undefined 谬误

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

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 时会报错

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 />;
});

原生组件栈

v16 中谬误调用栈的毛病:

  • 短少源码地位追溯,在控制台无奈点击跳转到到出错的中央
  • 无奈实用于生产环境

整体来说不如原生的 JavaScript 调用栈,不同于惯例压缩后的 JavaScript 调用栈,它们能够通过 sourcemap 的模式主动复原到原始函数的地位,而应用 React 组件栈,在生产环境下必须在调用栈信息和 bundle 大小间进行抉择。

在 v17 应用了不同的机制生成组件调用栈,间接从 JavaScript 原生谬误栈生成的,所以在生产环境也能按 sourcemap 还原回来,且反对点击跳到源码地位。

想具体理解的可见该 PR

移除公有导出 API

v17 删除了一些公有 API,次要是 React Native for Web 应用的

另外,还删除了 ReactTestUtils.SimulateNative 工具办法,因为其行为与语义不符,如果你想要一种简便的形式来触发测试中原生浏览器的事件,可间接应用 React Testing Library

启发式更新算法更新

援用 React17 新个性:启发式更新算法

  • React16 的 expirationTimes 模型只能辨别是否 >=expirationTimes决定节点是否更新。
  • React17 的 lanes 模型能够选定一个更新区间,并且动静的向区间中增减优先级,能够解决更细粒度的更新。

这个我目前也不是太分明具体算法,先不开展了有趣味的可去查阅相干材料

React 18 更新

并发模式

v18 的新个性是应用古代浏览器的个性构建的,彻底放弃对 IE 的反对。

v17 和 v18 的区别就是:从同步不可中断更新变成了异步可中断更新,v17 能够通过一些试验性的 API 开启并发模式,而 v18 则全面开启并发模式。

并发模式可帮忙利用放弃响应,并依据用户的设施性能和网速进行适当的调整,该模式通过使渲染可中断来修复阻塞渲染限度。在 Concurrent 模式中,React 能够同时更新多个状态。

这里参考下文辨别几个概念:

  • 并发模式 是实现 并发更新 的基本前提
  • v18 中,以是否应用 并发个性 作为是否开启 并发更新 的根据。
  • 并发个性 指开启 并发模式 后能力应用的个性,比方:useDeferredValue/useTransition

可浏览参考 Concurrent Mode(并发模式)

更新 render API

v18 应用 ReactDOM.createRoot() 创立一个新的根元素进行渲染,应用该 API,会主动启用并发模式。如果你降级到 v18,但没有应用 ReactDOM.createRoot() 代替 ReactDOM.render() 时,控制台会打印谬误日志要揭示你应用 React,该正告也象征此项变更没有造成 breaking change,而能够并存,当然尽量是不倡议。

// 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') as HTMLElement).render(<App />)

主动批处理

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

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

// 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 会以函数为作用域,函数外部的多个 setState 依然是批量更新

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

Suspense 反对 SSR

SSR 一次页面渲染的流程:

  1. 服务器获取页面所需数据
  2. 将组件渲染成 HTML 模式作为响应返回
  3. 客户端加载资源
  4. (hydrate)执行 JS,并生成页面最终内容

上述流程是串行执行的,v18 前的 SSR 有一个问题就是它不容许组件 ” 期待数据 ”,必须收集好所有的数据,能力开始向客户端发送 HTML。如果其中有一步比较慢,都会影响整体的渲染速度。

v18 中应用并发渲染个性扩大了 Suspense 的性能,使其反对流式 SSR,将 React 组件分解成更小的块,容许服务端一点一点返回页面,尽早发送 HTML 和选择性的 hydrate,从而能够使 SSR 更快的加载页面

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

具体可参考文章 React 18 中新的 Suspense SSR 架构

startTransition

Transitions 是 React 18 引入的一个全新的并发个性。它容许你将标记更新作为一个 transitions(过渡),这会通知 React 它们能够被中断执行,并防止回到曾经可见内容的 Suspense 降级计划。实质上是用于一些不是很急切的更新上,用来进行并发管制

在 v18 之前,所有的更新工作都被视为急切的工作,而 Concurrent Mode 模式能将渲染中断,能够让高优先级的工作先更新渲染。

React 的状态更新能够分为两类:

  • 紧急更新:比方点击按钮、搜寻框打字是须要立刻响应的行为,如果没有立刻响应给用户的体验就是感觉卡顿提早
  • 过渡 / 非紧急更新:将 UI 从一个视图过渡到另一个视图。一些提早能够承受的更新操作,不须要立刻响应

startTransition API 容许将更新标记为非紧急事件解决,被 startTransition 包裹的会提早更新的 state,期间可能被其余紧急渲染所抢占。因为 React 会在高优先级更新渲染实现之后,才会渲染低优先级工作的更新

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

const [inputValue, setInputValue] = useState()

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

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

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

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

这里有个比拟好的在线例子,能够间接感触到 startTransition的优化

useTransition

当有过渡工作(非紧急更新)时,咱们可能须要通知用户什么时候以后处于 pending(过渡)状态,因而 v18 提供了一个带有 isPending 标记的 Hook useTransition来跟踪 transition 状态,用于过渡期。

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

  • isPending: 指处于过渡状态,正在加载中
  • startTransition: 通过回调函数将状态更新包装起来通知 React 这是一个过渡工作,是一个低优先级的更新
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

useDeferredValueuseTransition 一样,都是标记了一次非紧急更新。useTransition是解决一段逻辑,而 useDeferredValue 是产生一个新状态,它是延时状态,这个新的状态则叫 DeferredValue。所以应用 useDeferredValue 能够推延状态的渲染

useDeferredValue 承受一个值,并返回该值的新正本,该正本将推延到紧急更新之后。如果以后渲染是一个紧急更新的后果,比方用户输出,React 将返回之前的值,而后在紧急渲染实现后渲染新的值。

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 是否提早取决于计算机的性能。

  • 感兴趣能够看下这篇文章:usedeferredvalue-in-react-18
  • 在线例子

useId

useId反对同一个组件在客户端和服务端生成雷同的惟一的 ID,防止 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级构造。

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

这里波及到 SSR 局部常识,这里不开展了,能够浏览该篇文章了解:

  • 为了生成惟一 id,React18 专门引入了新 Hook:useId

提供给第三方库的 Hook

这两个 Hook 日常开发根本用不到,简略带过

useSyncExternalStore

useSyncExternalStore 个别是第三方状态治理库应用如 Redux。它通过强制的同步状态更新,使得内部 store 能够反对并发读取。它实现了对外部数据源订阅时不再须要 useEffect

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

useInsertionEffect

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

useInsertionEffect(() => {console.log('useInsertionEffect 执行')
}, [])

参考

  • React 官网文档
  • React 18 总览
  • 「React18 新个性」深入浅出用户体验巨匠—transition
退出移动版