- Immer (#immer)
Shallow copy - поверхностное копирование, т.е. пересоздание с нуля только первого уровня объекта (вложенные объекты буду унаследованы по ссылке)
Structural sharing -
- возвращает независимую копию данных
- если в части исходной структуры данные НЕ поменялись, то их же и возвращаем (копируем по ссылке)
- если в части исходной структуры данные поменялись, то эти части пересоздаем с нуля с новыми значениями (пересоздаем через shallow copy)
- если в части исходной структуры данные поменялись ГЛУБОКО, то весь путь от этого глубокого изменения до корня на каждом уровне должен быть пересоздан через shallow copy (.../Object.assign)
В примере ниже мы хотим поменять m на 321. Это изменение вызовет shallow copy объекта под ключом k. Тогда для объекта y свойство k поменялось, а значит весь объект под ключом y тоже надо прогнать через shallow copy. Тоже самое для объекта x с измененным ключом y. И таким формируется путь изменений. y2 затронут при этом не будет, скопируется по ссылке
const x = { y: { k: { m: 123 } }, y2: ... }
Structural cloning - синоним deep copy. С недавних пор официальное api браузера structuredClone (клонирует многие вещи, но не все, не клонирует методы). Не путать с Structural sharing
Reflect
- встроенный объект js, содержащий набор методов-обёрток вокруг внутренних методов (стандартного ECMASCRIPT) объекта по спецификации, т.е. ([[Get]], [[Set]], [[Delete]], [[Construct]], ...)
- стандартный объект ECMASCRIPT - объект, поведение которого жёстко определено стандартными алгоритмами спецификации
- [[Get]], [[Set]], [[Delete]], [[Construct]] - не просто слова в квадратных скобках, это название стандартных алгоритмов спецификации, которые выполняют определенную последовательность шагов при взаимодействии с объектом (например, удаление, получение свойств и т.д.)
- объекты, у которых поведение не совпадает с этими стандартными алгоритмами, называются экзотическими. Например, Array, потому что его поведение установки нового свойства затрагивает перерасчет специфического только ему свойства length
Proxy
- встроенный в js декоратор над объектом
- в параметрах принимает исходный объект и объект с настройками декоратора (ловушки/trap)
- Настройки декоратора также перехватывают алгоритмы (стандартного ECMASCRIPT) поведения объекта. Мы, программисты, благодаря этому перехвату можем изменить поведение target (исходного объекта)
- из-за того что proxy действует не только в мире js, но и за его пределами (обращаясь к внутренним свойствами вроде [[Get]] и т.д.), то на чужой территории програмимсту надо вести себя в соответствии с чужими законами, в данном случае согласно спецификации ECMASCRIPT (например, перенаправление корректного this => proxy намертво связан с исходным объектом и это может приводить к проблемам при наследовании в прототипной цепи; возвращение булевого флага при set; правильной обработки ошибок; ...)
- Программист самостоятельно обрабатывает все вышеуказзаные случаи, либо полагается на Reflect, который берёт обработку на себя. Важно помнить о Reflect, который поможет программисту. Даже Reflect не может закрыть все corner cases, как например, внутренние слоты (например [[MapData]] это аналог внтуренних свойств объекта вроде [[Get]], ...) Map, Set, приватные поля классов и тогда программисту все же приходится самостоятельно обрабатывать эти случаи
- Revoke Proxy возможность разорвать связь между исходным объектом и proxy
let {proxy, revoke} = Proxy.revocable(target, handler)
Immer - библиотека, которая позволяет писать код в mutable стиле. Наружу отдаем immutable копию данных, используя под капотом structural sharing
Термины библиотек и Immer:
- Produce внешнее api, которое принимает 2 параметра: base и recipe, возвращает новую immutable копию данных
- Recipe колбек который имеет параметр draft и меняет его в мутирующем стиле
- Base исходные пользовательские данные (не обязательно объект, м.б. и примитивы, maps, ...)
- Copy => shallow copy base
- State объект, содержащий base + copy + метаинформация
- Draft это прокси над state
История развития immer
-
Рассмотрим react. Так react вызывает перерендер через сопоставление ссылок, а не вложенной не структуры, то важно на каждое изменение возвращать новую копию данных даже если оно произошло очень глубоко (принцип иммутабельности). Immer идеально ложится на данную философию
-
шаг 1 решения проблемы. Вместо работы с исходными данными ВСЕГДА создаём копию. DeepClone копия затратно по ресурсам, поэтому используем shallow copy
-
шаг 2 решения проблемы. Shallow copy имеет недостатки в виде НЕ обработки вложенных структуру данных и возврата каждый раз новой копии, даже если данные не поменялись
-
шаг 3 решения проблемы. Содание обёртку, именуемую state. Исходные данные - base, shallowCopy от base - copy. Обёртка также содержит любую доп. информацию для коректной работы state в виде meta. Недостаток третьего шага - пользователь вместо изначальной data работает со сложным api в виде state, лишний головняк
-
шаг 4 решения проблемы. Прячем state за proxy. Снаружи человек обращается будто бы к data, а внутри всё реализуется через proxy, используя structural sharing
-
шаг 5 решения проблемы. Любые вложенные объекты обрабатываются рекурсивно создавая для КАЖДОГО из них свой proxy над своим state. Чтобы вернуть пользователю не систему state, а нормальный имутабельный объект, необходимо пробежаться по всем этим state и собрать новый имутабельный объект
-
в процессе работы может образоваться микс base, copy, state. Чтобы избежать ситуации, когда снаружи у нас обычный base, а внутри изменен глубоко вложенный state, нам необходимо всю цепочку объектов, включая внешний base пересоздать (это философия structural sharing). Для этого флаг загрязнение глубоковложенного state распространяется на весь путь до верхнего base
-
produce внутри себя выполняет следующие действия:
- scope = getCurrentScope() // singleton на весь вызов одного produce
- draft = createProxy(base)
- result = recipe(draft)
- processResult(result, scope) // нормализация финального объекта для пользователя
-
scope содержит:
- drafts_ (собрание всех draft данного produce)
- immer_ (экземпляр immer от имени которого вызван produce, часто используется вместо this во внутренней логике)
- parent_ (родительский scope, produce можно вызывать внутри produce)
- canAutoFreeze (все возвращаемые копии объекта можно замораживать)
- unFinalizedDraft (количество незавершенных drafts. Завершенный draft - м.б. это draft который в функции processResult был обработан)
-
state содержит:
- base
- copy
- proxy
- revoke
- parent (родительский proxy, не путать с родителььским scope)
- scope
- isModified
- isFinalized
-
latest служебная функция, которая возвращет либо copy, либо base из state. Признак - наличие свойства base в state
-
peek(obj, prop) выдает актуальное значение по переданному ключу:
- если obj === draft, то latest(draft)[prop]
- если не draft, то obj[prop]
-
get ловушка:
- принимает 2 основных параметра: state и property (значение которого хотим получить)
- вызываем latest(state)[prop] и получаем value
- анализируем value на примитив. Если да, то return
- если value не примитив и не proxy if(value === peek(state.base_, prop)), то рекурсивно создаем proxy и записываем его в state.copy[prop]
- если value уже proxy, то вернуть его и ничего не делать
-
set
- принимается 3 основных параметра: state, property, value
- если пришедшее value равно уже имеющемуся, то запись не производить
- если пришедшее value НЕ равно уже имеющемуся и флаг isModified => true, то производим запись в copy
- если пришедшее value НЕ равно уже имеющемуся и флаг isModified => false, то производим запись в copy и загрязняем весь путь до корня
- никаких proxy не создается, они создаются в get
- если в set мы передаем obj в качестве value И под таким property уже лежит proxy над данным obj, то immer выкидывает proxy и устанавливает obj (corner case)
-
processResult/finalize - функции, которые нормализуют проксированные данные перед выдачей наружу
-
По умолчанию результаты работы immer (produce) фризятся, результаты вложенных produce не фризятся. Имеются возможности настройки того поведения (setAutoFreeze)
-
Object freeze/Object seal. freeze - более жесткий вариант, не позволяет модификацию свойства, а seal разрешает модификацию. Оба запрещают добавление/удаление
-
producer - преднастроенный produce (обертка с зашитым recipe). Упор на recipe
-
Каррированный produce (producer) м.б. использован в качестве callback для setState, что является основой хука useImmer
-
useImmer - хук, который объединяет в себе useState и Immer
-
при работе с custom classes Immer создает новый this, подтягивает старый prototype (вручную)
-
для exotic objects, Data, ... сложные случаи, Immer говорит не надеяться на него, а использовать return/не использовать Immer вообще
-
как искусственно зафризить/freeze Map - вручную перебить его методы (wrapper/monkey patching)
-
как поведет себя immer при ошибке в recipe (ожидаем undefined и ошибку в консоли) try/finally
-
original - ссылка на draft.base
-
current - слепок состояния внутри produce на момент вызова функции, новая копия
-
patches - идея того, как записывать изменения в виде строкового набора команд, которые надо исполнить, используется pattern command
-
в immer patch имеет вид:
{
op: "replace",
path: ["prop1", "prop2", ...],
value: {name: "John"}
}
-
pattern command - идея разобщения исполнителя и заказчика, путем введения третьей сущности. Третья сущность имеет связб как с исполнителем, так и с заказчиком. От исполнителя принимает текстовое описание того, что надо сделать, анализирует его (switch case), вызывает метод исполнителя. Минусы: усложнение кода. Плюсы: возможность сохранить команды и откатиться, отложить их выполнение, комбинировать
-
получить patches можно либо в третьем параметре produce, это колбек функция, где мы имеем доступ к патчам (программист определяет логику работы с ними). Либо функция produceWithPatches возвращает нам вторым параметром patches
-
freeze - все возвращенные из produce объекты авто фризятся. Это настраивается
-
если объект сильно вложенный и сложный, и в процессе Immer его надо авто фризить, то можно это сделать самостоятельно до и часть логики будет пропущена, что облегчит работу
-
если хотите реально вернуть из produce undefined, то воспользуйтесь return nothing. Т.к. любой возврат undefined в ином виде будет воспринят, как возврат draft
-
не миксуйте подходы в return от produce. Либо мутирующий стиль полагаясь на Immer, либо перехватывайте управление
-
createDraft,finishDraft - способы создать прокси без produce, для создания api над immer
-
immer старается вывести типы сам. многое крутится вокруг readonly
-
совпадение типов не равно сопадению данных. Мы должны гарантировать совпадение типов по baseState и draft
-
processResult - философия в том, чтобы нормализовать данные (избавиться от системы proxy, вернуть наружу просто данные), начиная с корня, учитывая ручное или автоматическое управление: отличия в patches (в автоматике pacthes накапливаются, в ручном режиме генерируется только один replacement...), запрещен микс автоматики и ручного управления (если immer не будет знать, по какой ветке работать, то он бросит ошибку)
-
finalize - философия в том, чтобы детализировать processResult, т.е. пройти по объекту и у каждого свойства вызвать finalizeProperty. finalize должен что-то вернуть:
- если primitive (через проверку izFrozen), то вернуть примитив;
- если base без изменений, вернуть base;
- если draft (produce внутри produce), вернуть draft;
- если обычный объект или copy, то вернуть данные прогнанные через finalizeProperty
-
finalizeProperty - философия в том, чтобы рекурсивно получать дочерние данные и записать их под ключом (set рекурсивного finalize). Также морозит нормализованное значение, накапливает патчи
RTK (Redux tool kit)
- deriving - выведение данных на основе других данных. Не надо заводить отдельный state для deriving data
- cache/memoization - cache (получение данных не из источника, например, copy); memoization (частный случай cache для ускорения получения результата, для предотвращения повторного запуска функции)
- selector - функция, которая дерайвит данные
- selector лучше иметь ближе к источнику данных (source of truth), иначе во всех местах, где этот селектор используется, его придется исправлять
- selectors можно располагать там, где есть source of truth (middleware, sage, reduce, component, ...)
Философия RTK, HOS (selectors высшего порядка)
-
redux в своей логике обновления state использует structural sharing (НЕ deepClone)
-
high-order functions пришло из ункционального декларативного программирования. Там функции представляют собой не процесс вычисления, сопоставление входынх и выходных данных (как на бумаге). Таких понятий, как замыкание, создание вложенных функций там нет. Поэтому единственным 2 способами использовать функцию в функции являются либо передача в параметрах, либо возвращение в качестве результата. Однако философия императивного js иная, с этой точки зрения можно взять функцию из замыкания, что также будет high-order function
-
Бывают selectors первого порядка, работают с источником данных. Selectors высшего порядка, которые работают с результатами selectors низшего порядка. Можно ли назвать selectors высшего порядка high-order selectors, такого определения нет нигде, но это удобно???
-
createSelector принимает параметры: последний является output selector, все до него input selectors.Selectors низшего порядка являются input для High-order-selectors/HOS (selectors высшего порядка). output callback являющийся частью HOS (selectors высшего порядка) рассчитывает результаты на основе результатов input selectors
-
input selector не должен всегда возвращать новый результат (в случае redux весь state), иначе теряется смысл оптимизириующей логики HOS (selectors высшего порядка), в частном случае мемоизации
-
output selector не имеет смысла, если он не производит трансформирующую логику. Легче сразу забрать данные от низшего selector без создания HOS (selectors высшего порядка), т.е. без использования re-select
-
философски redux and СУБД схожи в сути нормализации данных, селектировании, ..., но сильно отличаются технически
-
HOS (selectors высшего порядка) м.б. input selector для другого selector
-
HOS (selectors высшего порядка) кеширует данные для одного потребителя. Если один HOS (selectors высшего порядка) используется 2 и более потребителями, то они могут перезаписывать cache и смысли мемоизации текущего HOS (selectors высшего порядка) теряется
-
Вариант решения проблемы:
- фабрика selectors (пользовательская функция, задача которой каждый раз возвращать новый selector)
- вручную создавать HOS (selectors высшего порядка) для каждого потребителя
- использовать re-reselect (кеширование по дополнительному и редко меняющемуся атрибуту. Среди параметров скидываемых для HOS и этот параметр или их комбинация будет ключом cache)
- не смотря на все вышеописанные свободы, связи вида один HOS на один потребитель не избежать
-
СХЕМА: мемоизация использует кеширование, кеширование использует сравнение (equalityCheck). Данная схема универсальна
-
memoization спасает при тяжелых вычислениях
-
HOS спасает при ссылочных типах данных (в случае возврата каждый раз новой ссылки селектором нижнего порядка, calculation над этими ссылками м. возвращать постоянное значени, что предотвратит react от перерендера)
-
если логика программиста НЕ подразумевает calculation, а только селектирование, то смысла в HOS нет
-
не нужно создавать селектора первого уронвя на каждое свойство state, это ухудшает читаемость/поддерживаемость кода
-
каскадная мемоизация (cascade memoization) - мемоизация вложенных уровней. Возникает, когда есть вложенная цепочка функций, которые зависят друг от друга. Каждый уровень вложенности мы можем замемоизировать
-
локальные и глобальные селекторы - локальные селекторы получают кусок store, глобальные - весь store
-
В официальной доке reselect, redux и наших определениях есть расхождения. Синхронизация терминов:
- HOS / Memoized selector / Output Selector (исп. на сайте reselect)
- Selector низшего порядка / Dependencies / Input Selector
- Result Func / Output Selector (исп. на сайте redux)
-
argsMemoize (функция мемоизации работы inputs selectors)
-
memoize (функция мемоизации работы resultFunc)
-
LRU (least recently used) распределение задач в структуре данных в зависимости от того времени добавления. Есть линиия времени, есть линия списка. Самая свежие данные встают в конец списка, а самые старые данные остаются вначале списка. Если размер списка превышен при добавлении новых данных свежие данные вставляются в конец, а самые старые выкидываются
-
LRU м. релизовываться разными техническими прёмами (array, list, map, ...). Самый легкий способ - Map, т.к. он сохарняет порядок вставки сразу же
-
важно понимать и уметь написать функцию memoize. Реализация:
function checkForEquality(arg1, arg2) {
return JSON.stringify(arg1) === JSON.stringify(arg2);
}
function memoize(targetFunc) {
let memoArgs = [];
let memoResult = null;
function decoratedFunc(...args) {
if (checkForEquality(args, memoArgs)) {
return memoResult;
} else {
memoArgs = args;
memoResult = targetFunc(...memoArgs);
return memoResult;
}
}
return decoratedFunc;
}
-
функция memoize - это decorator. targetFunc - исходная функция для мемоизации, значит что если ее аргументы не изменились, то запускать снова targetFunc не надо. decoratedFunc - результат memoize, ее логика состоит в том, чтобы сохранять аргументы вызова и проверять, не изменились ли они. checkForEquality - вспомогательная функция для проверки изменения аргументов в decoratedFunc
-
HOS - богатый, в static свойствах и методах лежит много информации (включая информацию связанную с мемоизацией из пердыдущего пункта (наш термин/термин reselect): memoize/memoize, resultFunc/targetFunc, memoResult/lastResult...)
-
Кеширование и мемоизация НЕ синонимы. Мемоизирутся функции, кешируются данные. Мемоизация функции означает, что мы пропускаем лишний раз ее вызов за счет того, что ее аргументы не изменились. А аргументы не изменились, если они совпали с ранее закешированными. Количество разов кеширований можно увеличить с помощью LRU
-
LRUmemoize theory - обычно HOS при каскадной мемоизации для каждого из двух уровней поддерживает размер cache 1. Можно создать HOS, cache которого для каждого уровня мемоизации обслуживается LRU (увеличить размер cache). Это осуществляется с помощью третьего опционального объекта настроек HOS, где можно настроить как мемоизацию первого уровня (argMemoize), так и мемоизацию второго уровня (memoize). Каждая из этих мемоизаций может настраиваться отдельно с помощью своего объекта настроек:
{
memoizeOptions: {
equalityCheck: shallowEqual, // сравнение аргументов
resultEqualityCheck: shallowEqual, // сравнение результатов вызова функции
maxSize: 10
}
}
-
shallowEqual - сначала проверка объектов по ссылке, затем распаковка и поверхностное сравнение содержимого первого уровня. Рекурсивно вглубдь сравнение НЕ производится
-
withtypes - метод-декоратор, который подхватывает один из параметров дженерика. У HOS это state
-
WeakMap - структура данных, где ключами являются только объекты. Пока на ключ существует любая другая ссылка кроме данного WeakMap, он существует. Как только остается только ссылка WeakMap, то garbage collector удалляет ключ. У WeakMap меньшее число методов по сравнению с Map,т.к. мы не знаем когда отработает garbage collector
-
Существуют 2 режима работы garbage Collector: малый Scavange и большой. Малый запускается каждые несколько милисекунд. Большой запускается раз в несколько минут/часов
-
weakRef - специальный объект со скрытой слабой ссылкой. Слабая ссылка - НЕ сам объект weakRef, а указатель, который не препятствует удалению объекта сборщиком мусора
-
weakRef живет по расписанию большого режима работы garbage Collector
-
FinalizationRegistry - api, которое позволяет привязать привязать к удалению целевого объекта колбэк очистки (финализатор)
-
FinalizationRegistry.register принимает 3 параметра: target (отлеживаемый объект), heldValue (любое значение, пробрасываемое в колбек очистки), unregisterToken (токен для идентификации в случае отмены подписки, используется для unregister)
-
FinalizationRegistry.unregister принимает unregisterToken
-
Пример FinalizationRegistry + weakRef. Ссылка на главу
function fetchImg() {
// абстрактная функция для загрузки изображений...
}
function weakRefCache(fetchImg) {
const imgCache = new Map();
const registry = new FinalizationRegistry((imgName) => { // (1)
const cachedImg = imgCache.get(imgName);
if (cachedImg && !cachedImg.deref()) imgCache.delete(imgName);
});
return (imgName) => {
const cachedImg = imgCache.get(imgName);
if (cachedImg?.deref()) {
return cachedImg?.deref();
}
const newImg = fetchImg(imgName);
imgCache.set(imgName, new WeakRef(newImg));
registry.register(newImg, imgName); // (2)
return newImg;
};
}
const getCachedImg = weakRefCache(fetchImg);
-
отсутствие возвращаемого значения в функции лучше всего кодировать уникальным Symbol, чтобы его не спутать с user undefined, null, false, ...
-
идея weakMapMemoize - где массив аргументов мемоизированной функции мы переводим в связный список. Связный список имеет особую структуру (каждый элемент списка представляет собой кешированную ноду/cacheNode, где корень - кешированная нода самой мемоинзированной функции). В самой ноде мы храним аргумент и ссылку на следующую кешированную ноду следующего аргумента
-
результат (cacheNode.v/value) есть только у cacheNode последнего аргумента.
-
если argument примитив, то внутри cacheNode сохраним его в Map, если объект, то в WeakMap. Из-за того, что ссылки на другие cacheNode хранятся в структуре Map/WeakMap, у нас получается не односвязный список, а разветвленное дерево cacheNodes
-
так как новый аргумент сравнивается с ключом в Map (через get). Механизм сравнения управляется js через strictEquality, поэтому в отличие от LRUMemoize нет возможности настроить сравнение входящих аргументов
-
Как понимать фразу "функция от чего-то зависит"? Результат вычисления этой функции определяется на основе каких-то данных, используемой этой функцией: аргументы, данные в теле функции, данные из замыкания, встроенный this, ...
-
НЕ путать аргументы с зависимостями. Зависимости - ВСЕ данные от которых зависит резулььтат вычисления функции. Аргументы - частный случай зависимостей, который тоже влияет на результат вычисления функции
-
Реактивность - идея того, что при изменении нижележащих данных поменяются и вышележащие данные. Highorder Data depends on lowOrder Data??? (наше определение, такой терминологии нет).
-
push стратегия - изменение нижележащих данных обновит вышележащие данных (rxjs)
-
pull стратегия - изменение нижележащих данных НЕ будет обновлять вышележащие данны, пока кто-то не воспользуется этими вышележащим данными. Но сам факт изменения нижележащих данных будет обязателььно зафиксирован (autotracking)
-
Асинхронность - НЕ путать с реактивностью, НЕ про данные, а ПРО действия
-
autrotracking = reactivity + memoization. МЫ привыкли, что мемоизация - функция, аргументы, результат. Но если аргументы расширить до зависимостей, то получим autrotracking. Мемоизация, которая отслеживает изменение зависимостей и в результате этого пересчитывает cache, дает autrotracking
-
Зависимости в виде простых данных (чисел, объектов), их изменений невозможно отследить. Для этого эти простые данные оборачиваются в Cell/Storage, которые имеют перехватчики get/set. Технически перехватчики могут быть выражены разными средствами, например, Proxy, Class getter/setter, дескрипторы
-
get - позволяет создать связь между выполняемой функцией и зависимостями встречанными внутри этой функции
-
set - либо констатирует факт изменения данных (pull: autrotracking), либо сразу активирует измененные данные (push: rxjs)
-
patches - расширение структуры проекта изнутри
-
plugin - расширение структуры системы согласно подготовленному интерфейсу системы. НЕ просто пропатчить исходники, а именно в разрешенных точках
-
hook - расширение поведения системы в определенных точках
-
Objectish - Map/Set/Array/Object
-
forEach для map/set
-
Symbol.for - глобальные символы
-
Сервис (service) - набор взаимосвязанного кода (чаще всего выраженного через модули), решающий общую задачу (единый уровень абстракции), предоставляющий интерфейс для этого взаимодействия
Signals
- Dependency - ссылка между двумя сущностями
- Consumer - ячейка с формулой (потребляет данные). Может выступать как producer для другог consumer
- Producer - ячейка с источником данных (продуцирует данные)
- Связь между consumer и producer образует ребро, а вместе эти понятия образует граф
- pull стратегия для данных (во время чтения consumer), push - для уведомления (во время изменения producer, watcher уведомляет, что что-то изменилось)
- consumer имеет в dependency producers, а producer имеет в dependency consumers. Двойная ссылка
REACTIVE_NODE базовый уровень графа
/ | \
SIGNAL_NODE COMPUTED_NODE расширенный уровень графа
| | |
createSignal createComputed базовый JS уровень
| | |
class State class Computed Class Watcher расширенный JS уровень
Настоящий философский сигнал - это JS функция, вычисляющая значение (return from createSignal || return from createComputed).
Настоящий сигнал не зависит от типа (State || Computed || Watcher). Наоборот, State || Computed || Watcher - это api обертки над настоящими сигналами, которые скрывают их машинерию, и позволяют удобно пользоваться ими (создавать, читать, записывать...)
Чтобы вычисляемое значение не потерялось, и можно было проводить с ним разные манипуляции, функция сигнала хранит связь с служебным объектом (не от классов) сигнала, в котором и содержится и значение, и алгоритмы и метаинформация по обслуживанию механизма работы сигнала. В полифиле эта связь выражена символьным статическим свойством у функции сигнала.
функция, рожденная от createSignal - getter
getter[SIGNAL] = node, где node = Object.create(SIGNAL_NODE)
функция, рожденная от createComputed - computed
computed[SIGNAL] = node, где node = Object.create(COMPUTED_NODE)
То есть когда мы запрашиваем на базовом JS уровне значение сигнала, мы запускаем функцию, которая не сразу напрямую возвращает значение, а сначала всегда производится логика получения \ вычисления значения сигнала, а только потом его возвращение.
Watcher, хоть и является сигналом, он не хранит в себе значение, и не занимается логикой его получения \ вычисления. Поэтому его логика опирается сразу на REACTIVE_NODE базового уровня графа.
На расширенном JS уровне инстансы классов - отдельные объекты, не равные node-s других уровней. То есть получается такая ФИЛОСОФСКАЯ (не всегда выраженна также технически!) цепочка объектов: -> объект class State \ Computed \ Watcher -> объект служебной node связанная с getter/computed -> объект SIGNAL_NODE / COMPUTED_NODE -> объект REACTIVE_NODE
Каждый из этих объектов содержит специфический свойства и методы, свойственному текущему уровню абстракции. Связываться объекты соседних уровней абстракций могу между собой по разному, например, через тупое
копирование свойств, или через создание прототипной связи, или через композиционную ссылку.
Private
- private - набор данных с ограниченным доступом (в философском понимании)
- private:
- может лежать во всех объектах js под специальным скрытым полем
[[PrivateElements]]
- может лежать в privateEnvironment
- может лежать во всех объектах js под специальным скрытым полем
- private Environment - дополнительное новое лексическое окружение как часть execution Context
- execution context расширился, следовательно privateEnvironment будет присутствовать при выполнении любого кода, хоть Class, хоть function. Но заполняться данными privateEnvironment будет только в Class, в остальных случаях он равен null
- всё внутреннее содержимое класса, в частности функции и static blocks будут замыкаться на private environment Class
- static blocks выполняют свой код в момент evaluating of declaration/парсинга тела класса в порядке их объявления
- стоит помнить, что создается 2 корзинки: обычный lexicalEnvironment & privateEnvironment. У внутреннего содержимого класса замыкание есть по lexicalEnvironment, так и отдельное замыкание по privateEnvironment
- внесение данных в privateEnvironment просиходит в момент evaluating of declaration/парсинга тела класса (НЕ создания экземпляра класса)
- НЕ путать 2 разных понятия: privateIdentifier & privateName
- privateIdentifier - то строковое описание, которым мы пользуемся (#age)
- privateName - связанный с privateIdentifier уникальный ключ (по аналогии с Symbol)
- приватное свойство #age в одном классе НЕ будет равно #age в другом классе, потому что у них разных privateName из-за уникального ключа
- оператор in для private работает по-особому:
- проверяет наличие privateName в privateEnvironment
- если true, то идет проверка наличия приватного имени в самом объект под специальным скрытым полем
[[PrivateElements]]
- true только, если пройдут обе проверки. Этот логикой называется специалььным тремином brandChecking. Это не термин спецификации, а термин MDN