Спасибо всем за то, что снова попались на мою титульную вечеринку 😂. Это путешествие, которое я лично исследовал в последние месяцы, и я очень глубоко переживаю. На этом пути много трудностей, но держитесь до конца, я верю, что в итоге вы принесете пользу многим друзьям, это того стоит!
При этом я также благодарна всем за то, что лайкнули и пересмотрели предыдущую серию статей 😘, и у меня возникло много предложений и вопросов.Также буду писать каждую серию статей с душой и расти вместе со всеми! !
Стоять на коленях и выпрашивать лайки, внимание и звезду!Больше статей захлопнули ->
Следующий был недавно выпущен:Путешествие, чтобы раскрыть секреты практики React, обязательное для среднего и продвинутого интерфейса (ниже) ->
введение
Предыдущая трилогия интервью вкратце разобрала систему внешней структуры знаний, и это был лишь поверхностный вкус. Эта серия требует дальнейшего изучения и пониманиявнутренняя тайна. Сегодня я планирую врезаться с относительно новой точки зрения и разобраться в этом подробно.React
Внутренняя реализация.
-
1. Всем выгодно быть более удобным в повседневном бизнес-использовании React.;
-
2. Вы также можете интегрировать изученные идеи и расширить их в другие области.;
Так как же вникать в игрушку? Лучший способ:Разобрать - собрать.
Как мы все знаем,React
это очень чудесноWeb UI
framework, благодаря мощной архитектуре,логический уровеньа такжеслой просмотраразделение, так что его идеи и модели разработки могут быть хорошо перенесены на другие платформы, такие какReact-Native
. Итак, сегодня я собираюсь выпрыгнуть из нормыWeb-DOM
, цельРазработка веб-игр по образцу React. По сути, нам нужно реализовать наборReact
верхний слой и стыковка нижнего слояWebGL API
. Я с нетерпением жду возможности принести вам новое вдохновение и помощь с такой другой точки зрения. 😄
Tips:
WebGL
Это не является предметом этой статьи, и используемые API также очень ограничены, и вам не нужно иметь соответствующие запасы знаний.
Первая остановка: Пересекающиеся ворота — JSX
В качестве внешнего интерфейса нам нужно реализовать страницы, которые отображаются пользователям по одной. Итак, слой представления — это то, с чего начинается наша работа. Фронтенд-сфера продолжает быстро развиваться, и в конечном итоге это все для ответаКак эффективнее разрабатывать лучшие страницы. а такжеReact
один из ответов, которыйJSX
является важной первой остановкой.
что это такоеJSX
Шерстяная ткань?
JSX
только что вJS
Класс, согласованный в средеHTML
илиXML
Синтаксис динамического шаблона имеет чрезвычайно высокую удобочитаемость и расширяемость, цель состоит в том, чтобы использовать JS для более удобного построения структуры представления и макета.
// 这就是 JSX
const jsx = <JSX>Hello World</JSX>
Это совершенно новыйJS
синтаксис, не являющийся частью стандарта, успешно поставил классHTML
Синтаксис шаблона тега JS введен в JS, создавая новый и эффективный режим разработки. Но даже последняя версия двигателя V8 не может его поддерживать, так как же реализовать? ключпредварительно скомпилировано! выгода отBabel
мощность, мы можем пройтиПрекомпилировать, скомпилировать код во что-то, что браузер может понятьJS
.
babel-plugin-transform-jsx
ЭтоBabel
Плагин, основной функцией которого является компиляцияJSX
, вы можете настроить его напрямую (детская обувь интересует составление, вы можете узнать большеBabel
Принцип составления, который здесь не будет распространяться).
// 打包工具中的 babel loader 配置
use: {
loader: 'babel-loader',
options: {
plugins: [["transform-jsx", {
"function": "ReactWebGL.createElement",
"useVariables": true
}]],
}
}
После завершения настройки давайте сначала напишем абзацJSX
пытаться. Так как мы не используемDOM
, слой представления рисуется непосредственно наcanvas
, естественно не использовать обычныеHTML
помечен. Давайте сначала определим метку контейнера (<Container>
).
const jsx = (
<Container name="parent">
ReactWebGL Hello World
<Container name="child">
Child
</Container>
</Container>
)
какие. Ошибка в секундах. Забудьте об этом, давайте посмотрим на скомпилированный файл:
var jsx = ReactGL.createElement({
elementName: Container,
attributes: {
name: 'parent'
},
children: [
"ReactWebGL Hello World",
ReactWebGL.createElement({
elementName: Container,
attributes: {
name: "child"
},
children: ["Child"]
}),
]
});
Вот и всеBabel
сделать, это поставить вышеJSX
Код шаблона после разбора и извлечения информации тега преобразует ее в форму обычной функции. Эта функция указана в нашей конфигурацииReactWebGL.createElement
.
Далее смотрим на ошибку. Очевидно, поскольку есть некоторые переменные, которые не определены в контексте, давайте сначала определим их:
// 由于编译是直接 变量传递
// 因此标签名作为一个变量需要先定义为 string;
const Container = 'Container'
// 匹配配置中的 `ReactWebGL.createElement`
const ReactWebGL = {
createElement(tag) {
console.log(tag)
}
}
Вторая остановка: Город в небе - Виртуальный ДОМ
Пришел второй этап: что такоеВиртуальный DDMШерстяная ткань?
Как подсказывает название,Это не настоящий элемент представления, но абстрагирует реальный элемент представления как объект Javascript, который содержит всю информацию, полностью описывающую реальный элемент, но не имеет функции рендеринга.. В то же время из-заDOM
сам по себедревовидная структура, поэтому используйтеJavascript
Объекты вполне могут описать всю структуру страницы.
Прямо сейчасJSX
После компиляции параметр является одним из самых простыхвиртуальный DOMобъект (который мы называемVNode
):
// 一个最简单的 VNode
{
// 类型
type: 'Container'
// 属性
props: {
name: 'parent'
}
// 子级列表
children: [...]
}
Это просто обычныйJavascript
объект, так зачем его проектировать? У многих людей может возникнуть такая идея:Виртуальный DOM работает быстро, алгоритм сравнения очень мощный, а прямое управление реальным DOM обходится очень дорого.
Это не так просто, просто выслушайте меня подробно🧐. Представьте, когда происходит следующий сценарий:
-
первый рендер:
-
Разобрать
JSX
, сгенерироватьвиртуальное DOM-дерево, а затем после различных вычислений окончательный вызовDOM
Нарисуйте элемент представления; -
Очевидно, мы проходим
HTML
илиinnerHTML
Быстрее создавать элементы напрямую, и время белого экрана меньше, и верхних слоев большеВычислительное потребление и потребление памяти, вместо этогоПотеря производительности;
-
-
Минимальное обновление:
-
Нужно изменить титульный лист, звоните
setState
Запустите обновление. В настоящее время,React
не знаю новогоstate
Сколько изменений будет вызвано, это должно пройти через глобальный один за другимdiff
Определите измененный элемент перед запуском обновления. -
Напротив, получите соответствующую модификацию элемента заголовка, опуская
diff
, это будет более прямым и эффективным. Что, как говорится,виртуальный DOMВместо этого он медленнее, так зачем его использовать?
-
Если ваш сценарий заключается в обновлении элементов на странице в большом количестве и разбросанных, товиртуальный DOMЭто может значительно уменьшить сложность его бизнес-логики и достичь относительно высокой эффективности затрат. Прямое управлениеDOM
Элементы, с одной стороны, имеют сложную логику и слабую надежность кода, с другой стороны, неправильные операции могут привести к ухудшению производительности.
Так что скорость не показательвиртуальный DOMЦенностный фактор, его значение больше в:
-
виртуальный DOMФактически, этоПожертвовать минимальной производительностью и пространством в обмен на оптимизацию архитектуры, что может значительно улучшить масштабируемость и ремонтопригодность проекта;
-
правильно
DOM
операция проводитсяЦентрализованное управление, более безопасный, стабильный и эффективный; -
оказыватьа такжелогикаразвязка, весьмасоставнойа такжемодульный, что повышает возможность повторного использования кода и эффективность разработки;
-
виртуальный DOMЭто абстрактный объект, который можно подключить к различным слоям рендеринга для завершения.кроссплатформенный рендеринг. Например
RN / SSR
и другие программы;
JSX
простоСинтаксис автономного динамического шаблона, компилируя ввиртуальный DOM,а такжеReact
Сцепления нет. так что это может бытьДоступ к любой среде или фреймворку, например, мы также можемVue
используется вJSX
.
закончил говоритьвиртуальный DOM, возвращаемся к основному потоку. Необходимо разработать простейшуюVNode
структура, чтобы простоBabel
На основе скомпилированной структуры плюс дополнительные значения для удобного рендеринга.
// VNode 定义
interface VNode {
// 标签类型
type: any,
// 标签属性
props: { [key: string]: any },
// 子级列表
children: VNode[],
// 唯一标识
key: string
// 获取视图元素
ref: any
// 视图元素
elm: any
// 文本内容
text: string | number | undefined
}
// VNode 生产函数
function createVNode(type, props, ref, key, children, elm, text) {
return {
type,
props,
children,
ref,
key,
elm,
text,
}
}
теперь определеноVNode
После этого мы можем начать писать соответствующую функцию генератора (createElement
) вверх.
Третья остановка: Мост трансформации - createElement
Эта функция является легендарнойh
Функция дляМост между шаблонами и виртуальным домом. В большинстве основныхвиртуальный DOMВо всех библиотеках есть эта функция. существуетReact
, это черезBabel
БудуJSX
скомпилировано вh
функция, то естьReact.createElement
. пока вVue
, черезvue-loader
Буду<template>
собраны в егоh
функция. Основные функции этой функции:
Обработка для создания полного виртуального дерева DOM, для последующего рендеринга.
function createElement(tag) {
const { elementName: type, attributes: data, children } = tag
const { key, ref, ...props } = data
// 处理文本内容
let text
// 处理子级列表中的 string or number
// 同样转换为 VNode
if (children && children.length) {
let i, l = children.length
for (i = 0; i < l; ++i) {
const child = children[i]
if (['string', 'number'].includes(typeof child)) {
if (type === 'Text') {
// 基于 WebGL 的需要,区别于 DOM
// 这里新增一个 <Text> 标签,vnode.type === 'Text'
// 需特殊处理
if (text === undefined) text = ''
text += String(child)
} else {
// 非标签的文字节点, vnode.type === undefined
// 例如 <Container>Text</Container>
// 中间的 Text 其实是同样需要一个文字元素
children[i] = vnode(
undefined,
{},
undefined,
undefined,
undefined,
undefined,
String(children[i])
)
}
}
}
}
return vnode(type, props, ref, key, children, undefined, text)
}
Исполняй, отлично! распечататьJSX
, вы можете увидеть преобразованное деревоVNode Tree
, и содержит всю полную информацию в тегах нашего шаблона.
Рисунок 1. Дерево виртуальных узлов
WebGL API
к лучшемуОтделить слой вида, нам нужно использовать некоторыеWebGL
СвязанныйAPI
Просто завернутый:
const Api = {
// 根据 标签类型 创建 视图元素
createElement(vnode) {
return new PIXI[vnode.type]()
},
// 创建、设置 文本元素
createTextElement(vnode) {
const { text: content = '', style } = vnode
return new PIXI.Text(content, style)
},
setTextContent(elm, content) {
if (elm && ['string', 'number'].includes(typeof content)) {
elm.text = content
}
},
// 获取父级
parentNode(elm) {
return elm && elm.parent
},
// 添加、删除子级
appendChild(parent, child) {
parent.addChild(child)
},
removeChild(parent, child) {
if (child && child.parent) {
parent.removeChild(child)
}
},
// 获取下一个兄弟元素
nextSibling(elm) {
const parent = Api.parentNode(elm)
if (parent) {
const index = parent.children.indexof(elm)
return parent.children[index + 1]
} else {
return undefined
}
},
// 插入到指定元素之前
insertBefore(parentElm, newElm, referenceElm) {
if (referenceElm) {
const refIndex = parentElm.children.indexOf(referenceElm)
parentElm.addChildAt(newElm, refIndex)
} else {
Api.appendChild(parentElm, newElm)
}
},
}
Эту часть можно назватьстыковочный слой, вы можете подключаться к различным платформам, если вы используете роднойDOM
изAPI
, то этоWeb
рендеринг, чем-то похожий наreact-dom
Готово.
Для удобства демонстрации используемpixi.js
прийти какинтерфейс рендеринга. здесь сWebGL
Независимая библиотека, можно пристыковать кПроизвольный кадр рендеринга.
Четвертая станция: Столпы творения — Рендеринг
имеютAPI уровня интерфейсаа такжеVNode
, вы можете начатьСоздайте реальный элемент представления, и в то же время согласноvnode.props
Синхронизация свойств и событий привязки. наконецСоздание детей рекурсивно:
// 根据 vnode 创建 视图元素
function createElm(vnode) {
const { children, type, text, props } = vnode
// vnode 的 type 为字符串时,表示其为 元素节点
// 依照 type 创建 视图元素
if (type && typeof type === 'string') {
// 调用接口
vnode.elm = Api.createElement(vnode)
// 递归创建子级元素,并添加到父元素中
if (Array.isArray(children)) {
// 创建子级
let i, l = children.length
for (i = 0; i < l; ++i) {
Api.appendChild(vnode.elm, createElm(children[i]))
}
}
} else if (type === undefined && text) {
// 被非 <Text> 包裹的文字节点
vnode.elm = Api.createTextElement(vnode)
}
// 元素创建成功时,执行设置属性
if (vnode.elm) setProps(vnode.elm, props)
return vnode.elm
}
// 属性处理
// 将 虚拟DOM 上的属性同步设置到 上面创建的真实元素 elm 上
function setProps(elm, props) {
if (elm && typeof props === 'object') {
const keys = Object.keys(props)
let l = keys.length, i
for (i = 0; i < l; i++) {
const key = keys[i]
const value = props[key]
if (key.startsWith('on')) {
// 事件绑定
if (typeof value === 'function') {
const evName = key.substring(2).toLowerCase()
elm.on(evName, value)
}
} else {
// 属性设置
elm[key] = value
}
}
}
}
Наконец, мы можем начать рендерингVNode
:
function render(vnode, parent) {
// 根据 vnode 创建出对应的元素
const elm = createElm(vnode)
// 并添加到容器中即可
Api.appendChild(parent, elm)
return elm
}
Готово, вот мы успешно завершилиJSX
Первый рендеринг . Следующим шагом является:Как обновить вид?Вот легендаDiff
Алгоритмы пригодятся!
Пятая станция: ключ времени и пространства - Diff
Говоря овиртуальный DOMможно сразу отметить его ядроdiff
алгоритм. когда мы проходимsetState
Когда вы переходите к обновлению компонента, он должен регенерировать новый полныйвиртуальное DOM-дерево, надо сравнитьРазличия между старыми и новыми деревьями, а затем обновить его соответствующим образом. этоВычислить разницу между двумя объектамиалгоритм.
этоdiff
Алгоритмы на самом деле используют множество сценариев, например, знакомый нам контроль версий кода. Два представленных кода нужно сравнить, чтобы выяснить различия, а затем обновить и сохранить. просто здесьфайл кодасталвиртуальный DOM. из-завиртуальный DOMОн довольно сложный, содержит множество свойств и может иметь очень глубокий уровень, поэтому, если вы используете рекурсию обычного цикла для сравнения, временная сложность составляетO(n^3)
, эта производительность неприемлема.
Поэтому, основываясь на некоторых особенностях рендеринга веб-представлений, талантливые инженеры прошли сравнениеСоздавайте правила, выбирайте и выбирайте, что значительно оптимизирует эффективность алгоритма. Содержит следующие три стратегии оптимизации:
1. Стратегия сравнения на одном уровне
Поскольку в большинстве веб-представлений мы редко перемещаем элементы между уровнями, перемещаемые элементы обычно появляются в списках на одном уровне, поэтому здесь может быть стратегия оптимизации:
Выполняйте сравнения только на одном уровне, игнорируя перемещение элементов между уровнями..
традиционныйdiff
Алгоритм требует двух слоев циклов, и требуется сравнение между каждыми двумя узлами. После установления сравнения на одном уровне узлы нужно сравнивать только с узлами того же уровня, как показано на следующем рисунке.
Рисунок 2. Стратегия сравнения различий
На данный момент производительность была значительно улучшена, а временная сложность оптимизирована дляO(n^2)
. Кроме того,Если есть перемещение между уровнями, старый элемент будет удален напрямую и воссоздан в новой позиции, что также может обеспечить точную строку обновления. Но это может привести к потере состояния.
2. Уникальная стратегия идентификации
Хотя мы сделали оптимизацию того же сравнения слоев, на данный момент есть проблема:
Например, рисунок 2, когдаC1
/ C2
Когда мы обмениваемся позициями, когда мы выполняем циклическое выравнивание, поскольку все они принадлежат к одному и тому же типу узлов, невозможно правильно отличить только по типу, и нельзя идентифицировать движение позиции. делать толькоC1
изменился наC2
,ПучокC2
изменился наC1
. Это будет не толькоУбыточность, и может привести ксостояние утрачено.
Лучший способ должен быть:C1
а такжеC2
Правильно меняйте местами. Ключевой момент:Как правильно определить и отличить узлы. Итак, вот введениеkey
в качестве уникального идентификатора используйтеtype
+ key
Точное местоположение узла может быть точно определено, что оптимизирует временную сложность дляO(n)
.
3. Стратегия шаблона компонентов
По сложности уже есть эффективные оптимизации. Следующим шагом является оптимизация с логического уровня.
Во-первых, самая большая потеря: целевой узел не может быть точно расположен, и необходимообход дереваНайдите обновленный целевой узел.
Как показано на рисунке 2, нам просто нужно обновитьD
узел, Но это должно быть самым фундаментальным уровнемA
узелслой за слоемdiff
, полное сравнение нового и старого виртуальных деревьев, тут явно лишние потери. еслиD
узелИзвлечено в отдельный модуль, можно просто вызватьD
узелсобственныйdiff
. Поэтому был введеншаблон компонента,смоглиФрагментация виртуального DOM.
Если нам нужно обновить в то же времяA
/ D
А узлы? На самом деле левая ветвьB1
Узлы не нужно обновлять, нет необходимостиdiff
. если вы можетеB1
Узел имеет идентификатор, который идентифицирует себя как цель без обновления и может использоваться в потоке обновления.Активно прерывать поток обновления. тогда толькоdiff
A
-> B2
-> D
Эта линия наверху. Вот что мы знаемshouldComponentUpdate
.
Разработка реализации
Сначала разберем следующие дваVNode diff
Возможные ситуации:
- Узлы разных типов:
- непосредственныйСоздать новый элемента такжезаменить старые элементы;
- Однотипные узлы:
- Обновление свойств, событий;
- Рекурсивно обновлять список дочерних элементов;
Рисунок 3. Блок-схема различий
Согласно этой блок-схеме, мы можем начать со входа.
function diff(oldVNode, newVNode) {
if (isSameVNode(oldVNode, newVNode)) {
// 开始 diff
// diffVNode
...
} else {
// 新节点替换旧节点
// replaceVNode
...
}
}
// 根据 type || key 判断是否为同类型节点
function isSameVNode(oldVNode, newVNode) {
return oldVNode.key === newVNode.key && oldVNode.type === newVNode.type
}
1. Когда старый и новый узлы отличаются, замените напрямую (replaceVNode
)
function replaceVNode(oldVNode, newVNode) {
// 移除旧元素
const { elm: oldElm } = oldVNode
const parent = Api.parentNode(oldElm)
Api.removeChild(parent, oldElm)
// 创建新元素,并添加到父级中
const newElm = createElm(newVNode)
Api.appendChild(parent, newElm)
}
2. Когда старый и новый узлы являются одним и тем же узлом, начните формальное сравнение (diffVNode
)
Согласно приведенной выше блок-схеме, мы также можем понять, что здесь мы должны сделать следующее:
-
Сравните свойства и события (
diffProps
); -
Рекурсивно сравнить списки детей: Здесь тоже три падежа;
-
старые и новые узлыесть список детей, затем введитеСравнение списков (
diffChildren
); -
старые узлы не имеютребенок,Новый узел имеетребенок, то сразуновыйновый ребенок (
addVNodes
); -
старые узлы имеютребенок,Новый узел неребенок, то сразуудалятьстарый ребенок (
removeVNodes
);
-
Исходя из вышеизложенного, сначала осознаемdiffVNode
эта функция.
function diffVNode(oldVNode, newVNode) {
const { elm, children: oldChild, text: oldText, props: oldProps } = oldVNode
const { children: newChild, text: newText, props: newProps } = newVNode
if (oldVNode === newVNode || !elm) return
// 已判断为同一节点,目的为 更新元素
// 因此直接复用旧元素
newVNode.elm = elm
// 比对属性与事件
diffProps(elm, oldProps, newProps)
const hasOldChild = !!(oldChild && oldChild.length)
const hasNewChild = !!(newChild && newChild.length)
// 判断为 元素节点 或者 文字节点
if (newText === undefined) {
// 元素节点
// 判断如何更新子级
if (hasOldChild && hasNewChild) {
// 新旧节点均存在子级列表时,直接 diff 列表
if (oldChild !== newChild) {
// diff 列表
diffChildren(elm, oldChild, newChild)
}
} else if (hasNewChild) {
// 旧节点 不包含子级,而新节点包含子级
// 则直接新增新子级
addVNodes(elm, null, newChild, 0, newChild.length - 1)
} else if (hasOldChild) {
// 新子级不包含元素,而旧节点包含子级
// 则需要删除旧子级
removeVNodes(elm, oldChild, 0, oldChild.length - 1)
} else if (oldText !== undefined) {
// 当新旧均无子级
// 这里有可能存在 <Text> 标签,且新内容为空
// 因此直接清空旧元素文字
Api.setTextContent(elm, '')
}
} else if (oldText !== newText) {
// 文字节点
// 当新旧文字内容不同时,直接修改内容
Api.setTextContent(elm, newText)
}
}
// 更新属性
function diffProps(elm, oldProps, newProps) {
if (oldProps === newProps || !elm) return
if (typeof oldProps === 'object' && typeof newProps === 'object') {
let keys = Object.keys(oldProps), i, l = keys.length
// 重置被删除的旧属性
for (i = 0; i < l; i++) {
const key = keys[i]
const oldValue = oldProps[key], newValue = newProps[key]
if (key.startsWith('on')) {
/*
* 当存在旧事件,且新旧值不一致时
* 事件解绑
*/
if (typeof oldValue === 'function' && oldValue !== newValue) {
const evName = key.substring(2).toLowerCase()
elm.off(evName, oldValue)
}
} else {
/*
* 属性被赋值时会被自动重置
* 只需要重置被删除的属性即可
*/
if (newValue === undefined) {
// 元素属性默认值
elm[key] = DEFAULT_PROPS[key]
}
}
}
// 设置新属性
setProps(elm, newProps)
}
}
Сравните список детей (diffChildren
)
На самом деле сравнивать атрибуты и события относительно просто.Сравнение списка подуровней — это основная и наиболее проверяющая производительность часть всего алгоритма сравнения., поэтому алгоритм сравнения списков здесь определяет производительность всего рендеринга обновлений. существуетвиртуальный DOMКогда он впервые появился, он был относительно прост в использовании.Сначала в глубину (DFS) + выравнивание сортировкиПуть. Позже появились более эффективные и до сих пор используемыеАлгоритм двустороннего сравнения + сравнение ключевого значения, прямо сказатьdiff
Эффективность повышается на один уровень и лучше понимается Существует три стратегии согласования с разными приоритетами:
-
Приоритет от нового к старому спискуоба концаизчетыре узлаНачалопара за парой;
-
Если ни один не совпадает, попробуйтесравнение ключевого значения;
-
Такие какключевое значениеЕсли он совпадает, переместите и обновите узел;
-
Если не совпадает, то в соответствующей позицииДобавить новый узел;
-
-
После того, как все сравнения сделаны в конце, в спискеостальные узлывоплощать в жизньудалить или добавить;
Здесь все в замешательстве 🤣. Все в порядке, просто сначала немного поймите это. Затем мы напрямую используем анимацию, чтобы более интуитивно увидеть конкретный процесс алгоритма сравнения на обоих концах. Вот пример дочернего списка на рисунке 2, а именно:
-
oldChildren содержит 5 дочерних узлов:
[A, B, C, D, E]
; -
Дети newChildren изменены на:
[F, C, B, D, A]
;
Tips:
Например, A в старом и новом списках означает, что два узлаУзлы одного типа, то есть узел
type / key
равны;
Первый раунд сравнения:
Рисунок 4. Первый раунд цикла сравнения
-
1.Предпочтительно начинать с положительного направления обоих концов нового и старого списков,не то же самое:
A !== F
а такжеE !== A
; -
2.Два конца перекрестно ссылаются и находятсяПервый элемент старого списка совпадает с последним элементом нового списка. (
isSameVNode
):-
поместить первый элемент в старый списокпереехатьдо последнего элемента;
-
Продолжайте рекурсивно различать старые и новые
A
;
-
Второй раунд сравнения:
Рисунок 5. Второй раунд цикла сравнения
-
1.Точно так же выравнивание вперед предпочтительно с обоих концов,
B !== F & E !== D
; -
2.Выровнены по обоим концам,
B !== D & E !== F
; -
3.Войтиключевое сравнение:
-
Фиксированный дубльПервый элемент нового списка (
F
); -
цикл со старым спискомсписок ключейпоэлементное сравнениене может соответствовать;
-
Поэтому этоновый узел,непосредственныйСоздать и добавить в начало старого списка;
-
Третий раунд сравнения:
Рисунок 6. Третий цикл diff
-
1.Оба конца направлены вперед и выровнены поперек, и они не совпадают;
-
2.Войтиключевое сравнение;
-
Фиксированный дубльПервый элемент нового списка (
C
); -
Цикл сравнивает элемент за элементом со старым списком,соответствоватько второму пункту старого списка;
-
затем поместите второй элемент в старый списокперейти к началу, и продолжайте рекурсивно продолжать сравнивать старые и новые
C
;
-
Четвертый и пятый раунды сравнения:
Рисунок 7. Четвертый и пятый цикл diff
-
1.Сравнивается первый элемент, и совпадение успешно;
-
Рекурсивно продолжать различать старые и новые
B
; -
Переместите нижний индекс, чтобы перейти к следующему раунду сравнения;
-
-
2.Тот же первый матч;
-
Рекурсивно продолжать различать старые и новые
D
; -
Переместите нижний индекс, чтобы перейти к следующему раунду сравнения;
-
-
3.Новый цикл списка закончился,удалятьостальные узлы в старом списке;
После пяти раундов сравнения старый список был успешно обновлен. Чтобы включить три стратегии, которые мы объяснили выше, пример используется в более сложном и менее частом сценарии. В повседневном бизнесе большинство из них будут проще и эффективнее. Далее мы реализуем этот алгоритм в коде, используяДвойной курсор списка + цикл whileПуть:
function diffChildren(parentElm, oldChild, newChild) {
/**
* 更新子级列表
* 双列表游标 + while
*/
// 初始化游标
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldChild.length - 1, newEndIdx = newChild.length - 1
// 列表首尾节点
let oldStartVNode = oldChild[0], oldEndVNode = oldChild[oldEndIdx]
let newStartVNode = newChild[0], newEndVNode = newChild[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, before
/**
* 当起始游标 < 终止游标时,
* 表示列表中仍有未 diff 的节点
* 进入循环
*/
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
/**
* 1. 排除非有效的节点
* 剔除列表中包含的 undefined || false || null
*/
if (oldStartVNode == null) {
oldStartVNode = oldChild[++oldStartIdx]
} else if (oldEndVNode == null) {
oldEndVNode = oldChild[--oldEndIdx]
} else if (newStartVNode == null) {
newStartVNode = newChild[++newStartIdx]
} else if (newEndVNode == null) {
newEndVNode = newChild[--newEndIdx]
} else if (isSameVNode(oldStartVNode, newStartVNode)) {
/**
* 2. 正反向两两比对列表首项与末项匹配成功
* 移动游标,递归 diff 两个节点
* 均未匹配上,则进入 3. key 值比对
*/
diff(oldStartVNode, newStartVNode)
oldStartVNode = oldChild[++oldStartIdx]
newStartVNode = newChild[++newStartIdx]
} else if (isSameVNode(oldEndVNode, newEndVNode)) {
diff(oldEndVNode, newEndVNode)
oldEndVNode = oldChild[--oldEndIdx]
newEndVNode = newChild[--newEndIdx]
} else if (isSameVNode(oldStartVNode, newEndVNode)) {
Api.insertBefore(parentElm, oldStartVNode.elm, Api.nextSibling(oldEndVNode.elm))
diff(oldStartVNode, newEndVNode)
oldStartVNode = oldChild[++oldStartIdx]
newEndVNode = newChild[--newEndIdx]
} else if (isSameVNode(oldEndVNode, newStartVNode)) {
Api.insertBefore(parent, oldEndVNode.elm, oldStartVNode.elm)
diff(oldEndVNode, newStartVNode)
oldEndVNode = oldChild[--oldEndIdx]
newStartVNode = newChild[++newStartIdx]
} else {
/**
* 3. 两端比对均不匹配
* 进入 key 值比对
*/
// 根据剩余的旧列表创建 key list
if (!oldKeyToIdx) {
oldKeyToIdx = createKeyList(oldChild, oldStartIdx, oldEndIdx)
}
// 判断新列表项的 key值 是否存在
idxInOld = oldKeyToIdx[newStartVNode.key || '']
if (!idxInOld) {
/*
* 4. 新 key 值在旧列表中不存在
* 直接将该节点插入
*/
Api.insertBefore(parentElm, createElm(newStartVNode), oldStartVNode.elm)
newStartVNode = newChild[++newStartIdx]
} else {
/*
* 5. 新 key 在旧列表中存在时
* 继续判断是否为同类型节点
*/
elmToMove = oldChild[idxInOld]
if (isSameVNode(elmToMove, newStartVNode)) {
/*
* 6. 新旧节点类型一致
* key 有效,直接移动并 diff
*/
Api.insertBefore(parentElm, elmToMove.elm, oldStartVNode.elm)
diff(elmToMove, newStartVNode)
// 清空旧列表项
// 后续的比对可以直接跳过
oldChild[idxInOld] = undefined
} else {
/*
* 7. 新旧节点类型不一致
* key 效,直接创建元素并插入
*/
Api.insertBefore(parentElm, createElm(newStartVNode), oldStartVNode.elm)
}
newStartVNode = newChild[++newStartIdx]
}
}
}
/*
* 8. 当有游标列表为空时,则结束循环,进入策略3
* 当 旧列表为空 时,则创建并插入新列表中的剩余节点
* 当 新列表为空 时,则删除旧列表中的剩余节点
*/
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 新增节点
const vnode = newChild[newEndIdx + 1]
before = vnode ? vnode.elm : null
addVNodes(parentElm, before, newChild, newStartIdx, newEndIdx)
} else {
// 删除节点
removeVNodes(parentElm, oldChild, oldStartIdx, oldEndIdx)
}
}
}
Поздравляем детей~ Мы завершили легендарную两端比对算法
Немного 🥳. На самом деле, пока логика ясна в соответствии с приоритетом сравнения и суждения выполняются одно за другим, мышление остается относительно ясным.
Практические советы
Написав приведенный выше реальный код и поняв принцип реализации, мы действительно можем найти несколько хороших практик для оптимизации нашего кода и повышения производительности.
1.props
Доставка
JSX
Средний тег может передавать атрибуты, проще всегопередать по значению:
const tmp = <Container text="text" data={{ a: 1 }}>Text</Container>
Потому что, сравнивая, оцениваяprops
Когда есть изменения, используйтеконгруэнтныйСравнение. Таким образом, когда отображаемое значение равноэталонный объект, если прямо как выше,data
написать наJSX
, то каждый разdiff
Времяdata
Все значения являются совершенно новыми объектами и не будут равными. Даже если свойства объекта одинаковы, каждое свойство необходимо каждый раз циклически сравнивать.
Поэтому рекомендуется:
- Когда значение свойстваэталонный объекткогда, например
Object
,Array
,Function
и т. д., используйте напрямуюпройти по ссылке:
const data = { a: 1 }
const tmp = <Container text="text" data={data}>Text</Container>
- Точно так же, когда данные необходимо изменить, не изменяйте исходный объект напрямую, аСоблюдайте принцип неизменности данных, который создает новый объект.
Такое предложение может эффективно снизитьdiff
Когда потери производительности велики, когда сцена сложна, преимущества значительны.
2. Структура дерева рендеринга стабильна
diff
используемый алгоритмСравнение на одном уровнеПоэтому, если это перемещение между иерархиями, новый узел будет воссоздан, а исходный узел будет удален, а не настоящее перемещение. так что гарантияСтруктура дерева рендеринга стабильнаМожет эффективно улучшить производительность.
-
старайтесь избегать узлаПеремещайтесь по уровням;
-
Если это неизбежно, рассмотритесинхронизация состоянийЭта проблема;
-
Движение на одном уровне также
diff
Когда требуются дополнительные круговые сравнения, следует сократить ненужные частые перемещения;
3. Использование ключей
когда нужно в спискеVNode
издвижение на одном уровнепри добавлении уникального идентификатораkey
может эффективно улучшитьdiff
производительность, избегайте элементовперерисовать.
-
Позаботьтесь о том, чтобы
VNode.key
существуетdiff
до и послепоследовательный, чтобы эффективно повысить производительность и избежать использованияindex
,Math.random
, отметка времени и т. д.; -
Даже при отображении некруглого списка добавьте
key
Значение также вступит в силу, разныеkey
Это приведет к тому, что узел будет оценен как неоднородный узел, чтобы его можно было заменить;
4. Компонентизация
-
Высокая возможность повторного использованияи нужноЧастые обновленияУзлы извлекаются вкомпоненты, сделаю
VNode Tree
Фрагментация для более эффективного выполнениялокальное обновление, уменьшить срабатываниеdiff
Количество узлов, повышение производительности и повышение скорости повторного использования кода; -
Но из-за создания компонентов и
diff
По сравнению с обычными узлами он болеесложный, необходимо выполнить, например, жизненный цикл, сравнение компонентов и т. д., поэтому необходимоРазумное планирование,избегатьСлишком много компонентовПривести кПотеря памяти и снижение производительности,НемногоСтатические элементы с низким коэффициентом повторного использованияРазумнее использовать узел элемента напрямую;
Остановка 6: Места отдыха - Резюме
Из-за нехватки места эта статья пока будет завершена здесь. Подведем итоги и рассмотрим части, которые мы выполнили:
-
1.
JSX
этоСинтаксис динамического шаблона,пройти черезBabel
компилируется вcreateElement
функция; -
2.
createElement
БудуJSX
Перевести вВиртуальный дом (VNode), который содержит полную информацию о теге (тип, атрибут, список дочерних элементов); -
3. виртуальный DOMэтоПожертвовать минимальной производительностью и пространством для архитектурной оптимизациипуть, егосоставнойтак же какразъединениеИдея улучшает масштабируемость, возможность повторного использования и ремонтопригодность проекта.кроссплатформенный рендерингзаложить фундамент;
-
4.путем реализации
createElm
а такжеrender
,ЗаканчиватьVNode
изпервый рендер; -
5.
Diff
этоВычислить разницу между двумя VNodesалгоритм, использующийСравнение на одном уровне, уникальная идентификация, компонентный режимПроизводительность алгоритма сравнения оптимизирована, а временная сложность снижена доO(n)
; -
6.Сравнение списка (
diffChildren
) с использованиемАлгоритм двустороннего сравнения + сравнение ключевого значенияалгоритм, значительно улучшенныйDiff
эффективность; -
7.выполнить
diffVNode
,diffProps
,diffChildren
,ЗаканчиватьVNode
изДинамическое обновление;
Мы завершили ядро фреймворкаJSX
- Render
- Diff
Основной механизм обновления рендеринга, закладывающий основу для нижнего слоя. В следующей статье мы продолжим развивать этокомпонентный (Component
),обновление компонента (setState
)так же какЖизненный цикл. Я надеюсь, что это поможет вам лучше понять React и освоить отличное программирование.
Класс, завершенный этой статьейReact
Рамка моя собственная за последние несколько месяцевWeb
Эксперименты и исследования в игре, цель состоит в том, чтобы надеяться, что сможетСнизить порог для традиционных фронтенд-инженеров для разработки игра такжеВнедрить режим фронтенд-разработки для повышения эффективности разработки, так что фронтенд-разработка и разработка игр образуют определенную степень взаимодополняющих преимуществ. Конечно, эта цель большая и не простая, и предстоит еще много работы по планированию. Заинтересованные друзья переходят на github, чтобы просмотреть полный код:
Наконец, во второй половине прошлого года мы открыли технологическую компанию в Сямыне, в основном вигровое полеа такжеK12 STEAM Образованиеусилий в направлении. Я очень рад связаться со мной напрямую, если вы заинтересованы в получении дополнительной информации или хотите обсудить со мной! 🥳~~
Tips:
Видя, как блогер так усердно пишет, я прошу лайков, внимания и звездочек!Больше статей захлопнули ->
Электронная почта: 159042708@qq.com WeChat/QQ: 159042708