Автор: Чжан Чжао
Использование объектов в JS требует особого внимания к проблеме ссылки, а способ отсечь ссылку часто представляет собой глубокую копию. Но глубокое копирование относительно дорого. В этой статье в основном представлены immutable-js и immer, две библиотеки, которые работают с «неизменяемыми данными», и кратко анализируется реализация immer. Наконец, с помощью тестовых данных сравниваются и обобщаются преимущества и недостатки immutable-js и immer.
предисловие
Типы переменных в JS можно разделить на базовые типы и ссылочные типы.
В процессе использования ссылочные типы часто имеют некоторые нераспознанные побочные эффекты, поэтому в современном процессе разработки JS опытные разработчики будут сознательно записывать ссылочные неизменяемые типы данных в определенных местах.
// 因为引用所带来的副作用:
var a = [{ val: 1 }]
var b = a.map(item => item.val = 2)
// 期望:b 的每一个元素的 val 值变为 2,但最终 a 里面每个元素的 val 也变为了 2
console.log(a[0].val) // 2
Из приведенного выше примера мы можем обнаружить, что первоначальное намерение состояло в том, чтобы изменить значение каждого элемента в b только на 2, но непреднамеренно изменить результат каждого элемента в a, что, конечно, не соответствует ожиданиям. Далее, если a где-то используется, легко получить некоторые непредсказуемые и трудно отлаживаемые ошибки (поскольку его значение непреднамеренно изменено).
После обнаружения такой проблемы решение также простое. Вообще говоря, когда нам нужно передать переменную ссылочного типа (например, объект) в функцию, мы можем использовать Object.assign или ... для деконструкции объекта и успешного разрыва слоя ссылок. Например, приведенную выше задачу можно записать следующим образом:
var a = [{ val: 1 }]
var b = a.map(item => ({ ...item, val: 2 }))
console.log(a[0].val) // 1
console.log(b[0].val) // 2
Но при этом будет другая проблема.Будь то операция деструктурирования Object.assign или ... , сломанная ссылка - это только один уровень.Если объект вложен более чем на один уровень, все равно существует определенный риск.
// 深层次的对象嵌套,这里 a 里面的元素对象下又嵌套了一个 desc 对象
var a = [{
val: 1,
desc: { text: 'a' }
}]
var b = a.map(item => ({ ...item, val: 2 }))
console.log(a === b) // false
console.log(a[0].desc === b[0].desc) // true
b[0].desc.text = 'b'; // 改变 b 中对象元素对象下的内容
console.log(a[0].desc.text); // b (a 中元素的值无意中被改变了)
Результат выражения a[0].desc === b[0].desc по-прежнему верен, а это означает, что внутри программы a[0].desc и b[0].desc по-прежнему указывают на одну и ту же ссылку. Если следующий код случайно присвоит значение непосредственно через b[0].desc внутри функции, он обязательно изменит результат части a[0].desc с той же ссылкой. Например, в приведенном выше примере вложенный объект в b напрямую управляется «точкой», что в конечном итоге изменяет результат в a.
глубокая копия
Так что после этого в большинстве случаев мы будем рассматривать операции глубокого копирования, чтобы полностью избежать всех проблем, с которыми столкнулись выше. Глубокая копия, как следует из названия, заключается в том, что в процессе обхода, если встречается тип данных, на который можно ссылаться (например, объект Object в предыдущем примере), новый тип будет создан рекурсивно и полностью.
// 一个简单的深拷贝函数,只做了简单的判断
// 用户态这里输入的 obj 一定是一个 Plain Object,并且所有 value 也是 Plain Object
function deepClone(obj) {
const keys = Object.keys(obj)
return keys.reduce((memo, current) => {
const value = obj[current]
if (typeof value === 'object') {
// 如果当前结果是一个对象,那我们就继续递归这个结果
return {
...memo,
[current]: deepClone(value),
}
}
return {
...memo,
[current]: value,
}
}, {})
}
Простой тест с функцией deepClone выше
var a = {
val: 1,
desc: {
text: 'a',
},
}
var b = deepClone(a)
b.val = 2
console.log(a.val) // 1
console.log(b.val) // 2
b.desc.text = 'b'
console.log(a.desc.text) // 'a'
console.log(b.desc.text) // 'b'
Вышеупомянутый deepClone может удовлетворить простые потребности, но в реальной производственной работе нам нужно учитывать множество факторов.
Например:
- Как быть с геттером, сеттером и содержимым цепочки прототипов в ключе?
- значение является символом, как с этим бороться?
- Что, если значение является другим непростым объектом?
- Как быть с некоторыми циклическими ссылками внутри значения?
Поскольку существует слишком много неопределенных факторов, в реальной инженерной практике рекомендуется использовать функции инструмента в больших проектах с открытым исходным кодом. Наиболее часто используемым и известным является lodash.cloneDeep, безопасность и эффект гарантированы.
immutable
Эта концепция данных, которая устраняет побочные эффекты ссылочных типов данных, называется неизменяемыми, что означает неизменяемые данные.На самом деле, более уместно понимать неизменные отношения. Всякий раз, когда мы создаем данные, которые были deepClone, операции с побочными эффектами на новых данных не будут влиять на предыдущие данные, что является сущностью и сущностью неизменяемости.
Побочные эффекты здесь не ограничиваются назначением свойств с помощью «точечных» операций. Например, такие методы, как push, pop и splice в массиве, изменят исходный результат массива, и эти операции считаются неизменяемыми. Напротив, slice, map и другие операции, которые возвращают результат нового массива, являются операциями immutable.
Однако, хотя функция deepClone обрезает ссылочную связь и реализует неизменяемость, она относительно дорогая (поскольку она эквивалентна полному созданию нового объекта, и иногда мы не будем выполнять операции присваивания для некоторых значений, поэтому даже если мы сохраним ссылку не имеет значения).
Поэтому в 2014 году родился immutable-js Facebook, который обеспечивает неизменяемость между данными, оценивает эталонную ситуацию между данными во время выполнения и учитывает производительность.
Введение в immutable-js
immutable-js использует другой API структуры данных, который немного отличается от наших обычных операций.Он преобразует все нативные типы данных (Object, Array и т. д.) во внутренние объекты immutable-js (Map, List и т. д.) и любую операцию в конечном итоге вернет новое неизменное значение.
Приведенный выше пример с использованием immutable-js необходимо изменить следующим образом:
const { fromJS } = require('immutable')
const data = {
val: 1,
desc: {
text: 'a',
},
}
// 这里使用 fromJS 将 data 转变为 immutable 内部对象
const a = fromJS(data)
// 之后我们就可以调用内部对象上的方法如 get getIn set setIn 等,来操作原对象上的值
const b = a.set('val', 2)
console.log(a.get('val')) // 1
console.log(b.get('val')) // 2
const pathToText = ['desc', 'text']
const c = a.setIn([...pathToText], 'c')
console.log(a.getIn([...pathToText])) // 'a'
console.log(c.getIn([...pathToText])) // 'c'
С точки зрения производительности immutable-js также имеет свои преимущества, если привести простой пример:
const { fromJS } = require('immutable')
const data = {
content: {
time: '2018-02-01',
val: 'Hello World',
},
desc: {
text: 'a',
},
}
// 把 data 转化为 immutable-js 中的内置对象
const a = fromJS(data)
const b = a.setIn(['desc', 'text'], 'b')
console.log(b.get('desc') === a.get('desc')) // false
// content 的值没有改动过,所以 a 和 b 的 content 还保持着引用
console.log(b.get('content') === a.get('content')) // true
// 将 immutable-js 的内置对象又转化为 JS 原生的内容
const c = a.toJS()
const d = b.toJS()
// 这时我们发现所有的引用都断开了
console.log(c.desc === d.desc) // false
console.log(c.content === d.content) // false
Как видно из приведенного выше примера, в процессе манипулирования встроенным объектом immutable-js содержимое под объектом desc было изменено. Но на самом деле мы не изменили результат содержания. В процессе сравнения через === мы можем обнаружить, что ссылка на desc была отключена, но ссылка на контент осталась подключенной.
В структуре данных immutable-js объекты глубокого уровня по-прежнему могут гарантировать строгое равенство без изменений, что является еще одной особенностью immutable-js «структурное совместное использование глубоко вложенных объектов». То есть вложенный объект по-прежнему сохраняет внутри себя предыдущую ссылку до того, как он будет изменен, и после модификации ссылка прерывается, но это не повлияет на предыдущий результат.
Учащиеся, которые часто используют React, также должны быть знакомы с immutable-js, что является одной из причин, почему immutable-js может значительно повысить производительность страниц React.
Конечно, есть не только несколько примеров, которые могут добиться неизменяемых эффектов, В этой статье я в основном хочу представить библиотеку, которая реализует неизменяемость, а на самом деле immer.
Введение в иммерсию
Автор immer также является автором mobx. Mobx, кажется, интегрирует набор вещей Vue в React, и это получило хороший отклик в сообществе. immer — еще одна практика, которую он использует в immutable.
Самое большое отличие от immutable-js заключается в том, что immer использует API собственной структуры данных вместо использования встроенного API после преобразования его во встроенный объект, такой как immutable-js, Вот простой пример:
const produce = require('immer')
const state = { done: false, val: 'string', }
// Все операции с побочными эффектами можно поместить во второй параметр функции product // Окончательный возвращаемый результат не влияет на исходные данные const newState = произвести (состояние, (черновик) => { черновик.готово = правда })
console.log(state.done) // false console.log(newState.done) // true
Из приведенного выше примера мы можем обнаружить, что всю логику с побочными эффектами можно поместить в функцию второго параметра продукта для обработки. Любая операция с исходными данными внутри этой функции не окажет никакого влияния на исходный объект.
Здесь мы можем выполнять любые операции в функции, такие как push splice и другие неизменяемые API, и конечный результат не влияет на исходные данные.
Самое большое преимущество Immer здесь. Наше обучение не требует больших затрат, потому что у него мало API. Это не что иное, как размещение наших предыдущих операций во втором параметре функции продукта для выполнения.
Анализ принципа погружения
В исходном коде Immer используется новая функция ES6 — объект Proxy. Прокси-объекты позволяют перехватывать определенные операции и реализовывать настраиваемое поведение, но большинство студентов, изучающих JS, могут не часто использовать этот шаблон метапрограммирования в своей повседневной работе, поэтому вот краткое введение в его использование.
Proxy
Объект Proxy принимает два параметра, первый параметр - это объект, которым нужно оперировать, а второй параметр - установить свойство, соответствующее перехвату. Свойство здесь также поддерживает get, set и т. д., то есть захват чтения и запись соответствующего элемента, может выполнять в нем какие-то операции и, наконец, возвращать экземпляр объекта Proxy.
const proxy = new Proxy({}, {
get(target, key) {
// 这里的 target 就是 Proxy 的第一个参数对象
console.log('proxy get key', key)
},
set(target, key, value) {
console.log('value', value)
}
})
// 所有读取操作都被转发到了 get 方法内部
proxy.info // 'proxy get key info'
// 所有设置操作都被转发到了 set 方法内部
proxy.info = 1 // 'value 1'
Первый параметр, переданный в приведенном выше примере, — это пустой объект.Конечно, мы можем заменить его другими объектами с существующим содержимым, то есть целью в параметре функции.
прокси в иммере
Подход Иммера заключается в том, чтобы поддерживать состояние внутри, перехватывать все операции и внутренне определять, есть ли изменение, и в конечном итоге решать, как вернуться. Следующий пример представляет собой конструктор.Если вы передаете его экземпляр объекту Proxy в качестве первого параметра, вы можете использовать методы в более позднем объекте обработки:
class Store {
constructor(state) {
this.modified = false
this.source = state
this.copy = null
}
get(key) {
if (!this.modified) return this.source[key]
return this.copy[key]
}
set(key, value) {
if (!this.modified) this.modifing()
return this.copy[key] = value
}
modifing() {
if (this.modified) return
this.modified = true
// 这里使用原生的 API 实现一层 immutable,
// 数组使用 slice 则会创建一个新数组。对象则使用解构
this.copy = Array.isArray(this.source)
? this.source.slice()
: { ...this.source }
}
}
Вышеупомянутый конструктор Store пропускает много частей суждения по сравнению с исходным кодом. Экземпляр имеет три атрибута: модифицированный, источник и копия, а также три метода: получение, установка и изменение. Как встроенный флаг, измененный определяет, как установить и вернуть.
Самое главное в нем должна быть функция модификации, если сеттер сработал и он до этого не менялся, флаг модифицирования будет вручную установлен в true, а слой иммутабельности будет реализовываться вручную через нативный API.
Для второго параметра Proxy в упрощенной реализации мы просто делаем слой переадресации, и любое чтение и запись элементов перенаправляется во внутренние методы экземпляра хранилища для обработки.
const PROXY_FLAG = '@@SYMBOL_PROXY_FLAG'
const handler = {
get(target, key) {
// 如果遇到了这个 flag 我们直接返回我们操作的 target
if (key === PROXY_FLAG) return target
return target.get(key)
},
set(target, key, value) {
return target.set(key, value)
},
}
Цель добавления флага в геттер здесь состоит в том, чтобы в будущем было удобнее получать экземпляр хранилища из прокси-объекта.
Наконец, мы можем завершить функцию создания, создать экземпляр прокси после создания экземпляра хранилища. Затем передайте созданный экземпляр прокси во вторую функцию. Таким образом, независимо от того, что вы делаете внутри с побочными эффектами, в конечном итоге они будут разрешены внутри экземпляра хранилища. В конце концов получается измененный прокси-объект, и прокси-объект поддерживает внутри себя два состояния, оценивая измененное значение и определяя, какое из них будет возвращено.
function produce(state, producer) {
const store = new Store(state)
const proxy = new Proxy(store, handler)
// 执行我们传入的 producer 函数,我们实际操作的都是 proxy 实例,所有有副作用的操作都会在 proxy 内部进行判断,是否最终要对 store 进行改动。
producer(proxy)
// 处理完成之后,通过 flag 拿到 store 实例
const newState = proxy[PROXY_FLAG]
if (newState.modified) return newState.copy
return newState.source
}
Таким образом завершается самая простая версия из трех модулей, разделенных на конструктор хранилища, обработчик объекта обработки и состояние обработки продукции.Объединение их представляет собой самую малюсенькую версию иммерса, которая удаляет множество ненужных калибровочных, тестовых и избыточных переменных. Но у настоящего иммера есть и другие внутренние функции, такие как структурное совместное использование глубоко вложенных объектов, упомянутых выше, и так далее.
Конечно, как новый API, Proxy поддерживается не во всех средах, и Proxy не может быть заполнен полифиллом, поэтому immer использует Object.defineProperty для совместимости со средами, которые не поддерживают Proxy.
представление
Давайте проверим производительность иммера на практике с помощью простого теста. В этом тесте используется дерево состояний со 100 000 состояний, и мы записали время работы с 10 000 данных для сравнения.
замораживание означает, что дерево состояний заморожено и не может продолжать работать после его создания. Для обычных JS-объектов мы можем использовать Object.freeze для заморозки сгенерированного нами объекта дерева состояний, конечно же, как и у immer/immutable-js внутри есть свой метод замораживания и логика.
Конкретные тестовые файлы можно просмотреть, нажав здесь:GitHub.com/IM Marchis/IM Mai…performance_tests/add-data.js
Вот примеры того, что представляет каждая абсцисса:
- просто мутируйте: работайте напрямую через собственные операции, а замораживание напрямую вызывает Object.freeze для замораживания всего объекта.
- deepclone: Скопируйте исходные данные через глубокую копию.Время после замораживания относится к времени, когда объект глубокой копии заморожен.
- Редуктор: относится к нашей ручной обработке наших данных с помощью собственных неизменяемых API, таких как ... или Object.assign, Время после замораживания представляет собой время, когда мы замораживаем новый контент, который мы создали.
- Immutable js: означает, что мы манипулируем данными через immutable-js. toJS относится к преобразованию встроенных объектов immutable-js в собственный контент js.
- Immer: протестированы данные с прокси-сервером и без него с использованием defineProperty.
Наблюдая за приведенным выше рисунком, можно в основном сделать следующие результаты сравнения:
- С точки зрения mutate и deepclone, эталонный тест mutate определяет базовый уровень затрат на изменение данных, а глубокое копирование deepclone не имеет разделения структуры, поэтому эффективность будет намного хуже.
- Использование прокси-иммера примерно в два раза больше, чем написанного от руки редюсера, хотя на практике это незначительно.
- immer примерно так же быстр, как immutable-js. Однако immutable-js часто требует операций toJS в конце, и потери производительности здесь очень велики. Например, преобразование неизменяемых объектов JS обратно в обычные объекты, передача их в компоненты или передача их по сети и т. д. (есть также предварительные затраты на преобразование данных, полученных, например, с сервера, во встроенные объекты immutable-js).
- Версия immer для ES5 использует для реализации defineProperty, что значительно медленнее в тестировании. Поэтому попробуйте использовать immer в средах, поддерживающих прокси.
- В версии замораживания только mutate, deepclone и нативные редукторы могут рекурсивно заморозить полное дерево состояний, в то время как другие тестовые примеры замораживают только измененную часть дерева.
Суммировать
Из приведенного выше примера мы также можем суммировать преимущества и недостатки сравнения immutable-js и immer:
- Immer API очень прост, его почти легко начать, а миграция и преобразование проекта также относительно просты. С Immutable-js гораздо сложнее начать работу, а миграция или трансформация проектов с использованием immutable-js будет немного сложнее.
- Immer требует, чтобы среда поддерживала Proxy и defineProperty, иначе его нельзя будет использовать. Но immutable-js поддерживает компиляцию в код ES3, подходящий для всех сред JS.
- На эффективность работы Immer сильно влияют факторы окружающей среды. Эффективность immutable-js в целом относительно стабильна, но fromJS и toJS должны выполняться первыми в процессе преобразования, поэтому требуется часть затрат на раннюю эффективность.
Наконец: команда коммерческого интерфейса ByteDance набирает сотрудников! Вы можете каждый день говорить о своих идеалах с техническими лидерами мнений, участвовать в обмене и обмене техническими гигантами, наслаждаться четырехразовым питанием, лучшим гиковским оборудованием, бесплатными тренажерными залами и заниматься сложными делами с отличными людьми.Где вы можете найти такое место? ? Присоединяйтесь к нам!
Электронная почта для доставки резюме: zhangzhao.loatheb@bytedance.com