предисловие
Предыдущая статья из этой серии
Мы подробно объяснили принцип общих объектов и массивов для достижения отзывчивости, но прокси может сделать гораздо больше.Для новых, добавленных в es6Map,Set,WeakMap,WeakSetТакже возможна оперативная поддержка.
Но для этой части перехвата логика в коде является совершенно независимым набором.В этой статье будет рассмотрено, как добиться этого требования на основе перехвата функций.
Некоторые предпосылки для прочтения этой статьи:
Proxy
WeakMap
Reflect
Symbol.iterator(объясню)
почему особенный
В предыдущей статье предположим, что мы прошлиdata.aчитать ответные данныеdataатрибут, это приведет к захвату прокси-сервераget(target, key)
цельdata对应的原始对象Ключa
Мы можем дать ключ в этот момент:aЗарегистрируйте зависимость, а затем прочитайте исходные данные и верните их через Reflect.get(data, key).
Резюме:
/** 劫持get访问 收集依赖 */
function get(target: Raw, key: Key, receiver: ReactiveProxy) {
const result = Reflect.get(target, key, receiver)
// 收集依赖
registerRunningReaction({ target, key, receiver, type: "get" })
return result
}
И когда наш реактивный объектMapКогда дело доходит до типов данных, представьте себе такой сценарий:
const data = reactive(new Map([['a', 1]]))
observe(() => data.get('a'))
data.set('a', 2)
Способ чтения данных становитсяdata.get('a')В этой форме, если все еще используется get в предыдущей статье, что произойдет?
get(target, key)цель вmap原始对象, ключget,
То, что возвращает Reflect.get,map.getэтоfunction, затем черезgetЭта зависимость от регистрации ключа, это не то, что мы хотим, эффект, который мы хотим, достигается черезaЭтот ключ используется для регистрации зависимостей.
Итак, путь здесь函数劫持, представьте, что мы перехватываем доступ ко всем ключам на карте, например, пользователю использоватьmap.get, если этот get обращается к реализованной нами функции get, то эта функция get может свободно делать что угодно, например收集依赖~
Тогда следующая цель –Mapа такжеSetдоступ ко всем API (таким какhas, get, set, add) заменены нашими собственными методами, чтобы пользователи могли незаметно использовать эти API, но внутреннее устройство было захвачено нашим собственным кодом.
выполнить
Мы скорректировали структуру каталогов в предыдущей статье следующим образом:
src/handlers
// 数组和对象的handlers
├── base.ts
// map和set的handlers
├── collections.ts
// 统一导出
└── index.ts
Вход
Сначала посмотрите на преобразование записи handlers/index.ts.
import { collectionHandlers } from "./collections"
import { baseHandlers } from "./base"
import { Raw } from "types"
// @ts-ignore
// 根据对象的类型 获取Proxy的handlers
export const handlers = new Map([
[Map, collectionHandlers],
[Set, collectionHandlers],
[WeakMap, collectionHandlers],
[WeakSet, collectionHandlers],
[Object, baseHandlers],
[Array, baseHandlers],
[Int8Array, baseHandlers],
[Uint8Array, baseHandlers],
[Uint8ClampedArray, baseHandlers],
[Int16Array, baseHandlers],
[Uint16Array, baseHandlers],
[Int32Array, baseHandlers],
[Uint32Array, baseHandlers],
[Float32Array, baseHandlers],
[Float64Array, baseHandlers],
])
/** 获取Proxy的handlers */
export function getHandlers(obj: Raw) {
return handlers.get(obj.constructor)
}
Это определяет карту:handlersЭкспорт одинgetHandlersМетод получения второго параметра Proxy в соответствии с типом входящих данныхhandlers,
baseHandlersПодробно это было объяснено в первой части.
В этой статье в основном объясняетсяcollectionHandlers.
collections
Первый взглядcollectionsВход:
// 真正交给Proxy第二个参数的handlers只有一个get
// 把用户对于map的get、set这些api的访问全部移交给上面的劫持函数
export const collectionHandlers = {
get(target: Raw, key: Key, receiver: ReactiveProxy) {
// 返回上面被劫持的api
target = hasOwnProperty.call(instrumentations, key)
? instrumentations
: target
return Reflect.get(target, key, receiver)
},
}
Только один из всех наших обработчиковget, то есть доступ пользователя ко всем API на карте или наборе (например,has, get, set, add), будут переданы нашему собственному определенному API, который на самом деле является приложением для захвата функций.
Это ключinstrumentationsНа этом объекте наша собственная реализация этих API.
Реализация угона апи
получить и установить
export const instrumentations = {
get(key: Key) {
// 获取原始数据
const target = proxyToRaw.get(this)
// 获取原始数据的__proto__ 拿到原型链上的方法
const proto: any = Reflect.getPrototypeOf(this)
// 注册get类型的依赖
registerRunningReaction({ target, key, type: "get" })
// 调用原型链上的get方法求值 然后对于复杂类型继续定义成响应式
return findReactive(proto.get.apply(target, arguments))
},
set(key: Key, value: any) {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
// 是否是新增的key
const hadKey = proto.has.call(target, key)
// 拿到旧值
const oldValue = proto.get.call(target, key)
// 求出结果
const result = proto.set.apply(target, arguments)
if (!hadKey) {
// 新增key值时以type: add触发观察函数
queueReactionsForOperation({ target, key, value, type: "add" })
} else if (value !== oldValue) {
// 已存在的key的值发生变化时以type: set触发观察函数
queueReactionsForOperation({ target, key, value, oldValue, type: "set" })
}
return result
},
}
/** 对于返回值 如果是复杂类型 再进一步的定义为响应式 */
function findReactive(obj: Raw) {
const reactiveObj = rawToProxy.get(obj)
// 只有正在运行观察函数的时候才去定义响应式
if (hasRunningReaction() && isObject(obj)) {
if (reactiveObj) {
return reactiveObj
}
return reactive(obj)
}
return reactiveObj || obj
}
основнойgetа такжеsetМетод почти такой же, как реализация в предыдущей статье,getЗначение, возвращаемоеfindReactiveНе забудьте дополнительно определить адаптивные данные, чтобы обеспечить глубокую отзывчивость.
На этом этапе такой вариант использования можно запустить:
const data = reactive(new Map([['a', 1]]))
observe(() => console.log('a', data.get('a')))
data.set('a', 5)
// 重新打印出a 5
Далее мы реализуем некоторые конкретные API:
has
has (key) {
const target = proxyToRaw.get(this)
const proto = Reflect.getPrototypeOf(this)
registerRunningReactionForOperation({ target, key, type: 'has' })
return proto.has.apply(target, arguments)
},
add
add — это типичный процесс добавления ключа, который запускает функцию наблюдения, связанную с циклом.
add (key: Key) {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
const hadKey = proto.has.call(target, key)
const result = proto.add.apply(target, arguments)
if (!hadKey) {
queueReactionsForOperation({ target, key, value: key, type: 'add' })
}
return result
},
delete
delete также примерно такой же, как реализация deleteProperty в предыдущей статье, которая активирует функции наблюдения, связанные с циклом.
delete (key: Key) {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
const hadKey = proto.has.call(target, key)
const result = proto.delete.apply(target, arguments)
if (hadKey) {
queueReactionsForOperation({ target, key, type: 'delete' })
}
return result
},
clear
clear () {
const target: any = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
const hadItems = target.size !== 0
const result = proto.clear.apply(target, arguments)
if (hadItems) {
queueReactionsForOperation({ target, type: 'clear' })
}
return result
},
При запуске функции наблюдения выполняется некоторая специальная обработка для типа очистки, которая также является функцией наблюдения, связанной с циклом запуска.
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)
}
if (
type === 'add'
|| type === 'delete'
+ || type === 'clear'
) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATION_KEY
addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey)
}
return reactionsForKey
}
clearПри , получить функцию наблюдения, собранную каждой клавишей, а также получить функцию наблюдения цикла, который можно назвать наиболее полным триггером.
Логика тоже понятнаclearНеобходимо учитывать поведение каждой клавиши.Пока любая клавиша считывается в функции наблюдения, функция наблюдения должна выполняться повторно при очистке.
forEach
forEach (cb, ...args) {
const target = proxyToRaw.get(this)
const proto = Reflect.getPrototypeOf(this)
registerRunningReaction({ target, type: 'iterate' })
const wrappedCb = (value, ...rest) => cb(findObservable(value), ...rest)
return proto.forEach.call(target, wrappedCb, ...args)
},
Когда дело доходит до захвата forEach, все немного сложнее.
первыйregisterRunningReactionПри регистрации зависимостей используется ключiterate, это легко понять, потому что это операция обхода.
Таким образом, пользователь впоследствии выполняет新增или删除или используйтеclearПри работе внутренний вызов будет инициирован повторноforEachфункция наблюдения
Сосредоточьтесь на следующих двух фрагментах кода:
const wrappedCb = (value, ...rest) => cb(findObservable(value), ...rest)
return proto.forEach.call(target, wrappedCb, ...args)
wrapCb оборачивает функцию cb, переданную пользователем в forEach, а затем передает ее forEach в цепочке прототипов объекта коллекции, что является еще одним перехватом функции. Пользователь передал map.forEach(cb), и в итоге мы вызвали map.forEach(wrappedCb).
В этом wrapCb мы передаем исходное значение value, которое должно было быть получено в cb, черезfindObservableОпределяется как реагирующие данные для пользователя, так что реагирующая операция, выполняемая пользователем в forEach, также может собирать зависимости, и я должен восхищаться изобретательностью этого дизайна.
keys && size
get size () {
const target = proxyToRaw.get(this)
const proto = Reflect.getPrototypeOf(this)
registerRunningReaction({ target, type: 'iterate' })
return Reflect.get(proto, 'size', target)
},
keys () {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
registerRunningReaction({ target, type: 'iterate' })
return proto.keys.apply(target, arguments)
},
из-заkeysа такжеsizeВозвращаемое значение не обязательно определять как реактивное, поэтому просто верните исходное значение напрямую.
values
Рассмотрим типичный случай, требующий особого обращения.
values () {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
registerRunningReaction({ target, type: 'iterate' })
const iterator = proto.values.apply(target, arguments)
return patchIterator(iterator, false)
},
Здесь следует отметить момент знания, то есть метод значений объекта коллекции возвращает объект итератора.Map.values,
Этот объект итератора вызывается каждый разnext()вернет следующее значение на карте
, так что значение, полученное с помощью next(), также может стать响应式proxy, нам нужно использоватьpatchIteratorугонятьiterator
// 把iterator劫持成响应式的iterator
function patchIterator (iterator) {
const originalNext = iterator.next
iterator.next = () => {
let { done, value } = originalNext.call(iterator)
if (!done) {
value = findReactive(value)
}
return { done, value }
}
return iterator
}
Это также классическая логика перехвата функций, которая берет исходную{ done, value }Получите значение, определите значение как响应式proxy.
После понимания этой концепции остальные связанные обработчики легко понять.
entries
entries () {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
registerRunningReaction({ target, type: 'iterate' })
const iterator = proto.entries.apply(target, arguments)
return patchIterator(iterator, true)
},
вести перепискуentriesСуществует также специальная обработка, передача итератора вpatchIteratorКогда это необходимо специально пометить, этоentries,посмотриpatchIteratorизменения:
/** 把iterator劫持成响应式的iterator */
function patchIterator (iterator, isEntries) {
const originalNext = iterator.next
iterator.next = () => {
let { done, value } = originalNext.call(iterator)
if (!done) {
+ if (isEntries) {
+ value[1] = findReactive(value[1])
} else {
value = findReactive(value)
}
}
return { done, value }
}
return iterator
}
Каждый элемент операции записи представляет собой массив [key, val], поэтому при индексации [1] только значение определяется как отвечающее, а ключ не требует специальной обработки.
Symbol.iterator
[Symbol.iterator] () {
const target = proxyToRaw.get(this)
const proto: any = Reflect.getPrototypeOf(this)
registerRunningReaction({ target, type: 'iterate' })
const iterator = proto[Symbol.iterator].apply(target, arguments)
return patchIterator(iterator, target instanceof Map)
},
Вот еще одна довольно специальная обработка.[Symbol.iterator]Этот встроенный объект будет вfor ofОн срабатывает при выполнении операции.Подробности см. в документе mdn, приведенном в начале этой статьи. Так что нам также нужно использовать идею захвата итератора выше.
Второй параметр patchIterator связан с тем, чтоMapиспользование структуры данныхfor ofПри работе структура записей возвращается, поэтому также требуется специальная обработка.
Пасхальное яйцо TypeScript
Поскольку в этой статье рассказывается о Map, я подумал, что делать вывод типа для Map в TS неудобно, например, следующим методом:
function createMap<T extends object, K extends keyof T>(obj: T) {
const map = new Map<K, T>()
Object.keys(obj).forEach((key) => {
map.set(key as K, obj[key])
})
return map
}
// 提示出来的类型是 {
// a: number;
// b: string;
// }
const a = createMap({a: 1, b: '2'}).get('a')
Поскольку Map присваивается вызовом set, у ts нет возможности очень хорошо выполнять вывод типа и точно определять тип, соответствующий значению ключа.劫持Как насчет идей?
Суммировать
Код этой статьи находится в этом репозитории
GitHub.com/Забудьте об этом 1673495/Боюсь…
Идея перехвата функций появилась в различных интерфейсных библиотеках.Это почти навык, который необходимо освоить продвинутым игрокам.Я надеюсь, что благодаря изучению этой статьи вы сможете понять некоторые мощные функции перехвата функций. Также можно использовать прокси в Vue3 для достижения отзывчивости.