Поговорим о библиотекеObserver-Util, чтобы вы поняли принцип отзывчивости

внешний интерфейс реактивное программирование
Поговорим о библиотекеObserver-Util, чтобы вы поняли принцип отзывчивости

Отзывчивость должна быть знакома тем, кто использовал Vue или RxJS. Отзывчивость также является одной из основных особенностей Vue, поэтому, если мы хотим освоить Vue, мы должны иметь глубокое понимание отзывчивости. Далее Брат Абао начнет с режима наблюдателя, а затем совместитobserver-utilЭта библиотека поможет вам глубже изучить принципы отзывчивости.

Режим наблюдателя

Шаблон наблюдателя, который определяетодин ко многимОтношения позволяют нескольким объектам-наблюдателям одновременно отслеживать определенный объект-субъект.При изменении состояния объекта-субъекта все объекты-наблюдатели будут уведомлены, чтобы они могли автоматически обновлять себя. В шаблоне Observer есть две основные роли: Subject и Observer.

Следуйте «Дороге полного совершенствования», чтобы прочитать 4 бесплатные электронные книги (в общей сложности более 22 000 загрузок) и 50 учебных пособий «Повторное изучение TS» от брата Абао.

Поскольку шаблон наблюдателя поддерживает простую широковещательную связь, все наблюдатели автоматически уведомляются об обновлении сообщения. Давайте посмотрим, как использовать TypeScript для реализации шаблона наблюдателя:

1.1 Определение ConcreteObserver

interface Observer {
  notify: Function;
}

class ConcreteObserver implements Observer{
  constructor(private name: string) {}
  notify() {
    console.log(`${this.name} has been notified.`);
  }
}

1.2 Определение класса Subject

class Subject { 
  private observers: Observer[] = [];

  public addObserver(observer: Observer): void {
    this.observers.push(observer);
  }

  public notifyObservers(): void {
    console.log("notify all the observers");
    this.observers.forEach(observer => observer.notify());
  }
}

1.3 Пример использования

// ① 创建主题对象
const subject: Subject = new Subject();

// ② 添加观察者
const observerA = new ConcreteObserver("ObserverA");
const observerC = new ConcreteObserver("ObserverC");
subject.addObserver(observerA); 
subject.addObserver(observerC);

// ③ 通知所有观察者
subject.notifyObservers();

В приведенном выше примере есть в основном три шага: ① создать предметный объект, ② добавить наблюдателя и ③ уведомить наблюдателя. После успешного выполнения приведенного выше кода консоль выведет следующие результаты:

notify all the observers
ObserverA has been notified.
ObserverC has been notified.

В большинстве интерфейсных сценариев целью, которую мы наблюдаем, являются данные. Когда данные изменяются, страница может автоматически обновляться. Соответствующий эффект показан на следующем рисунке:

Чтобы добиться автоматического обновления, нам нужно выполнить два условия: одно — иметь возможность получать точные обновления, а другое — обнаруживать изменения в данных. Для получения точных обновлений необходимо собрать функции обновления (наблюдатели), которые заинтересованы в изменении данных.После завершения сбора данных при обнаружении изменения данных можно уведомить соответствующую функцию обновления.

Приведенное выше описание кажется довольно запутанным, но на самом деле, чтобы добиться автоматического обновления, нам достаточно сделать① Создать предметный объект, ② Добавить наблюдателя, ③ Уведомить наблюдателяЭти три шага автоматизированы, что является основной идеей реализации адаптивности. Далее, давайте возьмем конкретный пример:

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

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

Vue3 используетProxyAPI для достижения отзывчивости,ProxyAPI по сравнению сObject.definePropertyКаковы преимущества API? Брат А Бао не будет представлять его здесь, я планирую написать специальную статью, чтобы представить его позже.ProxyAPI. Далее брат А Бао начнет представлять главного героя этой статьи——observer-util:

Прозрачная реакция со 100% языковым охватом Сделано с ❤️ и прокси ES6.

GitHub.com/the-is/obs и…

Библиотека также использует прокси-API ES6 для реализации отзывчивости.Прежде чем представить его принцип работы, давайте посмотрим, как его использовать.

2. Введение в Observer-Util

observer-utilЭта библиотека также очень проста в использовании.observableа такжеobserveфункция, мы можем легко реализовать отзывчивость данных. Начнем с простого примера:

2.1 Известные свойства

import { observable, observe } from '@nx-js/observer-util';

const counter = observable({ num: 0 });
const countLogger = observe(() => console.log(counter.num)); // 输出 0

counter.num++; // 输出 1

В приведенном выше коде мы начинаем с@nx-js/observer-utilимпортировать в модулиobservableа такжеobserveфункция. вobservableфункции используются для создания наблюдаемых объектов, аobserveФункция используется для регистрации функций наблюдателя. После того, как приведенный выше код будет успешно выполнен, консоль по очереди выведет0а также1. Помимо известных свойств,observer-utilТакже поддерживаются динамические свойства.

2.2 Динамические свойства

import { observable, observe } from '@nx-js/observer-util';

const profile = observable();
observe(() => console.log(profile.name));

profile.name = 'abao'; // 输出 'abao'

После того, как приведенный выше код будет успешно выполнен, консоль по очереди выведетundefinedа такжеabao.observer-utilПомимо поддержки обычных объектов, он также поддерживает массивы и коллекции в ES6, такие как Map, Set и т. д. Здесь мы берем широко используемый массив в качестве примера, чтобы увидеть, как превратить объект массива в реагирующий объект.

2.3 Массивы

import { observable, observe } from '@nx-js/observer-util';

const users = observable([]);

observe(() => console.log(users.join(', ')));

users.push('abao'); // 输出 'abao'

users.push('kakuqo'); // 输出 'abao, kakuqo'

users.pop(); // 输出 'abao,'

Здесь брат Абао приводит лишь несколько простых примеров.observer-utilДругие небольшие партнеры, которые заинтересованы в использовании примеров, могут прочитатьREADME.mdдокументация. Далее брат А. Бао возьмет простейший пример для анализа.observer-utilПринцип реализации этой библиотеки реактивный.

Если вы хотите запустить приведенный выше пример локально, вы можете сначала изменить его.debug/index.jsв каталогеindex.jsфайл, а затем выполнить его в корневом каталогеnpm run debugЗаказ.

3. Анализ принципа наблюдателя-утилита

Во-первых, давайте вернемся к самому раннему примеру:

import { observable, observe } from '@nx-js/observer-util';

const counter = observable({ num: 0 }); // A
const countLogger = observe(() => console.log(counter.num)); // B

counter.num++; // C

В строке А мы проходимobservableФункция создает наблюдаемуюcounterобъект, внутренняя структура объекта выглядит следующим образом:

Наблюдая за приведенным выше рисунком, мы видим, что переменная counter указывает на объект Proxy, который содержит 3 внутренних слота. Такobservableкак функция преобразует наш{ num: 0 }объект, преобразованный вProxyЧто с объектом? в проектеsrc/observable.jsВ файле находим определение этой функции:

// src/observable.js
export function observable (obj = {}) {
  // 如果obj已经是一个observable对象或者不应该被包装,则直接返回它
  if (proxyToRaw.has(obj) || !builtIns.shouldInstrument(obj)) {
    return obj
  }

  // 如果obj已经有一个对应的observable对象,则将其返回。否则创建一个新的observable对象
  return rawToProxy.get(obj) || createObservable(obj)
}

появляется в приведенном выше кодеproxyToRawа такжеrawToProxyдва объекта, которые определены вsrc/internals.jsВ файле:

// src/internals.js
export const proxyToRaw = new WeakMap()
export const rawToProxy = new WeakMap()

Эти два объекта хранятся отдельноproxy => rawа такжеraw => proxyКартографические отношения междуrawпредставляет исходный объект,proxyозначает упакованныйProxyобъект. Очевидно, что при первом выполненииproxyToRaw.has(obj)а такжеrawToProxy.get(obj)вернется соответственноfalseа такжеundefined, поэтому выполнит||Логика в правой части оператора.

Давайте проанализируемshouldInstrumentфункция, которая определяется следующим образом:

// src/builtIns/index.js
export function shouldInstrument ({ constructor }) {
  const isBuiltIn =
    typeof constructor === 'function' &&
    constructor.name in globalObj &&
    globalObj[constructor.name] === constructor
  return !isBuiltIn || handlers.has(constructor)
}

существуетshouldInstrumentВнутри функции конструктор параметра obj будет использоваться для определения того, является ли он встроенным объектом.{ num: 0 }объект, его конструкторƒ Object() { [native code] },следовательноisBuiltInЗначение истинно, поэтому выполнение продолжится.||Логика в правой части оператора. вhandlersОбъект является объектом карты:

// src/builtIns/index.js
const handlers = new Map([
  [Map, collectionHandlers],
  [Set, collectionHandlers],
  [WeakMap, collectionHandlers],
  [WeakSet, collectionHandlers],
  [Object, false],
  [Array, false],
  [Int8Array, false],
  [Uint8Array, false],
  // 省略部分代码
  [Float64Array, false]
])

прочитай этоhandlersструктура, это очевидно!builtIns.shouldInstrument(obj)Результат выраженияfalse. Итак, далее наше вниманиеcreateObservableфункция:

function createObservable (obj) {
  const handlers = builtIns.getHandlers(obj) || baseHandlers
  const observable = new Proxy(obj, handlers)
  // 保存raw => proxy,proxy => raw 之间的映射关系
  rawToProxy.set(obj, observable)
  proxyToRaw.set(observable, obj)
  storeObservable(obj)
  return observable
}

Наблюдая за приведенным выше кодом, мы знаем, почему вызовobservable({ num: 0 })После функции возвращается объект Proxy. Для конструктора Proxy он поддерживает два параметра:

const p = new Proxy(target, handler)
  • цель: использоватьProxyобернутый целевой объект (может быть любым объектом, включая собственные массивы, функции или даже другой прокси);
  • обработчик: объект, который обычно имеет функции в качестве атрибутов.Функции в каждом атрибуте определяют прокси при выполнении различных операций.pповедение.

Цель в примере указывает на{ num: 0 }объект иhandlersбудет возвращать разные значения в зависимости от типа объектаhandlers:

// src/builtIns/index.js
export function getHandlers (obj) {
  return handlers.get(obj.constructor) // [Object, false],
}

а такжеbaseHandlers— это объект, который содержит «подводные камни», такие как get, has и set:

export default { get, has, ownKeys, set, deleteProperty }

После созданияobservableПосле объекта он сохранитнеобработанный => прокси, прокси => необработанныйОтношение сопоставления между, а затем вызовstoreObservableФункция выполняет операцию хранения, функция storeObservable определена вsrc/store.jsВ файле:

// src/store.js
const connectionStore = new WeakMap()

export function storeObservable (obj) {
  // 用于后续保存obj.key -> reaction之间映射关系
  connectionStore.set(obj, new Map())
}

Введя так много, брат Абао резюмирует предыдущее содержание картинкой:

Что касаетсяproxyToRawа такжеrawToProxyКакая польза от предметов? Поверьте, что после прочтения следующего кода вы узнаете ответ.

// src/observable.js
export function observable (obj = {}) {
  // 如果obj已经是一个observable对象或者不应该被包装,则直接返回它
  if (proxyToRaw.has(obj) || !builtIns.shouldInstrument(obj)) {
    return obj
  }

  // 如果obj已经有一个对应的observable对象,则将其返回。否则创建一个新的observable对象
  return rawToProxy.get(obj) || createObservable(obj)
}

Приступим к анализу строки B:

const countLogger = observe(() => console.log(counter.num)); // B

observeфункция определена вsrc/observer.jsВ документе его конкретное определение выглядит следующим образом:

// src/observer.js
export function observe (fn, options = {}) {
  // const IS_REACTION = Symbol('is reaction')
  const reaction = fn[IS_REACTION]
    ? fn
    : function reaction () {
      return runAsReaction(reaction, fn, this, arguments)
    }
  // 省略部分代码
  reaction[IS_REACTION] = true
  // 如果非lazy,则直接运行
  if (!options.lazy) {
    reaction()
  }
  return reaction
}

В приведенном выше коде входящийfnне так лиreactionфункция, если она есть, используйте ее напрямую. Если нет, то входящийfnупаковано вreactionфункцию, а затем вызвать эту функцию. существуетreactionВнутри функции будет вызываться другая функция —runAsReaction, как следует из названия, эта функция используется для запускаreactionфункция.

runAsReactionфункция определена вsrc/reactionRunner.jsВ файле:

// src/reactionRunner.js
const reactionStack = []

export function runAsReaction (reaction, fn, context, args) {
  // 省略部分代码
  if (reactionStack.indexOf(reaction) === -1) {
    // 释放(obj -> key -> reactions) 链接并复位清理器链接
    releaseReaction(reaction)

    try {
      // 压入到reactionStack堆栈中,以便于在get陷阱中能建立(observable.prop -> reaction)之间的联系
      reactionStack.push(reaction)
      return Reflect.apply(fn, context, args)
    } finally {
      // 从reactionStack堆栈中,移除已执行的reaction函数
      reactionStack.pop()
    }
  }
}

существуетrunAsReactionВ теле функции выполняемая в данный моментreactionфункция толчокreactionStackстек, затем используйтеReflect.applyВызов API переданfnфункция. когдаfnКогда функция выполняется, она выполняетсяconsole.log(counter.num)оператор, внутри которого оператор обращается кcounterобъектnumАтрибуты.counterОбъект является прокси-объектом, который срабатывает при доступе к свойствам объекта.baseHandlersсерединаgetловушка:

// src/handlers.js
function get (target, key, receiver) {
  const result = Reflect.get(target, key, receiver)
  // 注册并保存(observable.prop -> runningReaction)
  registerRunningReactionForOperation({ target, key, receiver, type: 'get' })
  const observableResult = rawToProxy.get(result)
  if (hasRunningReaction() && typeof result === 'object' && result !== null) {
    // 省略部分代码
  }
  return observableResult || result
}

В приведенной выше функцииregisterRunningReactionForOperationфункция для сохраненияobservable.prop -> runningReactionкартографические отношения между ними. По сути, это добавление соответствующих наблюдателей к указанным свойствам объекта, что является очень ответственным шагом. Итак, давайте сосредоточимся на анализеregisterRunningReactionForOperationфункция:

// src/reactionRunner.js
export function registerRunningReactionForOperation (operation) {
  // 从栈顶获取当前正在执行的reaction
  const runningReaction = reactionStack[reactionStack.length - 1]
  if (runningReaction) {
    debugOperation(runningReaction, operation)
    registerReactionForOperation(runningReaction, operation)
  }
}

существуетregisterRunningReactionForOperationфункция, перваяreactionStackПолучите из стека текущую функцию реакции и снова вызовите ее.registerReactionForOperationФункция регистрирует функцию реакции для текущей операции.Конкретная логика обработки следующая:

// src/store.js
export function registerReactionForOperation (reaction, { target, key, type }) {
  // 省略部分代码
  const reactionsForObj = connectionStore.get(target) // A
  let reactionsForKey = reactionsForObj.get(key) // B
  if (!reactionsForKey) { // C
    reactionsForKey = new Set()
    reactionsForObj.set(key, reactionsForKey)
  }
  if (!reactionsForKey.has(reaction)) { // D
    reactionsForKey.add(reaction)
    reaction.cleaners.push(reactionsForKey)
  }
}

вызовobservable(obj)Когда функция создает наблюдаемый объект, она будет использовать объект obj в качестве ключа и сохранит его вconnectionStore(connectionStore.set(obj, new Map())) объект. Брат АпоregisterReactionForOperationЛогика обработки внутри функции разделена на 4 части:

  • (A): получить значение, соответствующее target, из объекта connectionStore (WeakMap) и вернуть объект responseForObj (Map);
  • (B): Получить значение, соответствующее ключу (свойству объекта) из объекта responseForKey (Map), если он не существует, он вернет значение undefined;
  • (C): Если responseForKey не определен, объект Set будет создан и сохранен как значение в объекте responseForObj (Map);
  • (D): Определите, содержит ли коллекция responseForKey(Set) текущую функцию реакции, если нет, добавьте текущую функцию реакции в коллекцию responseForKey(Set).

Чтобы все лучше поняли содержание этой части, брат Абао продолжает резюмировать вышеизложенное, рисуя:

Поскольку каждое свойство объекта может быть связано с несколькимиreactionфункция, чтобы избежать дублирования, мы используем объект Set для хранения связанногоreactionфункция. И объект может содержать несколько свойств, поэтомуobserver-utilВнутренне объект Map используется для хранения каждого свойства с помощьюreactionСвязь между функциями.

Кроме того, для поддержки возможности превращать несколько объектов в наблюдаемые объекты и своевременно освобождать память при уничтожении исходного объекта.observer-utilОпределяет тип WeakMapconnectionStoreобъект для хранения отношения связи объекта. Для текущего примераconnectionStoreВнутренняя структура объекта выглядит так:

Наконец, давайте проанализируемcounter.num++;эта строка кода. Для простоты Brother Abao анализирует только основную логику обработки, а те, кому интересен полный код, могут ознакомиться с исходным кодом проекта. при исполненииcounter.num++;Эта строка кода вызовет наборsetловушка:

// src/handlers.js
function set (target, key, value, receiver) {
  // 省略部分代码
  const hadKey = hasOwnProperty.call(target, key)
  const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)
  if (!hadKey) {
    queueReactionsForOperation({ target, key, value, receiver, type: 'add' })
  } else if (value !== oldValue) {
    queueReactionsForOperation({
      target,
      key,
      value,
      oldValue,
      receiver,
      type: 'set'
    })
  }
  return result
}

Для нашего примера вызоветqueueReactionsForOperationфункция:

// src/reactionRunner.js
export function queueReactionsForOperation (operation) {
  // iterate and queue every reaction, which is triggered by obj.key mutation
  getReactionsForOperation(operation).forEach(queueReaction, operation)
}

существуетqueueReactionsForOperationФункция будет продолжать вызыватьgetReactionsForOperationФункция получает реакции, соответствующие текущей клавише:

// src/store.js
export function getReactionsForOperation ({ target, key, type }) {
  const reactionsForTarget = connectionStore.get(target)
  const reactionsForKey = new Set()

  if (type === 'clear') {
    reactionsForTarget.forEach((_, key) => {
      addReactionsForKey(reactionsForKey, reactionsForTarget, key)
    })
  } else {
    addReactionsForKey(reactionsForKey, reactionsForTarget, key)
  }
	// 省略部分代码
  return reactionsForKey
}

После успешного получения объекта реакции, соответствующего текущему ключу, он будет проходить объект для выполнения каждой реакции.Конкретная логика обработки определена вqueueReactionВ функции:

// src/reactionRunner.js
function queueReaction (reaction) {
  debugOperation(reaction, this)
  // queue the reaction for later execution or run it immediately
  if (typeof reaction.scheduler === 'function') {
    reaction.scheduler(reaction)
  } else if (typeof reaction.scheduler === 'object') {
    reaction.scheduler.add(reaction)
  } else {
    reaction()
  }
}

Поскольку наш пример не настроенschedulerпараметры, поэтому он будет выполняться напрямуюelseРазветвленный код, т.е. выполняетreaction()заявление.

ХОРОШО,observer-utilБыла проанализирована основная логика преобразования обычных объектов в наблюдаемые в этой библиотеке. Для обычных предметов,observer-utilСредство просмотра, предоставляемое внутри ловушки с помощью API-интерфейса get и set Proxy, автоматически добавляется (функция реакции добавления) и уведомляет наблюдателей (реакция выполнения функции) логики обработки.

Если вы прочитали содержание, представленное в этой статье, вы должны понять модуль реактивности в Vue3.targetMapСоответствующее определение:

// vue-next/packages/reactivity/src/effect.ts
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

Помимо обычных объектов и массивов,observer-utilТакже поддерживаются коллекции в ES6, такие как Map, Set и WeakMap. При работе с этими объектами при создании объекта Proxy используйтеcollectionHandlersобъект вместоbaseHandlersобъект. Брат Абао не будет представлять эту часть контента, и заинтересованные друзья могут сами прочитать соответствующий код.

Подпишитесь на «Дорогу развития полного стека», чтобы прочитать 4 бесплатные электронные книги (всего более 21 000 загрузок) и 10 серий анализа исходного кода от Brother Abao.

4. Справочные ресурсы