Бросание кирпичей и привлечение нефрита
Внедряя концепцию Virtual DOM, React значительно избегает недопустимых операций с Dom, что значительно повысило эффективность построения наших страниц. Но то, как эффективно узнать реальные изменения Dom, сравнивая старый и новый Virtual DOM, также определяет производительность страницы, React использует свой специальный алгоритм diff для решения этой проблемы. Комбинация Virtual DOM + React diff в значительной степени гарантирует производительность React, что делает его хорошей репутацией в отрасли. Алгоритм сравнения не является первым в React, React только что оптимизировал алгоритм сравнения, но благодаря этой оптимизации он значительно улучшил производительность React, что заставляет людей вздыхать о мудрости создателей React! Далее давайте рассмотрим алгоритм сравнения React.
Традиционный алгоритм сравнения
В начале статьи мы упомянули, что алгоритм сравнения React значительно улучшил производительность React, в то время как предыдущий алгоритм сравнения React был оптимизирован для традиционного алгоритма сравнения. Давайте сначала посмотрим, как выглядит традиционный алгоритм сравнения.
Традиционный алгоритм diff сравнивает узлы по очереди посредством рекурсии, что неэффективно, а сложность алгоритма достигает O(n^3), где n — общее количество узлов в дереве. Как его рассчитать, вы можете проверить ответ на Zhihu.
Разница реакции идет от O(n^3) до O(n) , как вы вычисляете O(n^3) и O(n)?
Насколько ужасен O(n^3)? Это означает, что если вы хотите отобразить 1000 узлов, вам придется последовательно выполнять миллиарды сравнений, и эта экспоненциальная стоимость производительности слишком высока для сцен внешнего рендеринга. В React временная сложность этого алгоритма сравнения снижена с O(n^3) до O(n). Насколько велико улучшение от O (n ^ 3) до O (n), давайте посмотрим на картинку.
Судя по картинке выше, улучшение, вызванное алгоритмом сравнения React, несомненно, огромно. Далее давайте посмотрим на другую картинку:
С 1979 по 2011 год потребовалось более 30 лет, чтобы довести временную сложность до O(n^3), но React потребовалось всего несколько лет, чтобы перейти от открытого исходного кода к настоящему, но внезапно она достигла O(n) , Еще раз спасибо создателям React. Так как же это делает удивительный алгоритм сравнения React?Принцип реакции
Ранее мы упоминали, что временная сложность традиционного алгоритма сравнения составляет O(n^3), где n — общее количество узлов в дереве. По мере увеличения n время, затрачиваемое на сравнение, резко возрастает. React использует свой специальный алгоритм сравнения для достижения скачка от O(n^3) к O(n), и волшебное оружие для достижения этого подвига — следующие три, казалось бы, простых стратегии сравнения:
- Операций перемещения узлов DOM по уровням в веб-интерфейсе очень мало, и их можно игнорировать..
- Два компонента с одним и тем же классом будут генерировать похожие древовидные структуры, а два компонента с разными классами будут генерировать разные древовидные структуры..
- Для группы дочерних узлов одного уровня их можно отличить по уникальному идентификатору..
На основе трех вышеуказанных стратегий React оптимизирует соответствующие различия деревьев, компонентов и элементов соответственно, что значительно повышает эффективность различий.
tree diff
Основываясь на Стратегии 1, React кратко и четко оптимизировал алгоритм дерева, то есть дерево сравнивается иерархически, и два дерева будут сравнивать только узлы на одном уровне.
Поскольку перемещение узлов DOM между уровнями незначительно, React будет сравнивать только узлы DOM на одном уровне, то есть все дочерние узлы одного и того же родительского узла. Когда будет обнаружено, что узел больше не существует, узел и его дочерние узлы будут полностью удалены и не будут использоваться для дальнейшего сравнения. Таким образом, для завершения сравнения всего дерева DOM требуется только один обход дерева.
Предпосылка стратегии 1 заключается в том, что в веб-интерфейсе очень мало межуровневых операций перемещения узлов DOM, но это не отрицает существования межуровневых операций узлов DOM Итак, как React справляется с такого рода операциями?
Далее показываем весь процесс обработки через картинку:Узел A (включая его дочерние узлы) полностью перемещается под узел D, потому что React будет просто учитывать преобразование положения узлов одного уровня, а для узлов разных уровней только операции создания и удаления. Когда корневой узел обнаружит, что A в дочернем узле исчез, он сразу уничтожит A; когда D найдет еще один дочерний узел A, он создаст новый A (включая дочерние узлы) в качестве своих дочерних узлов. В этот момент выполняется diff: создать A → создать B → создать C → удалить A.Можно обнаружить, что при перемещении узла по уровням операция мнимого перемещения не происходит, а пересоздается все дерево с A в качестве корневого узла. Это операция, которая влияет на производительность React, поэтому официально не рекомендуется выполнять межуровневые операции на узлах DOM.
При разработке компонентов поддержание стабильной структуры DOM поможет повысить производительность. Например, узлы могут быть скрыты или показаны с помощью CSS вместо фактического перемещения. Удалите или добавьте узлы DOM.
component diff
React создает приложения на основе компонентов, и стратегия сравнения компонентов также очень проста и эффективна.
- Если это компонент того же типа, продолжайте сравнивать дерево Virtual DOM в соответствии с исходной стратегией.
- Если нет, компонент оценивается как грязный компонент, и все дочерние узлы во всем компоненте заменяются.
- Для одного и того же типа компонента возможно, что его виртуальный DOM не изменится.Если вы можете знать это наверняка, вы можете сэкономить много времени на вычисление различий. Таким образом, React позволяет пользователям использовать shouldComponentUpdate(), чтобы определить, нужно ли анализировать компонент с помощью алгоритма сравнения, но если вызывается метод forceUpdate, shouldComponentUpdate будет недопустимым.
Далее давайте посмотрим, как в следующем примере реализовано преобразование:
Процесс преобразования выглядит следующим образом:Когда компонент D становится компонентом G, даже если эти два компонента имеют схожую структуру, как только React решит, что D и G являются компонентами разных типов, он не будет сравнивать их структуры, а напрямую удалит компонент D, воссоздаст компонент G и его дочерние узлы. Хотя, когда два компонента относятся к разным типам, но похожи по структуре, разница будет влиять на производительность, но, как говорится в официальном блоге React: Компоненты разных типов редко имеют одинаковые деревья DOM, поэтому этот экстремальный фактор трудно реализовать в фактическом процессе разработки. значительное влияние.element diff
Когда узлы находятся на одном уровне, diff предоставляет 3 вида операций с узлами, а именно INSERT_MARKUP (вставка), MOVE_EXISTING (перемещение) и REMOVE_NODE (удаление).
- INSERT_MARKUP : Новый тип компонента отсутствует в старом наборе, то есть совершенно новый узел, который необходимо вставить в новый узел.
- MOVE_EXISTING : в старой коллекции есть новый тип компонента, и элемент является обновляемым типом, был вызван generateComponentChildren receiveComponent , в этом случае prevChild=nextChild , вам нужно выполнить операцию перемещения, вы можете повторно использовать предыдущий узел DOM.
- REMOVE_NODE: старый тип компонента также доступен в новом наборе, но соответствующий элемент нельзя повторно использовать и обновлять напрямую, и необходимо выполнить операцию удаления, или Если старых компонентов нет в новой коллекции, их также нужно удалить.
Старый набор содержит узлы A, B, C и D, а обновленный новый набор содержит узлы B, A, D и C. В это время новый и старый наборы различаются и сравниваются, и если B!=A найдено, создайте и вставьте B В новый набор удалите старый набор A и так далее, создайте и вставьте A, D и C, удалите B, C и D.
Мы обнаружили, что это те же узлы, только позиция изменилась, но она должна выполнять сложные и неэффективные операции удаления и создания. На самом деле необходимо только перемещать позиции этих узлов. РЕАКТ предлагает стратегию оптимизации для этого явления:Разрешить разработчикам добавлять уникальный ключ к одному и тому же групповому субтитру одного уровня.Хотя это всего лишь небольшое изменение, производительность изменилась кардинально!Давайте посмотрим, как работает react diff после применения этой стратегии.
По ключу можно точно определить, что узлы в старом и новом наборах являются одним и тем же узлом, поэтому нет необходимости удалять и создавать узлы, просто переместите положение узла в старом наборе и обновите его до позиция узла в новом наборе.В это время React выдает результирующие результаты diff: B и D не выполняют никаких операций, а A и C могут выполнять операции перемещения.
Конкретный процесс показан в таблице:
index | узел | oldIndex | maxIndex | действовать |
---|---|---|---|---|
0 | B | 1 | 0 | oldIndex(1)>maxIndex(0), maxIndex=oldIndex, maxIndex становится 1 |
1 | A | 0 | 1 | oldIndex(0) |
2 | D | 3 | 1 | oldIndex(3)>maxIndex(1), maxIndex=oldIndex, maxIndex становится равным 3 |
3 | C | 2 | 3 | oldIndex(2) |
- index: индекс обхода новой коллекции.
- oldIndex: индекс текущего узла в старой коллекции.
- maxIndex: максимальное значение индекса среди узлов, посещенных новым набором, в старом наборе.
Сравните только oldIndex и maxIndex в столбце операции:
- Когда oldIndex>maxIndex, назначьте значение oldIndex для maxIndex.
- Когда oldIndex=maxIndex, никаких операций
- Когда oldIndex
Приведенный выше пример только в том случае, если узлы в старом и новом наборах все одинаковые узлы, тогда, если в новом наборе есть вновь добавленные узлы и в старом наборе есть узлы, которые необходимо удалить, как дифф работает?
index | узел | oldIndex | maxIndex | действовать |
---|---|---|---|---|
0 | B | 1 | 0 | oldIndex(1)>maxIndex(0), maxIndex=oldIndex, maxIndex становится 1 |
1 | E | - | 1 | oldIndex не существует, добавьте узел E на позицию index(1) |
2 | C | 2 | 1 | oldIndex(2)>maxIndex(1), maxIndex=oldIndex, maxIndex становится равным 2 |
3 | A | 0 | 2 | oldIndex(0) |
Примечание: Наконец, необходимо пройтись по старой коллекции в цикле, чтобы узнать узлы, которых нет в новой коллекции.В это время обнаруживается, что такой узел D существует, поэтому удалите узел D, а к этому времени операция diff завершена.
В том же столбце операции сравниваются только oldIndex и maxIndex, но oldIndex может и не существовать:
- старый индекс существует
- Когда oldIndex>maxIndex, назначьте значение oldIndex для maxIndex.
- Когда oldIndex=maxIndex, никаких операций
- Когда oldIndex
- старый индекс не существует
- Добавить текущий узел в позицию индекса
Конечно, этот дифф не идеален, давайте рассмотрим такую ситуацию:
На самом деле нам нужно выполнить операцию перемещения только для D. Однако, поскольку позиция D в старом наборе самая большая, oldIndex других узловВо время разработки сведите к минимуму такие операции, как перемещение последнего узла в начало списка. Когда количество узлов слишком велико или операция обновления выполняется слишком часто, это в определенной степени повлияет на производительность рендеринга React.
Благодаря наличию ключа реакция может точно определить, существует ли узел в новом наборе, что значительно повышает эффективность сравнения. Когда мы выполняем рендеринг списка во время разработки, если ключ не добавлен, реакция выдаст предупреждение с просьбой к разработчику добавить ключ, просто чтобы повысить эффективность сравнения. Но должно ли добавление ключа быть выше производительности без ключа? Давайте посмотрим на другой пример:
现在有一集合[1,2,3,4,5],渲染成如下的样子:
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
---------------
现在我们将这个集合的顺序打乱变成[1,3,2,5,4]。
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='4'>4</div>
操作:节点2移动至下标为2的位置,节点4移动至下标为4的位置。
2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>4</div>
操作:修改第1个到第5个节点的innerText
---------------
如果我们对这个集合进行增删的操作改成[1,3,2,5,6]。
1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='6'>6</div>
操作:节点2移动至下标为2的位置,新增节点6至下标为4的位置,删除节点4。
2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>6</div>
操作:修改第1个到第5个节点的innerText
---------------
通过上面这两个例子我们发现:
由于dom节点的移动操作开销是比较昂贵的,没有key的情况下要比有key的性能更好。
В приведенном выше примере мы обнаружили, что хотя добавление ключа повышает эффективность сравнения, это не обязательно повышает производительность страницы. Итак, мы должны обратить внимание на это:
Для простого рендеринга страницы со списком производительность без ключа выше, чем с ключом.
В соответствии с приведенной выше ситуацией, наконец, мы резюмируем роль ключа:
- Точно определить, находится ли текущий узел в старом наборе
- Значительно сократить количество обходов
прикладная практика
Пример кода адреса:GitHub.com/Набор Руи…
Обновить указанную область страницы
Сейчас есть такое требование, чтобы при изменении личности пользователя текущая страница перезагружала данные. На первый взгляд, это кажется очень простым и не сложным.Поскольку жизненный цикл componentDidUpdate используется для определения того, изменилась ли личность пользователя, если есть изменение, данные будут повторно запрошены, поэтому есть следующий кусок кода:import React from 'react';
import {connect} from 'react-redux';
let oldAuthType = '';//用来存储旧的用户身份
@connect(
state=>state.user
)
class Page1 extends React.PureComponent{
state={
loading:true
}
loadMainData(){
//这里采用了定时器去模拟数据请求
this.setState({
loading:true
});
const timer = setTimeout(()=>{
this.setState({
loading:false
});
clearTimeout(timer);
},2000);
}
componentDidUpdate(){
const {authType} = this.props;
//判断当前用户身份是否发生了改变
if(authType!==oldAuthType){
//存储新的用户身份
oldAuthType=authType;
//重新加载数据
this.loadMainData();
}
}
componentDidMount(){
oldAuthType=this.props.authType;
this.loadMainData();
}
render(){
const {loading} = this.state;
return (
<h2>{`页面1${loading?'加载中...':'加载完成'}`}</h2>
)
}
}
export default Page1;
Кажется, что нам нужно всего лишь добавить кусок кода, чтобы выполнить это требование, но когда у нас есть десятки страниц, этот метод кажется растянутым. Есть ли хороший способ выполнить это требование? На самом деле это очень просто, и этого можно добиться, используя возможности react diff. Для этого требования на самом деле надеются, что текущий компонент может быть уничтожен и восстановлен, так как же он может быть уничтожен и восстановлен? Благодаря приведенному выше резюме я обнаружил две ситуации, в которых можно добиться разрушения и регенерации компонентов.
- При изменении типа компонента
- При изменении значения ключа Далее мы объединяем эти две характеристики и используем для этого два метода.
Первый: ввести загрузочный компонент. При переключении удостоверения установите для загрузки значение true, и в это время отображается компонент загрузки; после завершения переключения удостоверения загрузка становится ложной, и отображаются его дочерние узлы.
<div className="g-main">{loading?<Loading/>:children}</div>
Второй тип: просто добавьте значение ключа в область обновления.Как только изменится личность пользователя, значение ключа изменится.
<div className="g-main" key={authType}>{children}</div>
Что касается первого и второго компромиссов, мое личное предложение выглядит следующим образом:
Если вам нужно запросить сервер, используйте первый, потому что у сервера запросов будет определенное время ожидания.Добавление компонента загрузки может улучшить самочувствие и опыт пользователя. Если вам не нужно запрашивать сервер, выберите второй, потому что второй проще и практичнее.
Легче отслеживать изменения реквизита
Для этого требования нам нравится инкапсулировать критерии поиска в компонент, а список запросов — в компонент. Список запросов получит атрибут параметра запроса следующим образом:import React from 'react';
import {Card} from 'antd';
import Filter from './components/filter';
import Teacher from './components/teacher';
export default class Demo2 extends React.PureComponent{
state={
filters:{
name:undefined,
height:undefined,
age:undefined
}
}
handleFilterChange=(filters)=>{
this.setState({
filters
});
}
render(){
const {filters} = this.state;
return <Card>
{/* 过滤器 */}
<Filter onChange={this.handleFilterChange}/>
{/* 查询列表 */}
<Teacher filters={filters}/>
</Card>
}
}
Теперь мы столкнулись с проблемой, как следить за изменением фильтров в компоненте Учитель.Поскольку фильтры относятся к эталонному типу, становится немного сложно отслеживать их изменения.К счастью, в lodash есть инструментальный метод для сравнения двух объектов, что делает это просто. Однако, если вы добавите дополнительные реквизиты в Учитель позже, когда вам нужно будет отслеживать изменения нескольких реквизитов, ваш код станет сложнее поддерживать. В ответ на эту проблему мы все еще можем реализовать ее через значение ключа.Когда ключ создается повторно для каждого поиска, компонент Учитель будет перезагружен. код показывает, как показано ниже:
import React from 'react';
import {Card} from 'antd';
import Filter from './components/filter';
import Teacher from './components/teacher';
export default class Demo2 extends React.PureComponent{
state={
filters:{
name:undefined,
height:undefined,
age:undefined
},
tableKey:this.createTableKey()
}
createTableKey(){
return Math.random().toString(36).substring(7);
}
handleFilterChange=(filters)=>{
this.setState({
filters,
//重新生成tableKey
tableKey:this.createTableKey()
});
}
render(){
const {filters,tableKey} = this.state;
return <Card>
{/* 过滤器 */}
<Filter onChange={this.handleFilterChange}/>
{/* 查询列表 */}
<Teacher key={tableKey} filters={filters}/>
</Card>
}
}
Даже если позже в Учитель добавятся новые реквизиты, нет проблем, просто склеить ключ:
<Teacher key={`${tableKey}-${prop1}-${prop2}`} filters={filters} prop1={prop1} prop2={prop2}/>
Проблема с ссылкой в реагирующем маршрутизаторе
Давайте сначала посмотрим на демо-код:import React from 'react';
import {Card,Spin,Divider,Row,Col} from 'antd';
import {Link} from 'react-router-dom';
const bookList = [{
bookId:'1',
bookName:'三国演义',
author:'罗贯中'
},{
bookId:'2',
bookName:'水浒传',
author:'施耐庵'
}]
export default class Demo3 extends React.PureComponent{
state={
bookList:[],
bookId:'',
loading:true
}
loadBookList(bookId){
this.setState({
loading:true
});
const timer = setTimeout(()=>{
this.setState({
loading:false,
bookId,
bookList
});
clearTimeout(timer);
},2000);
}
componentDidMount(){
const {match} = this.props;
const {params} = match;
const {bookId} = params;
this.loadBookList(bookId);
}
render(){
const {bookList,bookId,loading} = this.state;
const selectedBook = bookList.find((book)=>book.bookId===bookId);
return <Card>
<Spin spinning={loading}>
{
selectedBook&&(<div>
<img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/>
<h4>书名:{selectedBook?selectedBook.bookName:'--'}</h4>
<div>作者:{selectedBook?selectedBook.author:'--'}</div>
</div>)
}
<Divider orientation="left">关联图书</Divider>
<Row>
{
bookList.filter((book)=>book.bookId!==bookId).map((book)=>{
const {bookId,bookName} = book;
return <Col span={6}>
<img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/>
<h4><Link to={`/demo3/${bookId}`}>{bookName}</Link></h4>
</Col>
})
}
</Row>
</Spin>
</Card>
}
}
Через демо-гиф мы видим, что адрес адресной строки изменился, но он не проходит через componentDidMount для запроса данных, как мы себе представляли, а значит, наш компонент не реализовал такой процесс уничтожения и регенерации. Чтобы решить эту проблему, вы можете прослушивать изменения в componentDidUpdate:
componentDidUpdate(){
const {match} = this.props;
const {params} = match;
const {bookId} = params;
if(bookId!==this.state.bookId){
this.loadBookList(bookId);
}
}
Ранее мы говорили, что если вам нужно отслеживать несколько реквизитов на более позднем этапе, его будет сложнее поддерживать на более позднем этапе. Точно так же мы все еще используем ключ для решения этой проблемы. На домашней странице мы можем инкапсулировать страницу. в компонент BookDetail, и обернуть его во внешний слой layer, а затем добавить ключ в BookDetail, код выглядит следующим образом:
import React from 'react';
import BookDetail from './bookDetail';
export default class Demo3 extends React.PureComponent{
render(){
const {match} = this.props;
const {params} = match;
const {bookId} = params;
return <BookDetail key={bookId} bookId={bookId}/>
}
}
Преимущество этого в том, что наша структура кода более понятна, а последующее расширение новых функций относительно просто.
Заключение:
- Эффективность React выигрывает от системы сравнения Virtual DOM+React. Алгоритм сравнения не является оригинальным для реакции, реакция просто оптимизирует традиционный алгоритм сравнения. Но из-за его оптимизации временная сложность алгоритма diff внезапно снижается с O (n ^ 3) до O (n).
- Три стратегии для React diff:
- Операции перемещения узлов DOM по уровням в веб-интерфейсе очень малы и могут быть проигнорированы.
- Два компонента с одним и тем же классом будут генерировать похожие древовидные структуры, а два компонента с разными классами будут генерировать разные древовидные структуры.
- Для группы дочерних узлов одного уровня их можно отличить по уникальному идентификатору.
- При разработке компонентов поддержание стабильной структуры DOM поможет повысить производительность.
- Во время разработки сведите к минимуму такие операции, как перемещение последнего узла в начало списка.
- Наличие ключа повышает эффективность сравнения, но не обязательно повышает производительность.Помните, что в случае простого рендеринга списка производительность без ключа выше, чем с ключом.
- Знайте, как использовать возможности react diff для решения ряда проблем в нашей реальной разработке.