Статья впервые опубликованаGithub
Проблемы с традиционной глубокой копией
В JS есть важный тип, называемый ссылочным типом. В процессе использования этого типа, поскольку переданное значение является ссылкой, могут возникать некоторые побочные эффекты, такие как:
let a = { age: 1 }
let b = a
b.age = 2
Написание приведенного выше кода приведет кa
иb
свойства изменены. Всем точно не нужна такая ситуация в повседневной разработке, поэтому они будут использовать какие-то средства для отключения своих эталонных соединений. Для приведенной выше структуры данных поверхностное копирование может решить нашу проблему.
let b = { ...a }
b.age = 2
Однако поверхностная копия может отключить только один слой ссылок. Если структура данных представляет собой многоуровневый объект, поверхностная копия не может решить проблему. В настоящее время нам нужно использовать глубокую копию.
Практика глубокого копирования обычно делится на два типа:
JSON.parse(JSON.stringify(a))
- рекурсивная неглубокая копия
Первый метод имеет некоторые ограничения и не может быть использован во многих случаях, поэтому здесь он упоминаться не будет; второй метод — это, как правило, реализация функций глубокого копирования в библиотеке инструментов, например, в loadash.cloneDeep
. Хотя этот подход может устранить ограничения первого подхода, он не очень хорошо работает с большими данными, поскольку необходимо пройти весь объект.
Итак, есть ли способ понять, что только при изменении атрибута можно сделать глубокую копию этой части данных и решить проблему?JSON.parse(JSON.stringify(a))
ограничения. Такая практика, конечно, существует, вопрос только в том, как узнать, какие свойства изменил пользователь?
ответProxy
, перехвативset
иget
можем добиться того, чего хотим, конечноObject.defineProperty()
Также может. фактическиImmerЭта библиотека использует этот подход для генерации неизменяемых объектов, давайте попробуем передатьProxy
Для достижения высокопроизводительной версии глубокой копии.
выполнить
Давайте сначала поговорим об общей основной идее, на самом деле, есть три пункта:
- перехватывать
set
, все присвоения выполняются в копии (объект исходных данных неглубокой копии), так что исходный объект не будет затронут - перехватывать
get
, взять значение из копии или исходных данных по логике того, изменен ли атрибут или нет - Наконец, когда создается неизменяемый объект, исходный объект просматривается, чтобы определить, было ли изменено свойство, то есть определить, существует ли копия. Если оно не было изменено, будет возвращено исходное свойство, и больше нет необходимости перемещаться по объекту вложенного свойства, что повышает производительность. Если он был изменен, вам нужно назначить копию новому объекту и рекурсивно пройти
Следующим шагом является реализация, так как мы используемProxy
реализации, то он должен сгенерироватьProxy
объект, поэтому мы сначала реализуем генерациюProxy
функцию объекта.
// 用于判断是否为 proxy 对象
const isProxy = value => !!value && !!value[MY_IMMER]
// 存放生成的 proxy 对象
const proxies = new Map()
const getProxy = data => {
if (isProxy(data)) {
return data
}
if (isPlainObject(data) || Array.isArray(data)) {
if (proxies.has(data)) {
return proxies.get(data)
}
const proxy = new Proxy(data, objectTraps)
proxies.set(data, proxy)
return proxy
}
return data
}
- В первую очередь нам нужно определить, является ли входящий атрибут уже прокси-объектом, и если да, то его можно вернуть напрямую. Суть суждения здесь заключается в
value[MY_IMMER]
, потому что наш пользовательский перехват будет запускаться только тогда, когда это прокси-объектget
функция, в функции перехвата, чтобы определить, еслиkey
даMY_IMMER
возвращениеtarget
- Далее нам нужно определить, является ли параметр нормальным
Object
Сконструированный объект или массив,isPlainObject
В интернете много реализаций, поэтому я не буду здесь выкладывать код, если вам интересно, вы можете прочитать исходный код в конце статьи. - Наконец, нам нужно определить, был ли создан соответствующий прокси, и если да, то непосредственно из
Map
Влезть могут, иначе новое творение. Обратите внимание, что прокси-объект — это хранилище контейнеров.Map
Вместо обычного объекта это связано с тем, что если он хранится в обычном объекте, он появится при извлечении значения.взрывной стек, а конкретные причины можете придумать сами🤔
Далее нам нужно реализовать функцию перехвата прокси.Вот две основные идеи, упомянутые выше.
// 注意这里还是用到了 Map,原理和上文说的一致
const copies = new Map()
const objectTraps = {
get(target, key) {
if (key === MY_IMMER) return target
const data = copies.get(target) || target
return getProxy(data[key])
},
set(target, key, val) {
const copy = getCopy(target)
const newValue = getProxy(val)
// 这里的判断用于拿 proxy 的 target
// 否则直接 copy[key] = newValue 的话外部拿到的对象是个 proxy
copy[key] = isProxy(newValue) ? newValue[MY_IMMER] : newValue
return true
}
}
const getCopy = data => {
if (copies.has(data)) {
return copies.get(data)
}
const copy = Array.isArray(data) ? data.slice() : { ...data }
copies.set(data, copy)
return copy
}
- перехватывать
get
сначала надо судитьkey
Да или нетMY_IMMER
, если да, то это означает, что объект, к которому осуществляется доступ в данный момент, являетсяproxy
, нам нужно поставить правильныйtarget
Вернитесь назад. Тогда это нормальное возвращаемое значение, если копия существует, вернуть копию, в противном случае вернуть исходные данные. - перехватывать
set
Когда первым шагом должно быть создание копии, потому что нам всем нужно выполнять операции присваивания копии, иначе это повлияет на исходные данные. Тогда прокси-объект не может быть назначен при присвоении значения в копии, иначе окончательный сгенерированный неизменяемый объект будет иметь прокси-объект памяти, поэтому здесь нам нужно судить, является ли он прокси-объектом. - Логика создания копии очень проста, то есть определить тип данных и затем выполнить операцию поверхностного копирования
Наконец, логика создания неизменяемых объектов
const isChange = data => {
if (proxies.has(data) || copies.has(data)) return true
}
const finalize = data => {
if (isPlainObject(data) || Array.isArray(data)) {
if (!isChange(data)) {
return data
}
const copy = getCopy(data)
Object.keys(copy).forEach(key => {
copy[key] = finalize(copy[key])
})
return copy
}
return data
}
Логика тут собственно описана выше, то есть определить были ли изменены входящие параметры. Если он не был изменен, то он сразу вернется к исходным данным и остановит обход этой ветки, если был изменен, то возьмет значение из копии, а затем выполнит атрибуты во всей копии.finalize
функция.
Последний шаг — объединить все функции, упомянутые выше.
function produce(baseState, fn) {
// ...
const proxy = getProxy(baseState)
fn(proxy)
return finalize(baseState)
}
Вышеизложенное является реализацией всей идеи, давайте проверим, может ли функция, которую мы хотим, реализовать нормально.
const state = {
info: {
name: 'yck',
career: {
first: {
name: '111'
}
}
},
data: [1]
}
const data = produce(state, draftState => {
draftState.info.age = 26
draftState.info.career.first.name = '222'
})
console.log(data, state)
console.log(data.data === state.data)
Из значения, напечатанного приведенным выше кодом, мы можем видетьdata
иstate
Больше не та же ссылка, изменитеdata
Не вносит изменений в исходные данные, а также реализует только поверхностные копии измененных свойств. в объектеdata
Поскольку свойство не было изменено,data
Все та же ссылка, чтобы добиться разделения структуры.
Наконец
надетьАдрес источника, на самом деле, immer — это гораздо больше, чем эти коды реализации, и будет больше проверок данных и суждений о совместимости.Код в этой статье больше для предоставления другой идеи реализации глубокого копирования.
Если у вас есть какие-либо вопросы или другие вопросы, вы можете общаться в области комментариев.