Часть 2

В Части 1 я упомянул функцию $$invalidate, я объяснил, что концептуально функция $$invalidate работает следующим образом:

Но это не точная реализация функции $$invaldiate. Итак, в этой статье мы рассмотрим, как $$invalidate реализован в Svelte.

На момент написания Svelte находится на версии v3.20.1.

До версии 3.16.0

Есть большая оптимизация, которая изменяет базовую реализацию функции $$invalidate в v3.16.0, а именно в #3945. Основная концепция не изменится, но будет намного проще понять $$invalidate до изменения и узнать об изменении оптимизации отдельно.

Давайте объясним некоторые из переменных, которые вы увидите, некоторые из которых были введены в Часть 1:

$$.ctx

Официального названия для него нет. Вы можете назвать его контекст, так как это контекст, на котором основан шаблон для отображения в DOM.

Я назвал это переменными экземпляра. Поскольку это объект JavaScript, который содержит все переменные, которые вы:

  • объявлен в теге <script>
  • мутировал или переназначил
  • ссылка в шаблоне

который принадлежит экземпляру компонента.

Сами переменные экземпляра могут иметь примитивное значение, объект, массив или функцию.

Функция instance создает и возвращает объект ctx.

Функции, объявленные в теге <script>, будут ссылаться на переменную экземпляра, область действия которой ограничена закрытием функции instance:

Стройный РЕПЛ

Всякий раз, когда создается новый экземпляр компонента, вызывается функция instance, а объект ctx создается и фиксируется в новой области замыкания.

$$.грязный

$$.dirty — это объект, который используется для отслеживания того, какая переменная экземпляра только что изменилась и должна быть обновлена ​​в DOM.

Например, в следующем компоненте Svelte:

Стройный РЕПЛ

Начальный $$.dirty равен null (исходный код).

Если вы нажмете кнопку + Ловкость, $$.dirty превратится в:

Если вы нажмете кнопку "Повысить уровень", $$.dirty превратится в:

$$.dirty полезен для Svelte, так как он не обновляет DOM без необходимости.

Если вы посмотрите на функцию p (update) в скомпилированном коде, вы увидите, что Svelte проверяет, отмечена ли переменная в $$.dirty перед обновлением DOM.

После того, как Svelte обновляет DOM, $$.dirty возвращается к null, чтобы указать, что все изменения были применены к DOM.

$$ недействителен

$$invalidate — это секрет реактивности в Svelte.

Всякий раз, когда переменная

Svelte обернет назначение или обновление с помощью функции $$invalidate:

функция $$invalidate будет:

  1. обновить переменную в $$.ctx
  2. отметить переменную в $$.dirty
  3. запланировать обновление
  4. вернуть значение выражения присваивания или обновления

"Исходный код"

Одно интересное замечание о функции $$invalidate заключается в том, что она оборачивает выражение присваивания или обновления и возвращает то, что оценивает выражение.

Это делает $$invalidate цепочкой:

Казалось сложным, когда в одном операторе много выражений присваивания или обновления! 🙈

2-й аргумент $$invalidate — это дословное выражение присваивания или обновления. Но если оно содержит какое-либо подвыражение присваивания или обновления, мы рекурсивно заключаем его в $$invalidate.

В случае, когда выражение присваивания изменяет свойство объекта, мы передаем объект в качестве третьего аргумента функции $$invalidate, например:

Итак, мы обновляем переменную "obj" до obj вместо значения 2-го аргумента, "hello".

schedule_update

schedule_update назначает Svelte обновление DOM с внесенными на данный момент изменениями.

Svelte на момент написания (v3.20.1) использует очередь микрозадач для пакетного обновления обновлений. Фактическое обновление DOM происходит в следующей микрозадаче, так что любые синхронные $$invalidate операции, происходящие в рамках одной задачи, объединяются в следующее обновление DOM.

Чтобы запланировать следующую микрозадачу, Svelte использует обратный вызов Promise.

В flush мы вызываем обновление для каждого компонента, отмеченного как грязный:

"Исходный код"

Итак, если вы напишете компонент Svelte следующим образом:

Стройный РЕПЛ

Обновление DOM для givenName и familyName происходит в одной и той же микрозадаче:

  1. Нажмите "Обновить", чтобы вызвать функцию update.
  2. $$invalidate('givenName', givenName = 'Li Hau')
  3. Пометить переменную givenName грязной, $$.dirty['givenName'] = true
  4. Запланируйте обновление, schedule_update()
  5. Поскольку это первое обновление в стеке вызовов, поместите функцию flush в очередь микрозадач.
  6. $$invalidate('familyName', familyName = 'Tan')
  7. Пометить переменную familyName грязной, $$.dirty['familyName'] = true
  8. Запланируйте обновление, schedule_update()
  9. Начиная с update_scheduled = true ничего не делать.
  10. — Конец задачи —
  11. — Начало микрозадачи —
  12. flush() вызывает update() для каждого компонента, отмеченного как грязный
  13. Звонит $$.fragment.p($$.dirty, $$.ctx).
  • $$.dirty сейчас { givenName: true, familyName: true }
  • $$.ctx сейчас { givenName: 'Li Hau', familyName: 'Tan' }

14. In function p(dirty, ctx),

  • Обновите 1-й текстовый узел до $$.ctx['givenName'], если $$.dirty['givenName'] === true
  • Обновите 2-й текстовый узел до $$.ctx['familyName'], если $$.dirty['familyName'] === true

15. Сбрасывает $$.dirty на null

16. …

17. — Конец микрозадачи —

tl;dr

  • Для каждого назначения или обновления Svelte вызывает $$invalidate, чтобы обновить переменную в $$.ctx и пометить переменную как грязную в $$.dirty.
  • Актуальное обновление DOM помещается в следующую очередь микрозадач.
  • Чтобы обновить DOM для каждого компонента, вызывается компонент $$.fragment.p($$.diry, $$.ctx).
  • После обновления DOM $$.dirty сбрасывается на null.

v3.16.0

Одним из больших изменений в версии 3.16.0 является PR #3945, а именно отслеживание изменений на основе битовой маски.

Вместо того, чтобы помечать переменную как грязную с помощью объекта:

Svelte присваивает каждой переменной индекс:

и использует битовую маску для хранения грязной информации:

который намного компактнее предыдущего скомпилированного кода.

Битовая маска

Для тех, кто не понял, позвольте мне быстро объяснить, что это такое.

Конечно, если вы хотите узнать об этом больше, не стесняйтесь читать более подробное объяснение, например, это и это.

Самый компактный способ представления группы true или false — использовать биты. Если бит 1, это true, а если 0, то false.

Число может быть представлено в двоичном формате, 5 равно 0b0101 в двоичном формате.

Если 5 представлен в виде 4-битного двоичного файла, то он может хранить 4 логических значения, с 0-м и 2-м битами как true и 1-м и 3-м битами как false (чтение справа налево, от младшего бита к старшему биту).

Сколько логических значений может хранить число?

Это зависит от языка, 16-битное целое число в Java может хранить 16 логических значений.

В JavaScript числа могут быть представлены в 64 битах. Однако при использовании побитовых операций над числом JavaScript будет обрабатывать число как 32-битное.

Чтобы проверить или изменить логическое значение, хранящееся в числе, мы используем побитовые операции.

Второй операнд, который мы используем в побитовой операции, похож на маску, которая позволяет нам нацеливаться на определенный бит в 1-м числе, в котором хранятся наши логические значения.

Мы называем маску bitmask.

Битовая маска в Svelte

Как упоминалось ранее, мы присваиваем каждой переменной индекс:

Поэтому вместо того, чтобы возвращать переменную экземпляра как объект JavaScript, мы теперь возвращаем ее как массив JavaScript:

Доступ к переменной осуществляется через индекс, $$.ctx[index] вместо имени переменной:

Функция $$invalidate работает так же, за исключением того, что она принимает индекс вместо имени переменной:

$$.dirty теперь хранит список номеров. Каждое число содержит 31 логическое значение, каждое логическое значение указывает, является ли переменная этого индекса грязной или нет.

Чтобы установить переменную как грязную, мы используем побитовую операцию:

И чтобы проверить, является ли переменная грязной, мы также используем побитовую операцию!

При использовании битовой маски $$.dirty теперь сбрасывается на [-1] вместо null.

Разное: -1 – это 0b1111_1111 в двоичном формате, где все биты – 1.

Разрушение $$.dirty

Одна оптимизация размера кода, которую делает Svelte, заключается в том, чтобы всегда деструктурировать массив dirty в upфункции даты, если переменных меньше 32, поскольку мы все равно всегда будем обращаться к dirty[0]:

tl;dr

  • Базовый механизм для $$invalidate и schedule_update не меняется.
  • Используя битовую маску, скомпилированный код намного компактнее

Реактивная декларация

Svelte позволяет нам объявлять реактивные значения с помощью помеченного оператора, $:

Стройный РЕПЛ

Если вы посмотрите на скомпилированный вывод, то обнаружите, что в функцииinstance появились декларативные операторы:

Попробуйте изменить порядок реактивных объявлений и понаблюдайте за изменениями в скомпилированном выводе:

Стройный РЕПЛ

Некоторые наблюдения:

  • При наличии реактивных объявлений Svelte определяет пользовательский метод $$.update.
  • $$.update по умолчанию является неактивной функцией. (См. src/runtime/internal/Component.ts)
  • Svelte также использует $$invalidate для обновления значения реактивной переменной.
  • Svelte сортирует реактивные объявления и операторы на основе отношения зависимости между объявлениями и операторами.
  • quadrupled зависит от doubled, поэтому вычисляется quadrupled и $$invalidated после doubled.

Поскольку все реактивные объявления и операторы сгруппированы в метод $$.update, а также тот факт, что Svelte будет сортировать объявления и операторы в соответствии с их отношениями зависимости, это не имеет значения от местоположения или порядка, в котором вы их объявили.

Следующий компонент все еще работает:

Стройный РЕПЛ

Следующее, что вы можете спросить, когда вызывается $$.update?

Помните функцию update, которая вызывается в функции flush?

Я поставил комментарий NOTE:, говоря, что это будет важно позже. Что ж, это важно сейчас.

Функция $$.update вызывается в той же микрозадаче с обновлением DOM, прямо перед вызовом $$.fragment.p() для обновления DOM.

Следствием вышеуказанного факта является

1. Выполнение всех реактивных объявлений и операторов выполняется в пакетном режиме

Точно так же, как группируются обновления DOM, реактивные объявления и операторы также группируются!

Стройный РЕПЛ

Когда update() звонят,

  1. Подобно описанному выше процессу, $$invalidate оба 'givenName' и 'familyName' и планируют обновление
  2. — Конец задачи —
  3. — Начало микрозадачи —
  4. flush() вызывает update() для каждого компонента, отмеченного как грязный
  5. Пробегов $$.update()
  • Поскольку "givenName" и "familyName" изменились, оценивает и $$invalidate 'name'.
  • Поскольку имя изменилось, выполняется console.log('name', name);

6. Вызывает $$.fragment.p(...) для обновления DOM.

Как видите, несмотря на то, что мы обновили givenName и familyName, мы оцениваем только name и выполняем console.log('name', name) один раз вместо двух:

2. Значение реактивной переменной за пределами реактивных объявлений и операторов может быть устаревшим

Поскольку реактивные объявления и операторы группируются и выполняются в следующей микрозадаче, вы не можете ожидать, что значение будет обновляться синхронно.

Стройный РЕПЛ

Вместо этого вы должны ссылаться на реактивную переменную в другом реактивном объявлении или выражении:

Сортировка реактивных объявлений и утверждений

Svelte пытается максимально сохранить порядок реактивных объявлений и операторов, поскольку они объявлены.

Однако, если одно реактивное объявление или выражение ссылается на переменную, которая была определена другим реактивным объявлением, то оно будет вставлено после последнего реактивного объявления:

Реактивная переменная, которая не является реактивной

Компилятор Svelte отслеживает все переменные, объявленные в теге <script>.

Если все переменные реактивной декларации или оператора ссылаются на, никогда не изменяются или не переназначаются, то реактивная декларация или оператор не будут добавлены в $$.update.

Например:

Стройный РЕПЛ

Поскольку count никогда не изменяется и не переназначается, Svelte оптимизирует скомпилированный вывод, не определяя $$self.$$.update.

Если вы хотите узнать больше, подпишитесь на меня в Twitter.

Выложу в Твиттере, когда будет готова следующая часть, где расскажу о логических блоках, слотах, контексте и многом другом.

⬅ ⬅ Ранее в Часть 1.

Первоначально опубликовано на https://lihautan.com