предисловие
Чтобы лучше объяснить, я изменю порядок объявления интерфейсов, типов, функций и т. д. в исходном коде и добавлю некоторые комментарии для облегчения чтения.
В предыдущей главе мы представилиref
, если вы посмотрите внимательно, вы должны быть правыref
Это должно было быть хорошо известно. Если нет, то забытое.... или можно сначала посмотреть статью.
Предварительные знания, необходимые для прочтения этой статьи:
Reactive
reactive
В этом файле не так много кода, около 100 строк, и много логики на самом деле вhandlers
а такжеeffect
середина. Давайте сначала посмотрим на введение этого файла:
внешняя ссылка
import {
isObject, // 判断是否是对象
toTypeString // 获取数据的类型名称
} from '@vue/shared'
// 此处的handles最终会传递给Proxy(target, handle)的第二个参数
import {
mutableHandlers, // 可变数据代理处理
readonlyHandlers // 只读(不可变)数据代理处理
} from './baseHandlers'
// collections 指 Set, Map, WeakMap, WeakSet
import {
mutableCollectionHandlers, // 可变集合数据代理处理
readonlyCollectionHandlers // 只读集合数据代理处理
} from './collectionHandlers'
// 上篇文章中说了半天的泛型类型
import { UnwrapRef } from './ref'
// 看过单测篇的话,应该知道这个是被effect执行后返回的监听函数的类型
import { ReactiveEffect } from './effect'
Так что не бойтесь, многие просто ссылаются на простые методы и типы инструментов, и лишь немногие действительно связаны с внешними функциями.handlers
.
Типы и константы
Давайте посмотрим на объявления типов и объявления переменных, сначала посмотрим на множество комментариевtargetMap
.
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
// 翻译自上述英文:利用WeakMap是为了更好的减少内存开销。
export const targetMap = new WeakMap<any, KeyToDepMap>()
traget
означаетProxy(target, handle)
Первый параметр функции — это необработанные данные, которые мы хотим преобразовать в реактивные данные. но этоKeyToDepMap
На самом деле, я не знаю, что такое отображение. Отложите его на время, а затем посмотрите на него, когда мы на самом деле будем его использовать.
Продолжайте видеть, это стопка констант.
// raw这个单词在ref篇我们见过,它在这个库的含义是,原始数据
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
// 集合类型
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
// 用于正则判断是否符合可观察数据,object + array + collectionTypes
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
Если вы читали одну тестовую статью (реактивное 8,9,10 первое одиночное измерение), вы, возможно, помните сказанное ранее, две внутренние потребностиWeakMap
Реализовать двунаправленное сопоставление исходных данных и данных ответа. очевидноrawToReactive
а такжеreactiveToRaw
это эти двоеWeakMap
.rawToReadonly
а такжеreadonlyToRaw
Как следует из названия, он предназначен для сопоставления исходных данных и данных ответа, доступных только для чтения.WeakMap
.
readonlyValues
а такжеnonReactiveValues
Судя по заметкам и воспоминаниям о предыдущем одиночном тесте, может бытьmarkNonReactive
а такжеmarkReadonly
(Эта единственная тестовая статья не упоминается). Думаю, он используется для хранения данных, созданных с помощью этих двух API, которые можно увидеть позже.
collectionTypes
а такжеobservableValueRE
Просто посмотрите на записи.
Вспомогательная функция
в реальном видеreactive
Раньше мы сначала рассмотрели некоторые инструментальные решения в этом файле, чтобы при просмотре исходного кода вы не прыгали в беспорядке. Эта часть относительно проста, просто взгляните на нее.
// 数据是否可观察
const canObserve = (value: any): boolean => {
return (
// 整个vue3库都没搜到_isVue的逻辑,猜测是vue组件,不影响本库阅读
!value._isVue &&
// 虚拟dom的节点不可观察
!value._isVNode &&
// 属于可观察的数据类型
observableValueRE.test(toTypeString(value)) &&
// 该集合中存储的数据不可观察
!nonReactiveValues.has(value)
)
}
// 如果reactiveToRaw或readonlyToRaw中存在该数据了,说明就是响应式数据
export function isReactive(value: any): boolean {
return reactiveToRaw.has(value) || readonlyToRaw.has(value)
}
// 判断是否是只读的响应式数据
export function isReadonly(value: any): boolean {
return readonlyToRaw.has(value)
}
// 将响应式数据转为原始数据,如果不是响应数据,则返回源数据
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
// 传递数据,将其添加到只读数据集合中
// 注意readonlyValues是个WeakSet,利用set的元素唯一性,可以避免重复添加
export function markReadonly<T>(value: T): T {
readonlyValues.add(value)
return value
}
// 传递数据,将其添加至不可响应数据集合中
export function markNonReactive<T>(value: T): T {
nonReactiveValues.add(value)
return value
}
основная реализация
Приведенные выше коды являются приправами. Давайте посмотрим на основной код этого файла. Сначалаreactive
а такжеreadonly
функция
// 函数类型声明,接受一个对象,返回不会深度嵌套的Ref数据
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// 函数实现
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果传递的是一个只读响应式数据,则直接返回,这里其实可以直接用isReadonly
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
// 如果是被用户标记的只读数据,那通过readonly函数去封装
if (readonlyValues.has(target)) {
return readonly(target)
}
// 到这一步的target,可以保证为非只读数据
// 通过该方法,创建响应式对象数据
return createReactiveObject(
target, // 原始数据
rawToReactive, // 原始数据 -> 响应式数据映射
reactiveToRaw, // 响应式数据 -> 原始数据映射
mutableHandlers, // 可变数据的代理劫持方法
mutableCollectionHandlers // 可变集合数据的代理劫持方法
)
}
// 函数声明+实现,接受一个对象,返回一个只读的响应式数据。
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
// value is a mutable observable, retrieve its original and return
// a readonly version.
// 如果本身是响应式数据,获取其原始数据,并将target入参赋值为原始数据
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
// 创建响应式数据
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}
Код двух методов на самом деле очень прост, основная логика инкапсулирована.createReactiveObject
Основная роль обоих методов:
- пройти через
createReactiveObject
Соответствующая двунаправленная карта прокси-данных и реактивных данных. -
reactive
Сможет сделатьreadonly
, наоборотreadonly
метод тоже.
Продолжить чтение ниже:
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 不是一个对象,直接返回原始数据,在开发环境下会打警告
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 通过原始数据 -> 响应数据的映射,获取响应数据
let observed = toProxy.get(target)
// target already has corresponding Proxy
// 如果原始数据已经是响应式数据,则直接返回此响应数据
if (observed !== void 0) {
return observed
}
// target is already a Proxy
// 如果原始数据本身就是个响应数据了,直接返回自身
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
// 如果是不可观察的对象,则直接返回原对象
if (!canObserve(target)) {
return target
}
// 集合数据与(对象/数组) 两种数据的代理处理方式不同。
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 声明一个代理对象,也即是响应式数据
observed = new Proxy(target, handlers)
// 设置好原始数据与响应式数据的双向映射
toProxy.set(target, observed)
toRaw.set(observed, target)
// 在这里用到了targetMap,但是它的value值存放什么我们依旧不知道
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
Мы можем увидеть некоторые детали:
- Если передается не объект, только среда разработки сообщит о предупреждении и не вызовет исключения. Это связано с тем, что производственная среда чрезвычайно сложна, а поскольку js является динамическим языком, если сообщение об ошибке будет сообщено напрямую, это напрямую повлияет на различные онлайн-приложения. В этом он просто возвращает исходные данные, теряет отзывчивость, но не вызывает исключение для реальной страницы.
- Этот метод принципиально не имеет типа ТС.
reactive
Документ на самом деле очень прост для понимания, после прочтения у нас в голове только 2 вопроса:
-
baseHandlers
,collectionHandlers
Конкретная реализация и почему ее следует различать? -
targetMap
Что это такое?
Конечно, мы знаемhandlers
Он должен запускаться сбором зависимостей и ответом. Итак, давайте сначала посмотрим на два файла.
baseHandles
Откройте этот файл, а также сначала просмотрите внешние ссылки:
// 这些我们已经了解了
import { reactive, readonly, toRaw } from './reactive'
import { isRef } from './ref'
// 这些就是些工具方法,hasOwn 意为对象是否拥有某数据
import { isObject, hasOwn, isSymbol } from '@vue/shared'
// 这里定义了操作数据的行为枚举
import { OperationTypes } from './operations'
// LOCKED:global immutability lock
// 一个全局用来判断是否数据是不可变的开关
import { LOCKED } from './lock'
// 收集依赖跟触发监听函数的两个方法
import { track, trigger } from './effect'
Толькоtrack
а такжеtrigger
Мы не знаем внутреннюю реализацию , остальные либо уже понятны, либо нажимают на нее и понимают с первого взгляда.
Затем идет набор дескрипторов, представляющих поведение внутреннего языка JS.Если вы не понимаете, вы можете увидеть соответствующийMDN. Как им пользоваться можно будет увидеть позже.
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => Symbol[key])
.filter(key => typeof key === 'symbol')
)
Затем вы обнаружите, что ниже сотни строк кода, мы находимreactive
цитируется вmutableHandlers
,readonlyHandlers
. Давайте посмотрим на простойmutableHandlers
:
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
ЭтоProxyHandle
,оProxy
Если вы забудете, не забудьте прочитать его сноваMDN.
Затем мы, наконец, добрались до самой важной части всей адаптивной системы.traps
:get
,set
,deleteProperty
,has
,ownKeys
. КонечноProxy
достижимыйtrap
Не только эти пять. вdefineProperty
а такжеgetOwnPropertyDescriptor
дваtrap
Никакой реакции, никакого угона не требуется. есть еще одинenumerate
был заброшен.enumerate
угнал быfor-in
операция, то подумаешь, то это заброшено, нашеfor-in
Как сделать? Не волнуйся, все еще идетownKeys
этоtrap
, который затем запускает нашу функцию прослушивания.
Сказав это, вернемся к коду, мы начнем сget
Смотреть. этоtrap
черезcreateGetter
генерация функций, так что давайте посмотрим на это.
get
createGetter
Принять ввод:isReadonly
. Это естественноreadonlyHandlers
Китайский это биографияtrue
.
// 入参只有一个是否只读
function createGetter(isReadonly: boolean) {
// 关于proxy的get,请阅读:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
// receiver即是被创建出来的代理对象
return function get(target: any, key: string | symbol, receiver: any) {
// 如果还不了解Reflect,建议先阅读它的文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
// 获取原始数据的相应值
const res = Reflect.get(target, key, receiver)
// 如果是js的内置方法,不做依赖收集
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 如果是Ref类型数据,说明已经被收集过依赖,不做依赖收集,直接返回其value值。
if (isRef(res)) {
return res.value
}
// 收集依赖
track(target, OperationTypes.GET, key)
// 通过get获取的值不是对象的话,则直接返回即可
// 否则,根据isReadyonly返回响应数据
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
С первого взгляда вы обнаружите, чтоget
Каждое выражение метода на самом деле относительно простое, но кажется немного запутанным.
Вопрос 1: Зачем проходитьReflect
, а не напрямуюtarget[key]
?
В самом деле,target[key]
Вроде бы можно добиться эффекта, зачем использоватьReflect
, Еще проходитreceiver
Шерстяная ткань? Причина в том, что исходные данныеget
Все не так просто, как все думают, например вот этот случай:
const targetObj = {
get a() {
return this
}
}
const proxyObj = reactive(targetObj)
В настоящее время,proxyObj.a
В вашем воображении должно бытьproxyObj
ещеtargetObj
Шерстяная ткань? Я чувствую разумно, это должно бытьproxyObj
. ноa
Это не метод, нет прямогоcall/apply
. Его тоже можно реализовать, он более намотан, что примерно равно реализованномуReflect
полифилл для . Итак, благодаря ES6, воспользуйтесь преимуществамиReflect
, который может легко отразить существующее поведение операции на целевой объект как есть, и обеспечить реальную область видимости (через третий параметрreceiver
). этоreceiver
является сгенерированным прокси-объектом, который в приведенном выше примереproxyObj
.
Вопрос 2: Почему встроенные методы не должны собирать зависимости?
Если функция слушателя выглядит так:
const origin = {
a() {}
}
const observed = reactive(origin)
effect(() => {
console.log(observed.a.toString())
})
Очевидно, когдаorigin.a
при смене,observed.a.toString()
Он тоже должен меняться, так почему бы не следить за этим? Это очень просто, потому что его уже нетobserved.a.toString()
ходили один разget
trap, нет необходимости повторно собирать зависимости. Поэтому аналогичные встроенные методы возвращаются напрямую.
Вопрос 3: Почему его нужно использовать повторно, если значением свойства является объектreactive|readonly
воплощать в жизнь?
В примечаниях говорится:
need to lazy access readonly and reactive here to avoid circular dependency
Переведено на мандарин Да, его нужно использовать с задержкойreactive|readonly
чтобы избежать циклических зависимостей. Эти слова нужно попробовать, тщательно попробовать и, наконец, понять, попробовав некоторое время.
потому что из-заProxy
Это место, этоtrap
На самом деле перехватить можно только доступ и обновление объектов первого уровня. Если это вложенный объект, его нельзя захватить. Тогда у нас есть два метода:
метод первый: когда прошлоreactive|readonly
При преобразовании исходного объекта он рекурсивно разворачивается слой за слоем.Если это объект, используйте его снова.reactive
выполнить, затем идтиProxyHandle
. При доступе к этим вложенным свойствам в будущем он, естественно, попадет в ловушку. Но с этим возникает большая проблема: что, если на объект ссылаются по кругу? Это должно быть логическое суждение.Если значение атрибута оказывается самим собой, оно не будет рекурсивно. Что, если это полукруглая ссылка? Например:
const a = {
b: {
c: a
}
}
const A = {
B: {
C: a
}
}
Думайте масштабно.
Способ второй: То есть метод в исходниках, при преобразовании исходного объекта, не является рекурсивным. следовать заget
Когда ловушка будет найдена, если значение свойства окажется объектом, оно продолжит преобразование и захват. Вот что говорят заметкиlazy
. Используя этот метод, естественно можно избежать циклических ссылок. Еще одним очевидным преимуществом является возможность оптимизации производительности.
Помимо этих трех вопросов, есть еще одна маленькая деталь:
if (isRef(res)) {
return res.value
}
еслиRef
类型的数据,则直接返回 value 值。 Потому чтоref
В функции реализована соответствующая логика отслеживания зависимостей. Кроме того, если вы читали единственную тестовую статью и справочную статью, мы знаем, что именно этот код реализует возможность:reacitive
функция передает вложенныйRef
введите данные, которые возвращают рекурсивное решениеRef
тип реактивных данных.reactive
Тип возвращаемого значения функцииUnwrapNestedRefs
Кредит на это.
Но помните: чтобыreactive
пройти чистыйRef
Данные типа не будут развернуты, они только развернули вложенныеRef
данные. Пример выглядит следующим образом:
reactive(ref(4)) // = ref(4);
reactive({ a: ref(4) }) // = { a: 4 }
Пока, кромеtrack
Это внешний метод сбора зависимостей (см. далее).get
Уже разобрался.
Смотри нижеset
.
set
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
// 如果value是响应式数据,则返回其映射的源数据
value = toRaw(value)
// 获取旧值
const oldValue = target[key]
// 如果旧值是Ref数据,但新值不是,那更新旧的值的value属性值,返回更新成功
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// 代理对象中,是不是真的有这个key,没有说明操作是新增
const hadKey = hasOwn(target, key)
// 将本次设置行为,反射到原始对象上
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 如果是原始数据原型链上的数据操作,不做任何触发监听函数的行为。
if (target === toRaw(receiver)) {
// istanbul 是个单测覆盖率工具
/* istanbul ignore else */
if (__DEV__) {
// 开发环境下,会传给trigger一个扩展数据,包含了新旧值。明显的是便于开发环境下做一些调试。
const extraInfo = { oldValue, newValue: value }
// 如果不存在key时,说明是新增属性,操作类型为ADD
// 存在key,则说明为更新操作,当新值与旧值不相等时,才是真正的更新,进而触发trigger
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
// 同上述逻辑,只是少了extraInfo
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
set
а такжеget
Опять же, каждое выражение понятно, но у нас все еще есть вопросы.
Вопрос 1:isRef(oldValue) && !isRef(value)
Какова логика этого абзаца?
// 如果旧值是 Ref 数据,但新值不是,那更新旧的值的 value 属性值,返回更新成功
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
при каких обстоятельствах oldValue будетRef
Как насчет данных? На самом деле см.get
Часть времени, мы знаем,reactive
Имеет возможность распаковывать данные вложенных ссылок, например:
const a = {
b: ref(1)
}
const observed = reactive(a) // { b: 1 }
В настоящее время,observed.b
Выход равен 1, как операция присваиванияobserved.b = 2
Время.oldValue
Так как этоa.b
,ЯвляетсяRef
Введите данные, а нового значения нет, а затем напрямую изменитеa.b
ценность . Тогда зачем возвращаться напрямую, без срабатывания триггера? потому что вref
В функции уже есть логика захвата сета (код выкладывать не буду).
Вопрос 2: Когда будетtarget !== toRaw(receiver)
?
В прежних представленияхreceiver
вроде какthis
То же самое существует, ссылаясь на прокси-объект, выполняемый Proxy. Прокси-объект используетtoRaw
Преобразование, то есть в исходный объект, естественно следуетtarget
конгруэнтны. Это включает в себя частичное знание Подробное введение см.MDN. Среди них говорится:
Получатель: объект, который был первоначально вызван. Обычно сам прокси, но метод set обработчика также может быть вызван косвенно в цепочке прототипов или иным образом (и, следовательно, не обязательно сам прокси)
В этом смысл комментариев в коде:
don't trigger if target is something up in the prototype chain of original.
Например вот так:
const child = new Proxy(
{},
{
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('child', receiver)
return true
}
}
)
const parent = new Proxy(
{ a: 10 },
{
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('parent', receiver)
return true
}
}
)
Object.setPrototypeOf(child, parent)
child.a = 4
// 打印结果
// parent Proxy {child: true, a: 4}
// Proxy {child: true, a: 4}
В этом случае родительский объектparent
изset
Он также сработает один раз, но пройденныйreceiver
обеchild
, и тогда измененные данные всегда былиchild
. при этих обстоятельствах,parent
На самом деле изменений нет, и, по логике вещей, он не должен запускать свою функцию слушателя.
Вопрос 3: Массив может обновлять данные через методы, какова логика мониторинга этого процесса?
Для объекта мы можем напрямую присваивать значения свойств, но как насчет массивов? еслиconst arr = []
, то можно либоarr[0] = 'value'
, также можноarr.push('value')
, но нет никогоtrap
угоняет толчок. Но когда вы на самом деле отлаживаете, вы обнаружитеpush
тоже срабатывает дваждыset
.
const proxy = new Proxy([], {
set(target, key, value, receiver) {
console.log(key, value, target[key])
return Reflect.set(target, key, value, receiver)
}
})
proxy.push(1)
// 0 1 undefined
// length 1 1
фактическиpush
Внутренняя логика заключается в том, чтобы сначала присвоить значение индексу, а затем установитьlength
, сработал дваждыset
. Однако есть еще одно явление, которое, хотяpush
принесlength
Действие срабатывает дваждыset
, но когда логика длины достигнута, старая длина уже является новым значением, так какvalue === oldValue
, на самом деле идет только один разtrigger
. но! еслиshift
илиunshift
, эта логика не выполняется, и если длина массива равна N,shift
|unshift
принесет N разtrigger
. Это на самом деле включаетArray
Я не могу объяснить лежащую в основе реализацию и спецификацииECMA-262оArray
соответствующие стандарты.
Однако здесь остается небольшая дырочка.shift
|unshift
так же какsplice
, что вызовет несколько триггеров эффектов. существуетreacivity
В системе пока не замечено соответствующей оптимизации. Конечно, в процессе использования vue@3,runtime-core
Он по-прежнему будет выполнять пакетные обновления для рендеринга.
то здесь,set
Мы также разобрались со своей логикой, за исключением привнесенного извнеtrigger
. Но мы знаем, что он запускает функцию слушателя при изменении данных, и мы увидим это позже.
Следующий шаг проще.
другие ловушки
// 劫持属性删除
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue })
} else {
trigger(target, OperationTypes.DELETE, key)
}
}
return result
}
// 劫持 in 操作符
function has(target: any, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, OperationTypes.HAS, key)
return result
}
// 劫持 Object.keys
function ownKeys(target: any): (string | number | symbol)[] {
track(target, OperationTypes.ITERATE)
return Reflect.ownKeys(target)
}
эти несколькоtrap
В принципе ничего сложного, видно с первого взгляда.
посмотри наконецreadonly
Особая логика:
readonly
export const readonlyHandlers: ProxyHandler<any> = {
// 创建get的trap
get: createGetter(true),
// set的trap
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
// 开发环境操作只读数据报警告。
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
// 如果不可变开关已关闭,则允许设置数据变更
return set(target, key, value, receiver)
}
},
// delete的trap,逻辑跟set差不多
deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(
key
)}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},
has,
ownKeys
}
readonly
Это тоже очень просто.createGetter
Логика была замечена раньше. Однако некоторые студенты, которые не пришли, могут подумать,get
Ловушка не меняет данные, зачем нужно следитьreactive
делать различие, проходитьisReadonly
Шерстяная ткань? Это связано с тем, что, как было сказано выше, черезget
При сборе зависимостей для данных вложенных объектов происходит отложенный захват, поэтому их можно передавать только прозрачно.isReadonly
, чтобы последующие захваченные дочерние объекты знали, должны ли они быть доступны только для чтения.
has
а такжеownKeys
Поскольку данные не изменяются и зависимости не собираются рекурсивно, нет необходимости отличать ее от логики переменных данных.
Прочитав его, мы можем получить общее представление о времени зависимого сбора и срабатывании функции прослушивателя.
краткое содержание
Сделаем краткий обзор baseHandles:
- Что касается исходных данных объекта, они будут перехвачены прокси-сервером и вернут новые ответные данные (прокси-данные).
- Для любых операций чтения и записи данных прокси
Refelct
Отражение на исходном объекте. - Во время этого процесса для операций чтения выполняется логика сбора зависимостей. Для операций записи срабатывает логика функции слушателя.
Подводя итог, это на самом деле довольно просто. Но мы так и не увидели обработчиков для сбора данных, это настоящая хард-кость.
collectionHandlers
Откройте этот файл и обнаружите, что этот файл больше, чемreactive
а такжеbaseHandlers
Может быть намного дольше. Неожиданно оказалось, что обработка этого типа данных, которые обычно не используются, вызывает наибольшие затруднения.
Зачем заниматься этим отдельно
Перед просмотром исходников собственно и возникнет вопрос, а почемуSet
|Map
|WeakMap
|WeakSet
Требуют ли эти данные специальной обработки? Отличаются ли они от других данных? Откроем файл и увидим этоhandlers
, и обнаружил, что это оказалось так:
export const mutableCollectionHandlers: ProxyHandler<any> = {
get: createInstrumentationGetter(mutableInstrumentations)
}
export const readonlyCollectionHandlers: ProxyHandler<any> = {
get: createInstrumentationGetter(readonlyInstrumentations)
}
Толькоget
,нетset
,has
Эти. Это невежество, сказал хороший угонset
а такжеget
Шерстяная ткань? почему бы не угнатьset
уже? Причина в том, что это невозможно, мы можем просто попробовать:
const set = new Set([1, 2, 3])
const proxy = new Proxy(set, {
get(target, key, receiver) {
console.log(target, key, receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(target, key, value, receiver)
return Reflect.set(target, key, value, receiver)
}
})
proxy.add(4)
Этот код выдаст ошибку, как только запустится:
Uncaught TypeError: Method Set.prototype.add called on incompatible receiver [object Object]
нашел только угонаset
, или напрямую импортированныеReflect
, отражающее поведениеtarget
включено, будет сообщено об ошибке. Почему это так? Это на самом делеMap|Set
Что касается их внутренней реализации, данные, которые они хранят внутри, должны передаваться черезthis
Для доступа это называется "внутренние слоты", а при работе через прокси-объект,this
По сути, это прокси, а не набор, поэтому он не может получить доступ к своим внутренним данным, а к массиву, в силу каких-то исторических причин, можно. Подробное объяснение можно найти здесьВведение в ограничения прокси. Обходной путь также упоминается в этой статье:
let map = new Map()
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments)
return typeof value == 'function' ? value.bind(target) : value
}
})
proxy.set('test', 1)
Общий принцип заключается в том, что при получении функцииthis
Привязка к исходному объекту, который вы хотите захватитьmap|set
. Это позволяет избежатьthis
указывая на проблему.
Тогда мы как бы понимаем, почемуcollection
Данные нуждаются в специальной обработке, только угнатьget
. Как это сделать? Давайте посмотрим на код.
По соглашению сначала посмотрите на ссылки и методы инструмента:
import { toRaw, reactive, readonly } from './reactive'
import { track, trigger } from './effect'
import { OperationTypes } from './operations'
import { LOCKED } from './lock'
import {
isObject,
capitalize, // 首字母转成大写
hasOwn
} from '@vue/shared'
// 将数据转为reactive数据,如果不是对象,则直接返回自身
const toReactive = (value: any) => (isObject(value) ? reactive(value) : value)
const toReadonly = (value: any) => (isObject(value) ? readonly(value) : value)
Эти ссылки, мы должны в основном понять, не глядя на комментарии, за исключением метода инструментаcapitalize
Вам нужно открыть его и посмотреть, вы можете понять это с первого взгляда. Затем нам нужно настроить порядок чтения, сначала взгляните на то, как передатьget
изtrap
, захват операции записи.
Инструментарий
// proxy handlers
export const mutableCollectionHandlers: ProxyHandler<any> = {
// 创建一个插桩getter
get: createInstrumentationGetter(mutableInstrumentations)
}
Во-первых, нам нужно прочитать имя его функции,createInstrumentationGetter
. Ну, студенты, плохо владеющие английским, как я, могут не очень хорошо его понимать.Instrumentation
что это значит. Вот что значит инструментарий. Я не буду много рассказывать об «инструментарии»: обычное покрытие одним тестом часто достигается с помощью инструментария.
В этом коде инструментирование относится к методу, внедренному в фрагмент кода, который имеет другие функции. Цель состоит в том, чтобы захватить эти методы и добавить соответствующую логику. Давайте посмотрим, как здесь выполняется «инструментирование» (захват).
// 可变数据插桩对象,以及一系列相应的插桩方法
const mutableInstrumentations: any = {
get(key: any) {
return get(this, key, toReactive)
},
get size() {
return size(this)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false)
}
// 迭代器相关的方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false)
readonlyInstrumentations[method] = createIterableMethod(method, true)
})
// 创建getter的函数
function createInstrumentationGetter(instrumentations: any) {
// 返回一个被插桩后的get
return function getInstrumented(
target: any,
key: string | symbol,
receiver: any
) {
// 如果有插桩对象中有此key,且目标对象也有此key,
// 那就用这个插桩对象做反射get的对象,否则用原始对象
target =
hasOwn(instrumentations, key) && key in target ? instrumentations : target
return Reflect.get(target, key, receiver)
}
}
Из вышеизложенного мы знаем, что, посколькуProxy
а такжеcollection
Собственные характеристики данных не могут быть захваченыset
Или прямое отражение. Итак, здесь создается новый объект, которыйset
а такжеmap
то же имя метода. Методы, соответствующие этим именам методов, являются методами сбора зависимостей и запуска ответов, введенными после инструментирования. затем пройтиReflect
При отражении этого инструментированного объекта получаются данные после инструментирования и вызывается метод после инструментирования.
А для некоторых пользовательских свойств или методовReflect
Рефлексия идет не после инструментации, а исходных данных, для этих случаев не выполняется отзывчивая логика, как в одиночном тесте:
it('should not observe custom property mutations', () => {
let dummy
const map: any = reactive(new Map())
effect(() => (dummy = map.customProp))
expect(dummy).toBe(undefined)
map.customProp = 'Hello World'
expect(dummy).toBe(undefined)
})
Инструментальные операции чтения
Следующий взгляд на этот плагинmutableInstrumentations
, сверху вниз, мы сначала смотрим наget
.
const mutableInstrumentations: any = {
get(key: any) {
// this 即是调用get的对象,现实情况就是Proxy代理对象
// toReactive是一个将数据转为响应式数据的方法
return get(this, key, toReactive)
}
// ...省略其他
}
function get(target: any, key: any, wrap: (t: any) => any): any {
// 获取原始数据
target = toRaw(target)
// 由于Map可以用对象做key,所以key也有可能是个响应式数据,先转为原始数据
key = toRaw(key)
// 获取原始数据的原型对象
const proto: any = Reflect.getPrototypeOf(target)
// 收集依赖
track(target, OperationTypes.GET, key)
// 使用原型方法,通过原始数据去获得该key的值。
const res = proto.get.call(target, key)
// wrap 即传入的toReceive方法,将获取的value值转为响应式数据
return wrap(res)
}
Примечание: вget
метод, первый параметрtarget
не могу следоватьProxy
Первый параметр конструктора запутан.Proxy
Первый параметр функцииtarget
Относится к необработанным данным. пока вget
метод, этоtarget
По сути, это данные после проксирования. то естьReflect.get(target, key, receiver)
серединаreceiver
.
Тогда нам более понятно, суть метод прототипа через исходные данные +call this
, что позволяет избежать вышеуказанных проблем и возвращает реальные данные.
const mutableInstrumentations: any = {
// ...
get size() {
return size(this)
},
has
// ...
}
function size(target: any) {
// 获取原始数据
target = toRaw(target)
const proto = Reflect.getPrototypeOf(target)
track(target, OperationTypes.ITERATE)
return Reflect.get(proto, 'size', target)
}
function has(this: any, key: any): boolean {
// 获取原始数据
const target = toRaw(this)
key = toRaw(key)
const proto: any = Reflect.getPrototypeOf(target)
track(target, OperationTypes.HAS, key)
return proto.has.call(target, key)
}
size
а такжеhas
, это все "проверочная" логика. Толькоsize
это свойство, а не метод, поэтому его нужно начинать сget size()
способ угона. а такжеhas
Это метод, который не требует специальной привязки, и внутренняя логика обоих также проста.get
В основном то же самое. Но вот небольшая подробность о TypeScript.has
Первый параметр функцииthis
, это в тс естьподдельный параметр, когда эта функция действительно вызывается, ее не нужно передавать, поэтому она по-прежнему используется вот такsomeMap.has(key)
Просто хорошо.
В дополнение к этим двум методам проверки существуют также методы «проверки», связанные с итератором.
Инструментальные итераторы
Если вы мало знаете об итераторах, рекомендуется сначала прочитать соответствующие документы, такие какMDN.
// 迭代器相关的方法
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method] = createIterableMethod(method, false)
})
function createIterableMethod(method: string | symbol, isReadonly: boolean) {
return function(this: any, ...args: any[]) {
// 获取原始数据
const target = toRaw(this)
// 获取原型
const proto: any = Reflect.getPrototypeOf(target)
// 如果是entries方法,或者是map的迭代方法的话,isPair为true
// 这种情况下,迭代器方法的返回的是一个[key, value]的结构
const isPair =
method === 'entries' ||
(method === Symbol.iterator && target instanceof Map)
// 调用原型链上的相应迭代器方法
const innerIterator = proto[method].apply(target, args)
// 获取相应的转成响应数据的方法
const wrap = isReadonly ? toReadonly : toReactive
// 收集依赖
track(target, OperationTypes.ITERATE)
// return a wrapped iterator which returns observed versions of the
// values emitted from the real iterator
// 给返回的innerIterator插桩,将其value值转为响应式数据
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
return done
? // 为done的时候,value是最后一个值的next,是undefined,没必要做响应式转换了
{ value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol
[Symbol.iterator]() {
return this
}
}
}
}
Эта логика на самом деле в порядке, ядро в том, чтобы захватить метод итератора,next
Возвращаемое значение используетсяreactive
трансформировать. Единственное, что делает людей неясными, это то, чтоIterator
так же какMap|Set
незнакомый. Если вы действительно незнакомы, рекомендуется сначала просмотреть соответствующие документы.
Есть еще один, связанный с итераторомforEach
метод.
function createForEach(isReadonly: boolean) {
// 这个this,我们已经知道了是假参数,也就是forEach的调用者
return function forEach(this: any, callback: Function, thisArg?: any) {
const observed = this
const target = toRaw(observed)
const proto: any = Reflect.getPrototypeOf(target)
const wrap = isReadonly ? toReadonly : toReactive
track(target, OperationTypes.ITERATE)
// important: create sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
// 将传递进来的callback方法插桩,让传入callback的数据,转为响应式数据
function wrappedCallback(value: any, key: any) {
// forEach使用的数据,转为响应式数据
return callback.call(observed, wrap(value), wrap(key), observed)
}
return proto.forEach.call(target, wrappedCallback, thisArg)
}
}
forEach
Логика несложная, аналогичная части с итератором выше, она также перехватывает метод и возвращает исходные данные параметра в ответные данные.
Инструментальные операции записи
Затем посмотрите на операцию записи.
function add(this: any, value: any) {
// 获取原始数据
value = toRaw(value)
const target = toRaw(this)
// 获取原型
const proto: any = Reflect.getPrototypeOf(this)
// 通过原型方法,判断是否有这个key
const hadKey = proto.has.call(target, value)
// 通过原型方法,增加这个key
const result = proto.add.call(target, value)
// 原本没有key的话,说明真的是新增,则触发监听响应逻辑
if (!hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.ADD, value, { value })
} else {
trigger(target, OperationTypes.ADD, value)
}
}
return result
}
Мы обнаружили, что операция записи намного проще, фактически такая же, как иbaseHandlers
Логика аналогичная, за исключением того, что для техbase
данные, черезReflect
Удобное поведение отражения, а здесь нужно вручную получить цепочку прототипов и привязатьthis
Вот и все. Проверитьset
а такжеdeleteEntry
Код и логика аналогичны, поэтому я не буду вдаваться в подробности.
оreadonly
Сопутствующие тоже очень простые, и код больше не выкладываю, просто увеличу количество слов в статье. Так и будетadd|set|delete|clear
Эти методы написания завернуты в другой слой, и в среде разработки выдается предупреждение.
Вот, наконец, прочтитеcollcetionsHandlers
вся логика.
краткое содержание
Подводя итог еще раз, как он перехватывает данные коллекции.
- из-за
Set|Map
Основные проблемы дизайна таких данных сбора,Proxy
нельзя напрямую захватитьset
или пряморефлекторное поведение. - захват необработанных данных коллекции
get
, для его исходного метода или свойства,Reflect
Отражает на инструмент, в противном случае отражает исходный объект. - Метод вставки сначала пройдет
toRaw
, получить исходные данные прокси-данных, затем получить метод прототипа исходных данных, а затем привязатьthis
Для исходных данных вызовите соответствующий метод. - для
getter|has
Этот тип метода запроса вставляет логику для сбора зависимостей и преобразует возвращаемое значение в реактивные данные (has возвращает логическое значение, поэтому преобразование не требуется). - Для методов запросов, связанных с итератором, вставляется логика зависимости коллекции, а данные итеративного процесса преобразуются в реагирующие данные.
- Для методов, связанных с операциями записи, вставьте логику, запускающую мониторинг.
На самом деле принцип по-прежнему прост для понимания, но написать его труднее.
Суммировать
Пока, наконецreactive
Логика полностью завершена. Читать код в этой части немного сложно, потому что задействовано много базовых знаний, иначе вы будете везде путаться, но это тоже своего рода обучение, и процесс исследования довольно интересен.
В процессе мы обнаружили, что перехват массивов все еще немного недостаточен, прямое отражение в некоторых случаях будет многократно запускать функцию слушателя. прочувствовать что-то вродеcollection
Способ обработки данных может быть решен. Но это увеличивает сложность программы, и я не знаю, будут ли еще какие-то ямки.
Кроме того, мы обнаружили, что чтениеreactivity
Когда дело доходит до связанного кода, ts не включает в себя столько, сколько мы себе представляли, а во многих случаях и вовсе любой, но это нужно рассматривать диалектически. Прежде всего, как сказал Сяою, «Эти данные являются пользовательскими данными, и они сами по себе любые. Неохотно декларировать бессмысленно». И есть много дженериков и производных по пути, и стоимость очень высока. Во всяком случае, я пробовал это сам и ничего не мог с этим поделать. Кроме того, текущий код все еще находится в неофициальной стадии, если он слишком громоздкий в обслуживании. Для такого нерешительного человека, как я, если вы действительно хотите внести немного больше кода, это также сложно.
Эта статья немного сложна, если вы читаете ее медленно, большое спасибо за чтение~~
Следующий последнийeffect
Соответствующий анализ исходного кода, наконец, способный разгадать началоtargetMap
тайна, см.track
а такжеtrigger
Внутренняя реализация , собрала последний кусочек головоломки.