Уведомление
React 18
Мы отказались ie11
от поддержки и 2022年6月15日
прекратим ее поддержку ie
. Если вам нужна совместимость, вам нужно откатиться на React 17
версию.
React 18 中引入的新特性是使用现代浏览器的特性构建的,在IE中无法充分polyfill,比如micro-tasks
обновление
- Новый проект: используйте напрямую
npm
илиyarn
установите последнюю версию зависимостей (если это js, вам не нужно устанавливать файл объявления типов)
npm i react react-dom --save
npm i @types/react @types/react-dom -D
- Старый проект: сначала измените номер версии в зависимости на самый последний, затем удалите
node_modules
папку и переустановите:
npm i
новые возможности
1. API рендеринга
Для лучшего управления введен новый root节点
, который также поддерживает (параллельный режим рендеринга), что позволяет войти в (параллельный режим).React 18
root API
root API
new concurrent renderer
concurrent mode
// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root);
// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<App />);
В то же время при удалении компонента нам также необходимо unmountComponentAtNode
обновиться до root.unmount
:
// React 17
ReactDOM.unmountComponentAtNode(root);
// React 18
root.unmount();
Советы: Если мы React 18
будем использовать старый render api
, то после запуска проекта вы увидите предупреждение в консоли:
Это означает, что вы можете обновить свой проект непосредственно до React 18
версии, не вызывая прямого попадания break change
. Если вам необходимо сохранить React 17
функции версии, вы можете игнорировать эту ошибку, поскольку она 18
совместима между версиями.
В дополнение к этому, он был удален React 18
из метода , поскольку его использование обычно не дает ожидаемых результатов.render
回调函数
Suspense
В новой версии, если вам нужно render
использовать функцию обратного вызова в методе, мы можем реализовать ее в компоненте через useEffect
:
// React 17
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root, () => {
console.log('渲染完成');
});
// React 18
const AppWithCallback: React.FC = () => {
useEffect(() => {
console.log('渲染完成');
}, []);
return <App />;
};
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<AppWithCallback />);
Наконец, если ваш проект использует ssr
рендеринг на стороне сервера, вам необходимо hydration
перейти на hydrateRoot
:
// React 17
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.hydrate(<App />, root);
// React 18
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.hydrateRoot(root, <App />);
Кроме того, вам также необходимо обновить TypeScript
определение типа. Если ваш проект его использует TypeScript
, самое примечательное изменение заключается в том, что теперь при определении props
типа, если вам нужно получить подкомпонент children
, то вам нужно 显式的定义它
, например:
// React 17
interface MyButtonProps {
color: string;
}
const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 17 的 FC 中,默认携带了 children 属性
return <div>{children}</div>;
};
export default MyButton;
// React 18
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}
const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
return <div>{children}</div>;
};
export default MyButton;
2. автоматическая пакетная обработка setState
React 18
Улучшения производительности реализованы «из коробки» за счет выполнения пакетной обработки по умолчанию.
Пакетная обработка означает, что для повышения производительности 多个状态更新
пакетная обработка объединяется в пакеты на уровне данных 一次更新
(и 多个渲染
объединяется в пакеты на уровне представления 一次渲染
).
1. До React 18:
Теперь React 18 之前
мы React 事件处理函数
выполняем только пакетные обновления в формате . По умолчанию обновления в promise
, setTimeout
, 原生事件处理函数
в или 任何其它事件内
не группируются:
Сценарий 1: функция обработчика событий React
import React, { useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<button
onClick={() => {
setCount1(count => count + 1);
setCount2(count => count + 1);
// 在React事件中被批处理
}}
>
{`count1 is ${count1}, count2 is ${count2}`}
</button>
);
};
export default App;
Нажмите кнопку, чтобы распечатать console.log:
Как видите, количество рендерингов и количество обновлений одинаково.Даже если мы обновляем два состояния, компонент рендерится только один раз при каждом обновлении.
Однако, если мы поместим обновление статуса внутри promise
или setTimeout
:
Случай 2: setTimeout
import React, { useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
setTimeout(() => {
setCount1(count => count + 1);
setCount2(count => count + 1);
});
// 在 setTimeout 中不会进行批处理
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
Нажмите кнопку, чтобы перепечатать console.log:
Как видите, каждый раз, когда вы нажимаете кнопку обновления двух состояний, компонент будет отображаться дважды, а пакетные обновления выполняться не будут.
Сценарий 3: Собственные события js
import React, { useEffect, useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => {
document.body.addEventListener('click', () => {
setCount1(count => count + 1);
setCount2(count => count + 1);
});
// 在原生js事件中不会进行批处理
}, []);
return (
<>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</>
);
};
export default App;
Нажмите кнопку, чтобы перепечатать console.log:
Видно, что в нативном событии js результат такой же, как и во второй ситуации: каждый раз, когда вы нажимаете кнопку обновления двух состояний, компонент будет отображаться дважды, а пакетные обновления выполняться не будут.
2. В Реакте 18:
В React 18
трех приведенных выше примерах это произойдет только один раз, render
поскольку все обновления будут автоматически группироваться. Это, несомненно, значительно улучшает общую производительность приложения.
Однако в следующем примере React 18
рендеринг будет выполнен дважды :
import React, { useState } from 'react';
// React 18
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={async () => {
await setCount1(count => count + 1);
setCount2(count => count + 1);
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
Подведем итог:
- До версии 18 пакетная обработка автоматически выполнялась только в функции обработки событий реагирования, а в других случаях она обновлялась несколько раз.
- После 18 пакетная обработка в любом случае будет выполняться автоматически, и несколько обновлений всегда будут объединены в одно.
3. флешсинк
Пакетная обработка — это функция 破坏性改动
, если вы хотите выйти из пакетного обновления, вы можете использовать flushSync
:
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
flushSync(() => {
setCount1(count => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2(count => count + 1);
});
// 第二次更新
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
Примечание. flushSync
Несколько функций внутри функции setState
по-прежнему обновляются пакетно , что позволяет точно контролировать ненужные пакетные обновления.
Для получения дополнительной информации о 批处理
и вы можете обратиться к официальному подробному обзоруflushSync
React по автоматическому пакетному пакетированию .
4. Предупреждение о статусе обновления при удалении компонентов.
При разработке мы периодически сталкиваемся со следующими ошибками:
Эта ошибка означает: невозможно выполнить обновление статуса неподключенного (несмонтированного) компонента. Это недопустимая операция, указывающая на утечку памяти в нашем коде.
На самом деле эта ошибка встречается редко, и в предыдущих версиях это предупреждение воспринималось многими неправильно и в некоторой степени вводило в заблуждение.
Первоначальное намерение этой ошибки заключалось в том, чтобы предназначаться для некоторых особых сценариев, таких 你在useEffect里面设置了定时器,或者订阅了某个事件,从而在组件内部产生了副作用,而且忘记return一个函数清除副作用,则会发生内存泄漏……
как
Но в реальном развитии существует больше сценариев 我们在 useEffect 里面发送了一个异步请求,在异步函数还没有被 resolve 或者被 reject 的时候,我们就卸载了组件
. В этом случае предупреждение также будет активировано. Однако в этом случае утечки памяти внутри компонента нет, поскольку асинхронная функция была подвергнута сборке мусора, и на этом этапе предупреждение вводит в заблуждение .
По этому поводу у React также есть официальное объяснение:
Подводя итог вышеизложенным причинам, в 2007 году React 18
эта ошибка была официально удалена.
Для получения дополнительной информации об этой ошибке вы можете обратиться к официальным инструкциям React, нажмите здесь , чтобы просмотреть.
5. О возвращаемом значении компонентов React
- В React можно вернуть только
React 17
один, если вам это нужно . Если вы вернете явно , консоль выдаст ошибку во время выполнения.空组件
null
undefined
- В
React 18
версииundefined
больше не проверяются сбои, вызванные возвратом. Он может как возвращатьnull
, так и возвращатьundefined
(ноReact 18
файлdts
все равно будет проверяться и разрешен только возвратnull
. Эту ошибку типа можно игнорировать).
Официальное объяснение возвращаемого значения компонента: github.com/reactwg/rea…
6. Строгий режим
Журналы консоли больше не подавляются:
严格模式
React работает с каждым компонентом по мере его использования 两次渲染
, позволяя вам наблюдать некоторые неожиданные результаты. В версии React 17
ведение 其中一次渲染
журнала консоли отключено, чтобы его было легче читать.
Чтобы устранить замешательство сообщества по этому вопросу, в 2017 году React 18
это ограничение было официально снято. Если он у вас установлен React DevTools
, сообщения журнала для второго рендеринга будут выделены серым цветом и отображаются в консоли в мягкой форме.
Официальное объяснение строгого режима: github.com/reactwg/rea…
7. Саспенс больше не требует отступления для захвата.
В React 18
компоненте Suspense
чиновники 空的fallback
внесли изменения в способ обработки атрибутов: больше нельзя пропускать границы 缺失值
или . Вместо этого граница фиксируется и просматривается снаружи, и если она не найдена, она отображается как .值为null
fallback
Suspense
fallback
null
До обновления:
Раньше, если ваш Suspense
компонент не предоставлял fallback
свойство, React молча пропускал его и продолжал поиск до следующей границы:
// React 17
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
<Suspense> // <--- 这个边界被跳过,没有 fallback 属性
<Page />
</Suspense>
</Suspense>
);
};
export default App;
Команда React обнаружила, что это может привести к запутанным и трудным для отладки ситуациям. Например, если вы отлаживаете проблему и тестируете ее, создавая привязку к компоненту без свойства fallback
, Suspense
это может привести к неожиданным результатам и 不会警告
сказать, что у него 没有fallback
есть свойство.
Обновлено:
Теперь React будет использовать текущий компонент Suspense
в качестве границ, даже если Suspense
значение текущего компонента null
равно или undefined
:
// React 18
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 不使用
<Suspense> // <--- 这个边界被使用,将 fallback 渲染为 null
<Page />
</Suspense>
</Suspense>
);
};
export default App;
Это обновление означает нас 不再跨越边界组件
. Вместо этого мы захватим границу и отрисуем ее fallback
так, как если бы вы предоставили null
компонент, который возвращает объект . Это означает, что приостановленный Suspense
компонент будет вести себя так, как ожидалось, и fallback
не возникнет проблем, если вы забудете указать атрибут.
Официальное объяснение о Suspense: github.com/reactwg/rea…
Новый API
1. идентификатор использования
const id = useId();
Поддерживает один и тот же компонент для генерации одного и того же уникального идентификатора на клиенте и сервере во избежание hydration
несовместимости. Это решает существующие проблемы в версиях React 17
и ниже. 17
Поскольку наш сервер предоставляет HTML
Yes при рендеринге 无序的
, useId
принцип заключается в том, что каждый из них id
представляет иерархическую структуру компонента в дереве компонентов.
Подробнее об useId смотрите в посте useId в рабочей группе .
Например, useSyncExternalStore.
useSyncExternalStore
Это новый API, который претерпел изменения и был useMutableSource
изменен. Он в основном используется для решения проблемы разрыва внешних данных.
useSyncExternalStore позволяет компонентам React безопасно и эффективно читать внешние источники данных под CM, вызывая синхронное обновление данных. В параллельном режиме рендеринг React будет выполняться слайсами (в единицах волокна), а обновления с более высоким приоритетом могут быть перемежены посередине. Если общедоступные данные (например, данные в Redux) изменяются при обновлении с высоким приоритетом, необходимо перезапустить предыдущий рендеринг с низким приоритетом, иначе состояние будет противоречивым.
useSyncExternalStore
Обычно он используется сторонними библиотеками управления состоянием, и нам не нужно обращать на него внимание в повседневной работе. Потому что React
он useState
изначально решил tear(撕裂)
проблемы, связанные с функцией параллелизма. useSyncExternalStore
В основном для разработчиков фреймворков, например , его нельзя использовать redux
напрямую при управлении состоянием. Вместо этого он поддерживает внешний объект для реализации обновлений данных. Без управления он не может автоматически решить проблему разрыва. Поэтому такой API предоставляется внешнему миру.React
state
store
发布订阅模式
React
React
React
В настоящее время React-Redux 8.0
он реализован useSyncExternalStore
на основе .
Дополнительные сведения об useSyncExternalStore см. в обзорной публикации useSyncExternalStore и сведения об API useSyncExternalStore .
3. использоватьInsertionEffect
const useCSS = rule => {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
};
const App: React.FC = () => {
const className = useCSS(rule);
return <div className={className} />;
};
export default App;
Эти хуки рекомендуется css-in-js
использовать только библиотекам. Время выполнения этого хука — DOM
после и до генерации useLayoutEffect
. Его принцип работы примерно такой же, useLayoutEffect
как и у , за исключением того, что DOM
ссылка на узел в это время недоступна. Обычно он используется для <style>
предварительного внедрения скриптов.
Дополнительные сведения об useInsertionEffect см. в Руководстве по обновлению библиотеки для<style>
.
Параллельный режим
Параллельный режим (далее — CM
) переводится как параллельный режим. Возможно, мы слышали эту концепцию много раз. Фактически, эта концепция стала очень зрелой в прошлом году и React 17
может быть 试验性
включена через некоторые API в CM
.
CM 本身并不是一个功能,而是一个底层设计
Параллельный режим помогает приложениям оперативно реагировать и корректировать их соответствующим образом в зависимости от возможностей устройства пользователя и скорости сети, устраняя ограничения за счет прерывания рендеринга 阻塞渲染
. В Concurrent
режиме React
несколько состояний могут обновляться одновременно.
Возможно, это слишком сложно сказать, но это можно суммировать в одном предложении:
React 17
Разница между и React 18
заключается в следующем: от 同步不可中断更新
изменено до 异步可中断更新
.
Здесь наступает важный момент, пожалуйста, не пропускайте следующую часть:
Мы упоминали в начале статьи: В React 18
, предусмотрены новые root api
, нужно только render
обновиться createRoot(root).render(<App />)
, чтобы включить параллельный режим.
Итак, в этот момент некоторые ученики могут спросить: означает ли включение 并发模式
включение 并发更新
?
НЕТ! В React 17
некоторых экспериментальных функциях в , включение 并发模式
означает включение 并发更新
, но React 18
после выпуска официальной версии, из-за официальных корректировок политики, React больше не полагается на 并发模式
включение 并发更新
.
Другими словами: то, что он включен, 并发模式
не обязательно означает, что он включен 并发更新
!
Краткое содержание одним предложением: В 18
, больше нет нескольких моделей, но они основаны 是否使用并发特性
на 是否开启并发更新
действиях .
v18
Сколько версий имеется на рынке от самой старой версии до текущей React
?
Его можно резюмировать с архитектурной точки зрения: в настоящее время существует две архитектуры:
递归
Обновление бесперебойноеStack Reconciler
(старая архитектура)遍历
Обновление в прерываемом режимеFiber Reconciler
(новая архитектура)
Вы можете выбрать, включать ли новую архитектуру , поэтому 并发更新
для всех версий, имеющихся в настоящее время на рынке, React
существуют четыре ситуации :
- Старая архитектура (v15 и предыдущие версии)
- Новая архитектура, одновременные обновления не включены, поведение соответствует случаю 1 (по умолчанию к этому случаю относятся версии 16 и 17).
- Новая архитектура, одновременные обновления не включены, но включены одновременный режим и некоторые новые функции (например
Automatic Batching
, в v18 так по умолчанию). - Новая архитектура, включение параллельного режима, включение одновременных обновлений
并发特性
Относится к 并发更新
функциям, которые можно использовать только после включения, например:
useDeferredValue
useTransition
диаграмма отношений:
stateDiagram-v2
[*] --> React18
React18 --> ReactDOM.render
React18 --> ReactDOM.createRoot
ReactDOM.render --> 未开启并发模式
ReactDOM.createRoot --> 开启并发模式
未开启并发模式 --> 未开启自动批处理
开启并发模式 --> 开启自动批处理
未开启自动批处理 --> 未开启并发更新
开启自动批处理 --> 未使用并发特性
开启自动批处理 --> 使用并发特性
未使用并发特性 --> 未启并发更新
使用并发特性 --> 开启并发更新
После четкого понимания их взаимоотношений мы можем продолжить изучение 并发更新
:
Возможности параллелизма:
1. начатьпереход
Запустите следующий код в версии 18:
import React, { useState, useEffect, useTransition } from 'react';
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
// 使用了并发特性,开启并发更新
startTransition(() => {
setList(new Array(10000).fill(null));
});
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
Поскольку он выполняется (используется ) в функции обратного вызова, он setList
сработает .startTransition
并发特性
setList
并发更新
startTransition
, в основном для обеспечения отзывчивости пользовательского интерфейса при выполнении большого количества задач. Этот новый API может “过渡”
значительно улучшить взаимодействие с пользователем, помечая определенные обновления startTransition
как setState
срочные 紧急渲染
.
- Дополнительные сведения о startTransition см. в разделе Шаблоны для startTransition .
二、useDeferredValue
Возврат значения отложенного ответа позволяет state
задержке вступить в силу. Значение станет последним значением только в том случае, если нет срочного обновления. useDeferredValue
Вроде startTransition
, оба помечены как несрочное обновление.
С точки зрения введения useDeferredValue
, useTransition
очень ли это похоже на ?
- То же:
useDeferredValue
по сути то же самое, что и внутренняя реализацияuseTransition
, она помечена как延迟更新
задача. - Разница
useTransition
в том, что задача обновления превращается в задачу отложенного обновления иuseDeferredValue
генерируется новое значение, которое используется в качестве статуса отложенного. (Один используется для переноса методов, другой — для переноса значений)
Поэтому startTransition
мы также можем использовать приведенный выше пример useDeferredValue
для реализации:
import React, { useState, useEffect, useDeferredValue } from 'react';
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
// 使用了并发特性,开启并发更新
const deferredList = useDeferredValue(list);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
Затем запустите проект и проверьте распечатанную диаграмму стека выполнения:
В настоящее время наши задачи разделены на разные кадры для каждого кадра task
, а JS脚本
время выполнения составляет примерно около 10 % 5ms
. Таким образом, у браузера остается время для выполнения макета стиля и рисования стилей , что снижает вероятность пропуска кадров.
- Дополнительные сведения об useDeferredValue см. в разделе Новое в версии 18: useDeferredValue .
3. Обычные ситуации
Мы можем отключить функцию параллелизма и запустить проект в обычной среде:
import React, { useState, useEffect } from 'react';
const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
export default App;
Запустите проект и проверьте распечатанную диаграмму стека выполнения:
Из напечатанной диаграммы стека выполнения видно, что из-за большого количества компонентов (10 000) время выполнения JS равно, что 500ms
означает, что без функции параллелизма: когда одновременно отрисовываются 10 000 тегов, страница будет заблокирована. некоторое время 0.5秒
, вызывая задержку, но если включить одновременные обновления, такой проблемы не будет.
这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)
в заключение
- Смысл одновременных обновлений в
交替执行
разных задачах.Когда зарезервированного времени недостаточно,React
управление потоком возвращается браузеру, ожидая прихода следующего времени кадра, а затем продолжая прерванную работу. 并发模式
Это并发更新
основная предпосылка для реализации时间切片
Это并发更新
специфическое средство реализации
Заключение
Выше приведено общее содержание этого React
обновления. Если есть какие-либо ошибки, исправьте их.