Работа с DOM в JS фреймворках
Мостовой Никита
@xnimorz
nik.mostovoy@gmail.com
Скролл
Скролл
Reconciliation
Скролл
Reconciliation => inline callbacks, selectors
Скролл
Reconciliation => inline callbacks, selectors
6
Скролл
Reconciliation => inline callbacks, selectors
6
6
6
6
6
6
8
Много
- Pure
- Pure
- Selectors
- Pure
- Cache \ memoization
- Selectors
Ну, так элементов много,
react-virtualized
Ну, так элементов много,
react-virtualized
Как бы да, но нет...
6
6
6
6
6
6
8
Много
- Большой проект с 0
- Большой проект с 0
- Миграция hh.ru на react
А давайте, давайте подумаем над задачей?
А давайте, давайте подумаем над задачей?
Есть два дерева, необходимо определить измененные элементы
Как мы поймем, что изменилось?
Как мы поймем, что изменилось?
BFS (Поиск в ширину)
DFS (Поиск в глубину)
DFS (Поиск в глубину)
+ Линейная сложность O(n)
Алгоритм поиска в ширину или глубину
+ Линейная сложность O(n)
+ В базовом варианте просты для реализации
Алгоритм поиска в ширину или глубину
+ Линейная сложность O(n)
+ В базовом варианте просты для реализации
– Мы полностью завязаны на порядок элементов
Алгоритм поиска в ширину или глубину
Алгоритм поиска в ширину или глубину
+ Линейная сложность O(n)
+ В базовом варианте просты для реализации
– Мы полностью завязаны на порядок элементов
! Не является полноценным алгоритмом сверки
Алгоритм поиска в ширину или глубину
+ Линейная сложность O(n)
+ В базовом варианте просты для реализации
– Мы полностью завязаны на порядок элементов
! Не является полноценным алгоритмом сверки
! В нашем случае является жадным алгоритмом
Изменился порядок элементов №1
Изменился порядок элементов №1
Изменился порядок элементов №1
Изменился порядок элементов №1
Изменился порядок элементов №1
Изменился порядок элементов №1
Изменился порядок элементов №1
Изменился порядок элементов №1
O(n )
2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
Изменился порядок элементов №2
[
]
Изменился порядок элементов №2
[
]
- Iterable
Изменился порядок элементов №2
[
]
- Iterable
- childNodes (HTMLCollection)
Изменился порядок элементов №2
[
]
Изменился порядок элементов №2
[
]
Изменился порядок элементов №2
Изменился порядок элементов №2
const Item = el.childNodes[1];
Изменился порядок элементов №2
const Item = el.childNodes[1];
const Item = model.get('8');
Изменился порядок элементов №2
const Item = el.childNodes[1];
const Item = model.get('8');
{}
Map
Изменился порядок элементов №2
LinkedHashMap:
- Doubly linked list
- HashMap (key)
Изменился порядок элементов №2
- Keyed элементы позволяют нам инвалидировать меньше
Изменился порядок элементов №2
- Keyed элементы позволяют нам инвалидировать меньше
- Сравнение происходит поэлементно за 1 проход, без перебора
Изменился порядок элементов №2
- Keyed элементы позволяют нам инвалидировать меньше
- Сравнение происходит поэлементно за 1 проход, без перебора
- Key — ключи хешмапы
Как понять что изменился только тип?
Как понять что изменился только тип?
Как понять что изменился только тип?
O(n )
3
O(n )
3
O(n )
3
8447 — O(n)
O(n )
3
8447 — O(n)
71,351,809 — O(n )
2
O(n )
3
8447 — O(n)
71,351,809 — O(n )
2
602,708,730,623 — O(n )
3
Как понять что изменился только тип?
Важное уточнение
- Мы говорим про механизм сверки
Важное уточнение
- Мы говорим про механизм сверки
- Мы не говорим об оптимизациях для уровня компонентов (PureComponent \ SCU в реакте, автоматическое отслеживание данных во vue)
Важное уточнение
- Мы говорим про механизм сверки
- Подобный механизм сверки может работать как с DOM так и с VDOM
- Мы не говорим об оптимизациях для уровня компонентов (PureComponent \ SCU в реакте, автоматическое отслеживание данных во vue)
Что мы имеем в итоге?
- Инвалидация узлов (сверка) — O(n)
Что мы имеем в итоге?
- Инвалидация узлов (сверка) — O(n)
- Ререндер (внесение изменений в DOM) — O(n)
Что мы имеем в итоге?
- Инвалидация узлов (сверка) — O(n)
- Layout
- Ререндер (внесение изменений в DOM) — O(n)
Что мы имеем в итоге?
- Инвалидация узлов (сверка) — O(n)
- Layout
На ререндер \ layout мы можем повлиять только улучшая код конечного приложения
- Ререндер (внесение изменений в DOM) — O(n)
Как мы это достигли?
- Структура данных
Как мы это достигли?
- Структура данных
- "Инвалидируй, если не уверен"
Как мы это достигли?
- Структура данных
- "Инвалидируй, если не уверен"
- Поэлементное сравнение
А что если компонент будет сообщать какие узлы нужно инвалидировать напрямую?
import React, { useState } from "react";
import Component from "./Component";
function App() {
const [ articles, setArticles ] = useState([
{ id: 1, text: "foo" },
{ id: 2, text: "bar" },
{ id: 2, data: {} },
]);
return (
<div>
<Title>Hello world</Title>
<Content articles={articles} />
</div>
);
}
function Title({ children }) {
return <H1>{children}</H1>;
}
function Content({ articles }) {
return (
<div>
{articles.map((item) => {
if (item.text) {
return <article key={item.id}>{text}</article>;
}
return <Component key={item.id} article={item} />;
})}
</div>
);
}
import React, { useState } from "react";
import Component from "./Component";
function App() {
const [ articles, setArticles ] = useState([
{ id: 1, text: "foo" },
{ id: 2, text: "bar" },
{ id: 2, data: {} },
]);
return (
<div>
<Title>Hello world</Title>
<Content articles={articles} />
</div>
);
}
function Title({ children }) {
return <H1>{children}</H1>;
}
function Content({ articles }) {
return (
<div>
{articles.map((item) => {
if (item.text) {
return (
<article key={item.id}>
{text}
</article>
);
}
return (
<Component key={item.id} article={item} />
);
})}
</div>
);
}
import React, { useState } from "react";
import Component from "./Component";
function App() {
const [ articles, setArticles ] = useState([
{ id: 1, text: "foo" },
{ id: 2, text: "bar" },
{ id: 2, data: {} },
]);
const [ title, setTitle ] = useState('Hello world');
const invalidator = {
div: {
children: {
h1: {deps: [title]},
Content: {deps: [title ? title : 'stub', articles]},
},
deps: [],
}
}
return (
<div>
<h1>{title}</h1>
<Content title={title ? title : 'stub'} articles={articles} />
</div>
);
}
import React, { useState } from "react";
import Component from "./Component";
function App() {
const [ articles, setArticles ] = useState([
{ id: 1, text: "foo" },
{ id: 2, text: "bar" },
{ id: 2, data: {} },
]);
const [ title, setTitle ] = useState('Hello world');
const updater = {
h1: {
update: (ctx, $1) => {ctx.el.textContent = $1}
},
Content: {
update: () => {},
}
}
const invalidator = {
div: {
children: {
h1: {deps: [title]},
Content: {deps: [title ? title : 'stub', articles]},
},
deps: [],
}
}
return (
<div>
<h1>{title}</h1>
<Content title={title ? title : 'stub'} articles={articles} />
</div>
);
}
import React, { useState } from "react";
import Component from "./Component";
function App() {
const [ articles, setArticles ] = useState([
{ id: 1, text: "foo" },
{ id: 2, text: "bar" },
{ id: 2, data: {} },
]);
const [ title, setTitle ] = useState('Hello world');
const updater = {
h1: {
update: (ctx, $1) => {ctx.el.textContent = $1}
},
Content: {
update: () => {},
}
}
const invalidator = {
div: {
children: {
h1: {deps: [title]},
Content: {deps: [title ? title : 'stub', articles]},
},
deps: [],
}
}
const invertedInvalidator = {
title: [h1, Content],
articles: [Content],
}
return (
<div>
<h1>{title}</h1>
<Content title={title ? title : 'stub'} articles={articles} />
</div>
);
}
Как такое достигается?
1) Обходим AST, по пути кешируя все Identifier
Как такое достигается?
1) Обходим AST, по пути кешируя все Identifier
2) Сохраняем в переменную блок начала вывода компонента (jsx, template)
Как такое достигается?
3) Ищем в выводе компонента обращения к Identifier
1) Обходим AST, по пути кешируя все Identifier
2) Сохраняем в переменную блок начала вывода компонента (jsx, template)
Как такое достигается?
3) Ищем в выводе компонента обращения к Identifier
4) Помечаем эти identifier как аргументы для invalidator
1) Обходим AST, по пути кешируя все Identifier
2) Сохраняем в переменную блок начала вывода компонента (jsx, template)
Как такое достигается?
3) Ищем в выводе компонента обращения к Identifier
4) Помечаем эти identifier как аргументы для invalidator
1) Обходим AST, по пути кешируя все Identifier
2) Сохраняем в переменную блок начала вывода компонента (jsx, template)
5) Перед выводом компонента описываем invalidator
<script>
import Title from "./Title";
import Content from "./Content";
</script>
<div>
<Title text="Hello world" />
<Content />
</div>
App.svelte
<script>
import Title from "./Title";
import Content from "./Content";
</script>
<div>
<Title text="Hello world" />
<Content />
</div>
App.svelte
<script>
export let text
</script>
<h1>
{text}
</h1>
Title.svelte
App.svelte
<script>
import Component from "./Component";
import articles from "./store";
</script>
<div>
{#each $articles as item (item.id)}
{#if item.text}
<article>{item.text}</article>
{:else}
<Component article={item} />
{/if}
{/each}
</div>
Content.svelte
<script>
export let text
</script>
<h1>
{text}
</h1>
Title.svelte
<script>
import Title from "./Title";
import Content from "./Content";
</script>
<div>
<Title text="Hello world" />
<Content />
</div>
App.svelte
<script>
import Title from "./Title";
import Content from "./Content";
</script>
<div>
<Title text="Hello world" />
<Content />
</div>
App.svelte
<script>
import Component from "./Component";
import articles from "./store";
</script>
<div>
{#each $articles as item (item.id)}
{#if item.text}
<article>{item.text}</article>
{:else}
<Component article={item} />
{/if}
{/each}
</div>
Content.svelte
import { writable } from "svelte/store";
export const articles = writable([
{ id: 1, text: "foo" },
{ id: 2, text: "bar" },
{ id: 3, data: { foo: "bar" } },
]);
Store.js
<script>
export let text
</script>
<h1>
{text}
</h1>
Title.svelte
И это быстро?
И это быстро?
Не совсем
И это быстро?
И это быстро?
1) Компонент описывает свою инвалидацию. Нужен механизм уведомления. В простом варианте — Event emitter
И это быстро?
1) Компонент описывает свою инвалидацию. Нужен механизм уведомления. В простом варианте — Event emitter
2) Если компонент описывает свое обновление — встает вопрос согласованности обновления у всех компонентов на странице
И это быстро?
3) Если нужны дополнительные свистелки (Suspense и пр. — также встает вопрос согласованности)
1) Компонент описывает свою инвалидацию. Нужен механизм уведомления. В простом варианте — Event emitter
2) Если компонент описывает свое обновление — встает вопрос согласованности обновления у всех компонентов на странице
Итого
- Полноценные алгоритмы сверки — крайне дорогие
Итого
- Полноценные алгоритмы сверки — крайне дорогие
- Современные фреймворки решают проблему либо в рантайме улучшенными алгоритмами обхода, либо пытаются прекомпилировать код
Итого
- Полноценные алгоритмы сверки — крайне дорогие
- Современные фреймворки решают проблему либо в рантайме улучшенными алгоритмами обхода, либо пытаются прекомпилировать код
- Прекомпилированный код позволяет решить вопрос инвалидации за O(1), но встает вопрос корректного согласования обновлений
Итого
- Полноценные алгоритмы сверки — крайне дорогие
- Современные фреймворки решают проблему либо в рантайме улучшенными алгоритмами обхода, либо пытаются прекомпилировать код
- Прекомпилированный код позволяет решить вопрос инвалидации за O(1), но встает вопрос корректного согласования обновлений
- Не обязательно решение с прекомпиляцией будет быстрее runtime решения на практике
Мостовой Никита
@xnimorz
nik.mostovoy@gmail.com
Работа с DOM в JS фреймворках
By Nik Mostovoy
Работа с DOM в JS фреймворках
- 1,414