В предыдущей статье я кратко описал, что такоеVirtual DOM, я подробно расскажу в этой главеDiff
Алгоритмы и почемуReact
а такжеVue
Значение ключа требуется во всех циклах.
Что такое алгоритм DOM Diff
Веб-интерфейс по сути представляет собой древовидную структуру DOM, изменение какой-либо его части означает изменение соответствующего DOM-узла. В React/Vue виртуальный DOM используется для имитации реальной древовидной структуры.Все они имеют две виртуальные DOM, одна из которых представляет собой отображение реальной структуры DOM, а другая — виртуальную DOM, сгенерированную после модификации, а затем используют алгоритм Efficient Diff. чтобы просмотреть и проанализировать разницу между старой и новой структурой Virtual DOM и, наконец, исправить различные узлы.
Однако при наличии двух виртуальных DOM использование стандартного алгоритма Diff определенно невозможно.Используя традиционный алгоритм Diff для рекурсивного обхода узлов для сравнения, сложность должна достигатьO(n^3), где n — общее количество узлов, что очень неэффективно.Предположим, мы хотим отобразить 1000 узлов, тогда нам приходится последовательно выполнять миллиарды сравнений, чего точно недостаточно для удовлетворения требований к производительности.
Вот традиционный алгоритм Diff:
// 存储比较结果
let result = []
// 比较两棵树
const diffTree = function (beforeTree, afterTree) {
// 获取较大树的 长度
let count = Math.max(beforeTree.children.length, afterTree.children.length)
// 进行循环遍历
for (let i = 0; i < count; i++) {
const beforeChildren = beforeTree.children[i]
const afterChildren = afterTree.children[i]
// 如果原树没有,新树有,则添加
if (beforeChildren === undefined) {
result.push({
type: 'add',
element: afterChildren
})
// 如果原树有,新树没有,则删除
} else if (afterChildren === undefined) {
result.push({
type: 'remove',
element: beforeChildren
})
// 如果节点名称对应不上,则删除原树节点并添加新树节点
} else if (beforeChildren.tagName !== afterChildren.tagName) {
result.push({
type: 'remove',
elevation: beforeChildren
})
result.push({
type: 'add',
element: beforeChildren
})
// 如果节点名称一样,但内容改变,则修改原树节点的内容
} else if (beforeChildren.innerTHML !== afterChildren.innerTHML) {
// 如果没有其他子节点,则直接改变
if (beforeChildren.children.length === 0) {
result.push({
type: 'changed',
beforeElement: beforeChildren,
afterElement: afterChildren,
html: afterChildren.innerTHML
});
} else {
// 否则进行递归比较
diffTree(beforeChildren, afterChildren)
}
}
}
// 最后返回结果
return result
}
Однако сложность оптимизированного алгоритма Diff составляет всегоO(n), что связано с оптимизацией алгоритма Diff.Инженеры оптимизировали алгоритм Diff следующим образом в соответствии с характеристиками реальной древовидной структуры DOM.
- Tree Diff (сравнение уровней)
- Component Diff (сравнение компонентов)
- Element Diff (сравнение элемента/узла)
Проще говоря, есть два понятия:
- Один и тот же компонент будет генерировать аналогичную структуру DOM, а разные компоненты будут генерировать разные структуры DOM.
- Дочерние узлы одного уровня можно отличить по уникальному идентификатору.
Сравнение уровней Tree Diff (сравнение древовидной структуры)
Правила сравнения Tree Diff можно полностью увидеть с помощью общей диаграммы:
Для левого и правого деревьев, старого дерева и нового дерева сначала выполняется сравнение уровня структуры дерева, и сравниваются только узлы DOM в одном цветовом поле, то есть все дочерние узлы под одним и тем же родительским узлом.
Если какой-либо узел не совпадает, узел и его дочерние узлы будут полностью удалены, и обход не будет продолжен.
На основе этой стратегии сложность алгоритма снижается доO(n).
В это время некоторые студенты хотят спросить, если я хочу переместить узел на другой узел, то есть выполнить межуровневую операцию, как поведет себя Diff?
Как показано ниже:
С корневым узлом C все дерево будет создано заново, а не просто перемещено Процесс создания выглядит следующим образом.create C
->``create F->
create G->
удалить С`.
Это высокопроизводительная операция,Официальная рекомендация — не выполнять межуровневые операции над узлами DOM.Вы можете скрывать и отображать узлы с помощью CSS вместо фактического удаления и добавления узлов DOM..
Примечание. При разработке компонентов поддержание стабильной структуры DOM может помочь повысить производительность. Например, узлы можно скрыть или показать с помощью CSS без фактического удаления или добавления узлов DOM.
Компонент Diff Сравнение компонентов
React
/Vue
Он создает приложения на основе компонентов, и стратегия, используемая для сравнения компонентов, также очень лаконична и эффективна.
Для этого есть три стратегии:
- Компоненты одного типа (т. е. два узла — это два разных экземпляра одного и того же класса компонентов)
- Если компоненты одинаковы, продолжайте сравнивать их структуры Virtual DOM иерархически.
- Если компонент A преобразован в компонент B, но в Virtual DOM, отображаемом компонентом A и компонентом B, нет изменений (то есть порядок дочерних узлов, состояние и т. д. не изменились), если разработчик может знать это заранее, тогда может сэкономить много времени Diff. В React пользователям разрешено передавать
shouldComponentUpdate()
чтобы определить, нужен ли компонентdiff
Анализ алгоритмов.
- различные типы компонентов
- прямо судить как
dirty component
, который, в свою очередь, заменяет все содержимое всего компонента.
- прямо судить как
Уведомление:
Если структуры компонента A и компонента B похожи, но React считает, что это разные типы компонентов, он не будет сравнивать их структуры, а удалит компонент A и его дочерние элементы и создаст компонент B и его дочерние элементы.
Для каштана: точно такая же структура, даже если компоненты D и G компоненты, но при изменении все равно будут удалены и созданы заново.
Компонент Diff будет сравнивать только то, изменилось ли содержимое одного и того же набора узлов. То есть, если у A есть два узла B и C в старом дереве, а у A есть два узла C и B в новом дереве, независимо от того, изменятся ли позиции B и C, слой компонента будет считаться неизменным. Однако если состояние в A изменится, будет считаться, что компонент изменился, и тогда компонент будет обновлен.
Сравнение элементов Element Diff (коллекции узлов под одним и тем же родительским элементом на одном уровне, сравните)
Когда DOM находится на том же уровне, Diff обеспечивает три операции с узлами, а именно:удалить (REMOVE_NODE),Вставить (INSERT_MARKUP),Переместить (MOVE_EXISTING).
удалить (REMOVE_NODE)
Старый тип компонента также присутствует в новой коллекции, но соответствующийelement
Если он отличается, его нельзя повторно использовать и обновлять напрямую, и необходимо выполнить операцию удаления, или, если старого компонента нет в новом наборе, также необходимо выполнить операцию удаления.
как показано на рисунке:
Вставить (INSERT_MARKUP)
Старый не устанавливает новый тип компонента, т.е. новый узел требуется для вставки нового узла.
как показано на рисунке:
Переместить (MOVE_EXISTING)
Старая коллекция новых типов компонентов, иelement
Это обновляемый тип, в настоящее время требуется операция перемещения, и можно повторно использовать предыдущий узел DOM.
Проблема с отсутствием значения ключа
Как показано на рисунке ниже, старый набор содержит узлы: A, B, C, D, а обновленный новый набор содержит узлы: B, A, D, C. В это время новый и старый наборы сравниваются методом diff. , и оказывается, что B != A , затем создайте и вставьте B в новый набор, удалите старый набор A и так далее, создайте и вставьте A, D и C, удалите B, C и D.
React считает такого рода операции громоздкими и избыточными, потому что это одни и те же узлы, но из-за изменения положения требуются сложные и неэффективные операции удаления и создания, по сути нужно только переместить позиции этих узлов.
В ответ на это явление React предлагает стратегию оптимизации: позволить разработчикам добавлять уникальные ключи для различения одной и той же группы дочерних узлов на одном уровне.Хотя это лишь небольшое изменение, производительность претерпела потрясающие изменения!
Почему цикл должен добавлять уникальное значение ключа
После добавления значения ключа к элементу, React/Vue выполнит дифференциальное сравнение при выполнении Diff, то есть через ключ выясняется, что узлы в новом и старом наборах являются одним и тем же узлом, поэтому нет необходимости чтобы удалить и создать узлы, просто нужно Положение узла в коллекции перемещается и обновляется до положения узла в новой коллекции.В это время результат сравнения, данный React, таков: B и D ничего не делают, А и С ничего не делают.移动
операция, можно.
Так как же работает такой эффективный diff?
Проще говоря, есть следующие шаги:
-
Пройдитесь по узлам нового набора и используйте уникальный ключ, чтобы определить, существует ли один и тот же узел в старом и новом наборах.
-
Если такой же узел существует, выполняется операция перемещения, но перед перемещением необходимо сравнить положение текущего узла в старом наборе с lastIndex, если он отличается, выполняется перемещение узла, в противном случае операция Не выполнена.
Это последовательный метод оптимизации. LastIndex всегда обновляется, указывая, что посещенный узел находится в самой правой позиции (т. е. наибольшей позиции) в старой коллекции. Если текущий посещенный узел в новой коллекции больше, чем lastIndex, это означает, что текущий посещенный узел находится в Если старый набор более поздний, чем предыдущий узел, узел не будет влиять на положение других узлов, поэтому его не нужно добавлять в очередь различий, то есть операцию перемещения не выполняется.Только когда посещенный узел меньше lastIndex, его нужно переместить.
Вот целая картина для примера.
Как показано на рисунке выше, используя новое дерево в качестве основы цикла:
- Индекс B в старом наборе равен
BIndex=1
,В настоящее времяlastIndex=0
, В настоящее время,lastIndex < BIndex
, ничего не делать и взять значениеlastIndex=Math.max(BIndex, lastIndex)
- Нижний индекс A в старом наборе равен
AIndex=0
,В настоящее времяlastIndex=1
, В настоящее время,lastIndex > AIndex
, в это время A в старом дереве необходимо переместить в нижний индекс, какlastIndex
положение и принять значениеlastIndex=Math.max(AIndex, lastIndex)
- Индекс D в старом наборе равен
DIndex=3
,В настоящее времяlastIndex=1
, В настоящее время,lastIndex < DIndex
, ничего не делать и взять значениеlastIndex=Math.max(DIndex, lastIndex)
- Индекс C в старом наборе равен
CIndex=2
,В настоящее времяlastIndex=3
, В настоящее время,lastIndex > CIndex
, вам нужно переместить C в старом дереве в индекс какlastIndex
положение и принять значениеlastIndex=Math.max(CIndex, lastIndex)
- Поскольку C уже является последним узлом, здесь Diff заканчивается.
Выше в основном анализируется ситуация перемещения узлов, когда в старой и новой коллекциях есть одинаковые узлы, но разные позиции.Если в новой коллекции есть вновь добавленные узлы, а в старой коллекции есть узлы, которые необходимо удалить, как React diff сравнивает и работает с шерстяной тканью?
Возьмите эту картинку в качестве примера:
Тот же процесс, что и выше:
- B Тот же процесс, что и выше
- Если в старом наборе нет набора E, считается, что такой же узел E не существует в старом наборе, затем создается новый узел E, обновляется lastIndex = 1 и обновляется положение E до положения в старом наборе. новый набор.
- С То же, что и выше
- То же, что и выше
- Когда набор Diff завершен, старый набор необходимо пройти в цикле, чтобы определить, есть ли узел, которого нет в новом наборе, но все еще существует в старом наборе.Если есть, удалите его, и цикл найдет что D является таким узлом, поэтому удалите D, завершите Diff.
При таком способе зацикливания зоркие читатели найдут проблему: если поменять местами начальную и конечную позиции коллекции, накладные расходы будут большими.
Как показано на рисунке выше, алгоритм DIff в это время переместит все A, B и C в конец D, что приведет к большому перемещению DOM.На самом деле нам нужно только переместить D в начало коллекции. только один раз.
Видно, что в процессе разработки такие операции, как перемещение последнего узла в начало списка, должны быть сведены к минимуму.Когда количество узлов слишком велико или операции обновления выполняются слишком часто, это повлияет на производительность рендеринга Реагировать до определенной степени.
Проблема с обновлением без значения ключа
В дополнение к вышесказанному, отсутствие добавления значения ключа приведет к удалению и добавлению всей коллекции и не приведет к перемещению операции DOM, что приведет к большому количеству ненужных накладных расходов, но в сочетании с приведенной выше ассоциацией Component Diff, если A, B, C, D — это компоненты одного типа. Что произойдет, если значение ключа не будет добавлено?
Поговорим о картинке:
Вот Vue:
Вот Реакт:
Мы обнаружили, что после того, как React или Vue удалит второй элемент, состояние внутри третьего списка элементов по-прежнему наследует содержимое второго списка.
Это связано с тем, что React/Vue считает, что это один и тот же тип компонента до и после изменения, а содержимое реквизита не изменилось, поэтому изменение не будет запущено.
Процесс выглядит следующим образом:
- Так как 1 не изменилось, тоПовторное использование на местепредыдущий 1
- Поскольку 2 стало 3, элементы-потомки в нем повторно используются на месте. Некоторые люди не понимают, почему элементы-потомки повторно используются на месте, тогда это потому, что атрибуты данных/состояния элементов-потомков не затрагиваются тем, что 2 становится 3
- Поскольку 3 больше нет, удалите все его дочерние элементы.
Метод взлома заключается в добавлении уникального ключа, чтобы Diff знал, что даже компоненты одного типа различаются именами.
Делая динамические изменения, старайтесь не использовать index в качестве ключа цикла, если использовать index в качестве ключа, то при удалении второго элемента индекс изменится с 1, 2, 3 на 1, 2 (вместо 1 , 3), то это все равно может вызывать ошибки обновления.
Суммировать
- Стратегия React/Vue Diff снижает сложность обхода до O(n), что является основной оптимизацией.
- Когда React/Vue зацикливается, он должен добавить уникальное значение ключа,
Это может не только эффективно повысить эффективность Diff, уменьшает перерисовку DOM и избегает некоторых странных ошибок - Сведите к минимуму межуровневые изменения компонентов, попробуйте использовать v-show/display:none для поддержания стабильности структуры DOM и предотвращения таких операций, как добавление и удаление, потребляющих много производительности.
- Минимизируйте такие операции, как перемещение хвоста узла в голову узла.Когда количество узлов слишком велико или операция обновления выполняется слишком часто, это в определенной степени повлияет на производительность рендеринга React.
- Кроме того, React использует архитектуру Fiber начиная с версии 16. Эта архитектура решает проблемы с производительностью крупномасштабных проектов React и некоторые болевые точки предыдущего фреймворка.Я познакомлю вас с загадкой архитектуры Fiber и ее отличием от предыдущей архитектуры. в следующей главе.
Обновлено 05.08.2019:
После того, как друзья напомнили мне в области комментариев, я обнаружил, что существует очевидная проблема с предложением, которое я написал, что добавление ключа может повысить эффективность DIff.
Официальная документация Vue:
Специальный атрибут ключа в основном используется в алгоритме виртуального DOM Vue для идентификации VNodes при сравнении старых и новых узлов. Без ключей Vue использует алгоритм, который сводит к минимуму динамические элементы и пытается максимально исправить/повторно использовать элементы одного и того же типа. С ключом он изменит порядок элементов на основе изменения ключа и удалит элементы, ключ которых не существует.
Дочерние элементы с одним и тем же родительским элементом должны иметьуникальный ключ. Повторяющиеся ключи вызовут ошибки рендеринга.
Реагировать на официальную документацию:
Ключ помогает React определить, какие элементы были изменены, добавлены или удалены. Элементам внутри массива должны быть назначены ключи, чтобы элементы имели стабильную идентичность: ключи должны быть уникальными.
По этому вопросу я делаю следующие выводы:
Когда тройняшки стоят в ряд, как узнать, кто старший?
Как отличить, если старший брат вдруг поменяется местами со старшим третьим?
Поставьте им карточки, напишите старшего, второго и третьего.
Так вы не ошибетесь. главное роль.
Ведь роль ключа заключается в обновлении компонентаПроверить, совпадают ли два узла. Если они совпадают, используйте их повторно, если отличаются, удалите старые и создайте новые.
Плюсы и минусы отказа от добавления ключа:
- Когда компонент чистый и нет привязки данных, исходя из этой предпосылки, узлы можно повторно использовать более эффективно, а скорость сравнения также выше без ключа, потому что для добавления или удаления узлов с ключом требуется время, поэтому нет необходимости добавлять ключ, чтобы получить преимущества повторного использования на месте.
- Этот режим принесет некоторые скрытые побочные эффекты, такие как эффект перехода, который может не быть сгенерирован, или будет несовпадение состояния в некоторых узлах с привязанным состоянием данных (формы).
Для Вью:
- Основная роль — это ключ к эффективному списку обновлений виртуального DOM, значение ключа — единственная основа для оценки элемента элемента VDOM.
- Не гарантируется, что использование ключа будет на 100 % быстрее, чем без него. Это то же самое, что и Vdom не гарантирует, что оно будет быстрее, чем работа с собственным DOM. Это просто компромисс. На самом деле, использование индекса в качестве ключа не рекомендуется, если вы не можете гарантировать, что они не изменятся.
Для реакции:
- Ключ не используется для повышения производительности реакции, но для производительности полезно использовать ключ правильно.
- Невозможно использовать случайный ключ для использования
- Ключи те же.Если свойства компонента изменяются, реакция только обновит свойства, соответствующие компонентам, если нет изменений, он не будет обновляться. Если ключ отличается, реакция сначала уничтожает компонент (будет выполнен компонент componentWillUnmount компонента с состоянием), а затем воссоздает компонент (будут выполнены как конструктор, так и componentWillUnmount компонента с состоянием).
В конце статьи, если вам интересно попасть в яму маленьких программ, можете попробовать Taro, фреймворк для написания небольших программ на React, на его основе я сделаю мультитерминальный UI фреймворк.MP-ColorUI, Мы заинтересованы можем перейти кGithubstar, ниже представлена демо-версия апплета.