Работа с 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,393