Skip to content

bullet03/computer_science

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 

Repository files navigation

Computer Science

СОДЕРЖАНИЕ

  • 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

  • immer_history

  • 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). Также морозит нормализованное значение, накапливает патчи


Signals

  • Dependency - ссылка между двумя сущностями
  • Consumer - ячейка с формулой (потребляет данные). Может выступать как producer для другог consumer
  • Producer - ячейка с источником данных (продуцирует данные)
  • Связь между consumer и producer образует ребро, а вместе эти понятия образует граф
  • pull стратегия для данных (во время чтения consumer), push - для уведомления (во время изменения producer, watcher уведомляет, что что-то изменилось)
  • consumer имеет в dependency producers, а producer имеет в dependency consumers. Двойная ссылка

Уровни абстракций сигналов, увиденных в "signal-polyfill"

              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
  • 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

Модель памяти (memory model) Описывает связь имён с данными

  • идентификатор - именование переменной
  • значение - данные
  • указатель - адрес ячейки памяти
  • ячейка памяти может содержать либо значение либо указатель Копирование массива по ссылке - копирование указателя, по которому лежат значения
  • copy arr by reference

About

Basic computer knowledge notes, mostly JavaScript

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •