Что меня не устраивало
Библиотеки react-redux и redux-saga просты, гибки и удобны, однако имеют избыточность кода. Основные элементы это:
Фабрики событий
const actionCreatorFactory = type => payload => ({ type, payload }); export const INITIALIZE = 'INITIALIZE'; export const ON_FIELD_CHANGE = 'ON_FIELD_CHANGE'; export const onFieldChange = actionCreatorFactory(ON_FIELD_CHANGE); export const HANDLE_FIELD = 'HANDLE_FIELD'; export const handleField = actionCreatorFactory(HANDLE_FIELD); export const GO_TO_NEXT_STEP = 'GO_TO_NEXT_STEP'; export const goToNextStep = actionCreatorFactory(GO_TO_NEXT_STEP);
В таком виде меня смущает несколько вещей:
— описание типов событий. В этом примере можно конечно обойтись и без констант, но все равно придется передавать его тип в фабрику, которое будет идентично имени созданного события в верблюжей(camelCase) нотификации.
— если вы забыли структуру пайлоада(payload), что бы его вспомнить, надо перейти к reducer/saga, где используется это событие, и посмотреть что там нужно передавать
Редьюсеры
import getInitialState, { formDataInitialState as initialState, } from '../helpers/initialState'; import { HANDLE_FIELD_DONE, ON_FIELD_CHANGE, RESET } from '../actionCreators'; export default (state = initialState, { type, payload }) => { switch (type) { case RESET: { return getInitialState().formDataInitialState; } case ON_FIELD_CHANGE: { const { name } = payload; return { ...state, [name]: '', } } case HANDLE_FIELD_DONE: { const { name, value } = payload; return { ...state, [name]: value, } } } return state; };
Тут в целом напрягает только использование конструкции switchСаги
import { all, put, select, fork, takeEvery } from 'redux-saga/effects'; import { runServerSideValidation } from '../actionCreators'; import { HANDLE_FIELD } from '../actionCreators'; function* takeHandleFieldAction() { yield takeEvery(HANDLE_FIELD, function*({ payload }) { const { validation, formData } = yield select( ({ validation, formData }) => ({ validation: validation[payload.name], formData, }) ); const valueFromState = formData[payload.name]; if (payload.value !== valueFromState) { const { name, value } = payload; const { validator } = validation.serverValidator; yield put( runServerSideValidation({ name, value, validator, formData, }) ); } }); } export default function* rootSaga() { yield all([fork(takeHandleFieldAction())]); }
В сагах самая большая из проблем, сложность чтения. Просто беглым взглядом понять, что делает сага сложно. Одна из причин этому — глубокая вложенность. Можно конечно вынести воркеры, что бы уменьшить глубину, однако тогда увеличивается избыточность.
import { all, put, select, fork, takeEvery } from 'redux-saga/effects'; import { runServerSideValidation } from '../actionCreators'; import { HANDLE_FIELD } from '../actionCreators'; function* takeHandleFieldWorker({ payload }) { const { validation, formData } = yield select( ({ validation, formData }) => ({ validation: validation[payload.name], formData, }) ); const valueFromState = formData[payload.name]; if (payload.value !== valueFromState) { const { name, value } = payload; const { validator } = validation.serverValidator; yield put( runServerSideValidation({ name, value, validator, formData, }) ); } } function* takeHandleFieldWatcher() { yield takeEvery(HANDLE_FIELD, takeHandleFieldWorker); } export default function* rootSaga() { yield all([fork(takeHandleFieldWatcher())]); }
Так же, есть проверки, которые или увеличат вложенность, или добавят точки выхода из саги, что ухудшает наш код.
Как я пытаюсь решить эти проблемы
Давайте по порядку.
Фабрики событий
import { actionsCreator, payload } from 'sweet-redux-saga'; @actionsCreator() class ActionsFactory { initialize; @payload('field', 'value') onFieldChange; @payload('field') handleField; @payload('nextStep') goToNextStep; }
создаем класс с аннотацией actionsCreator(). При создании экземпляра класса, полям не имеющим значения(initialize;/onFieldChange;/handleField;/gpToNextStep;) будет присвоен привычный нам action creator. Если событие содержит данные, имена полей передаем через аннотацию payload(...[fieldNames]). После преобразования предыдущий пример будет выглядеть вот так:
class ActionsFactory { initialize = () => ({ type: 'INITIALIZE', payload: undefined, }); onFieldChange = (field, value) => ({ type: 'ON_FIELD_CHANGE', payload: { field, value, }, }); handleField = field => ({ type: 'HANDLE_FIELD', payload: { field, }, }); goToNextStep = nextStep => ({ type: 'GO_TO_NEXT_STEP', payload: { nextStep, }, }); }
так же у полей будут переопределены методы toString, toPrimitive, valueOf. Они будут возвращать строковое представление типа события:
const actionsFactory = new ActionsFactory(); console.log(String(actionsFactory.onFieldChange)); //Вернет 'ON_FIELD_CHANGE'
Редьюсеры
import getInitialState, { formDataInitialState as initialState, } from '../helpers/initialState'; import { HANDLE_FIELD_DONE, ON_FIELD_CHANGE, RESET } from '../actionCreators'; import { reducer } from '../../../leadforms-gen-v2/src/decorators/ReducerDecorator'; @reducer(initialState) export class FormDataReducer { [RESET]() { return getInitialState().formDataInitialState; } [ON_FIELD_CHANGE](state, payload) { const { name } = payload; return { ...state, [name]: '', }; } [HANDLE_FIELD_DONE](state, payload) { const { name, value } = payload; return { ...state, [name]: value, }; } }
создаем класс с аннотацией reducer([initialState]). При создании экземпляра класса, на выходе получится функция принимающая состояние и экшен, и возвращающая результат обработки экшена.
function reducer(state = initialState, action) { if (!action) { return state; } const reducer = instance[action.type]; if (reducer && typeof reducer === 'function') { return reducer(state, action.payload); } return state; }
Саги
import { all, put, select, } from 'redux-saga/effects'; import { runServerSideValidation } from '../actionCreators'; import { HANDLE_FIELD } from '../actionCreators'; import { sagas, takeEvery, filterActions } from 'sweet-redux-saga'; @sagas() class MySagas { @takeEvery([HANDLE_FIELD]) @filterActions( ({state, payload }) => state.formData[payload.name] === payload.value ) * takeHandleFieldAction({ payload }) { const { validation, formData } = yield select(({ validation, formData }) => ({ validation: validation[payload.name], formData, })); const { name, value } = payload; const { validator } = validation.serverValidator; yield put( runServerSideValidation({ name, value, validator, formData, }) ); } } export default function* rootSaga() { yield all([ new MySagas(), ]); }
создаем класс с аннотацией sagas(). При создании экземпляра класса получаем генератор функций, вызывающий все поля класс помеченных аннотацией takeEvery([...[actionTypes]]) или takeLatest([...[actionTypes]]) в отдельном потоке:
function* mySagas() { yield all([fork(mySagas.takeHandleFieldAction())]); }
так же с полями можно использовать аннотацию filterActions({ state, type, payload }), в этом случае сага будут вызвана только если функция вернет true.
Заключение
В целом, используя эти простые аннотации, саги стали понятнее и чище, уменьшилось количество кода, необходимое для добавление новой логики. Навигация по проекту стала проще.
Эти аннотация я вынес в пакет sweet-redux-saga. Если есть другие решения, буду рад, если поделитесь со мной.
MaZaAa
Как уменьшить количество и увеличить читаемость кода в react-redux, redux-saga?
— Забыть уже наконец об этом ужасе, который тянется с давних пор, открыть глаза и использовать MobX.
dmitryorelopt Автор
не работал с MobX, но посмотрел пару статей на хабре только что, и не совсем понятно пару моментов:
1) Как на 1 событие изменить 2 независимые части стора?
2) Как создавать динамические хранилища? имею ввиду, если список полей одного из куска стора, известен только во время выполнения?
ну и такая мелочь, что бизнес логика и обновление стора идет в одном месте, может это во мне привычка говорит, но идея эта мне не очень нравится
MaZaAa
Не нужно опираться на то, что написано в статьях, нужно просто принимать это к сведению, но думать важно именно своей головой, а не статьями и тем что говорят и пишут «авторитеты». MobX можно использовать вообще в разных вариациях, он не обладает ограничениями и не загоняет в рамки.
Вот вам живой пример — codesandbox.io/s/ecstatic-cloud-l956n
Вообще на дворе 2020 год и мне кажется дико когда React developer'ы не знают что такое MobX и даже не пробовали его, ладно ещё в 2016 году, но в 2020 это нонсанс конечно)
P.S. я заменил Redux на MobX ещё в 2016 году и с тех пор получаю удовольствие от работы с реактом, до этого было больше боли из-за редакса.
dmitryorelopt Автор
будь MobX серебряной пуле, что-то мне подсказывает, он бы использовался на всех проектах, на которые я приходил. но это ладно, мне и вправду интересно, у Вас 4 года опыта в MobX, как там описывается логика взаимодействия между несвязанными компонентами?
пример:
изменилось поле А, я его проверил, оно корректно, мне надо сделать запрос на бэк, получить данные других полей и обновить их так же. (изменили ZIP code, надо получить город и штат)
это все будет в том же месте, где я буду обновлять основное поле в перемешку?
MaZaAa
Да как душе угодно это можно сделать, я же говорю он не накладывает ограничений. Вы просо работаете с ним как с любым другим объектом/классом, просто у него есть реактивные свойства на изменения которых автоматически реагирует реакт, так же вы сами можете назначить реакции на изменения тех или иных свойств. Я же кидал сссылку выше там можете посмотреть как оно работает и поэксперементировать сразу же.
dmitryorelopt Автор
ну я покопал в том направлении, в принципе выглядит интересно, надо попробовать )
dmitryorelopt Автор
посмотрел вакансии фронтов, и на том сайте было 90 вакансий с упоминанием redux и только 4 с mobx. Интересно, почему…
MaZaAa
— Есть проекты которые начинаются с нуля.
— Есть проекты где начинают писать версию «2.0».
— Есть проекты которые можно рефакторить и заменять redux на mobx.
— Во многих вакансиях указан только Redux, но по факту никто не против внедрить mobx.
Критической проблемы в этом нет, да большинство существующих проектов к сожалению написаны на redux'e, но тут причина банальная, люди не поднимают голову и не смотрят по сторонам. И/или не рискуют попробовать что-то другое.
Я например с 2016 года проекты только с mobx'ом пишу и вообще кайфую. До этого меня раздражал реакт из-за редакса, а с момента как узнал про mobx то эта связка стала идеальной для меня.
dmitryorelopt Автор
это все понятно, но все же, 4 против 90, ладно там 30/90 ну 20/90, а так, Вы говорите с 2016, так мало кто перешел? vue с 2015 первая версия, и на том же сайте соотношение react/vue 283/83
VolCh
Ну у нас до недавнего времени MobX не упоминался в вакансиях, собственно даже знание React "будет плюсом", поскольку практика показла, что нормальный фронтендер довольно быстро осваивает и React, и MobX
dmitryorelopt Автор
в целом, мне интересно попробовать, я лишь высказал вещи, которые бросились в глаза ;)
MaZaAa
Вам же говорят, написано одно, по факту другое
strannik_k
Потому-что:
* Redux раньше появился.
* Redux хорошо пропиарили в начале и до сих продолжают.
* Как следствие пиара, большинство работали только с redux.
* Как следствие пиара, большинство библиотек компонентов, темплейтов используют redux.
* В redux также описана архитектура, поэтому понятно как с ним сделать одинаковую структуру для разных страниц проекта. В случае mobx нужен соответствующий опыт, либо самому до этого додуматься.
* Абрамов прям супер ит-евангелист. Несмотря на его небольшой опыт, к нему прислушиваются большинство. Какую бы чушь он не продвинул в мире реакта, большинство реакт-разработчиков будут ею пользоваться.
* Команды бэкендеров, фулл-стэков переходя на написание SPA, смотрят, что сейчас популярно. Ага, react, redux. Будем тоже это использовать. Новички в веб-разработке поступают аналогично.
TheShock
Плюс многим удобно, что редакс — это говнокод возведённый в идеологию
MaZaAa
Плюсую и дает возможность говнокодить и это будет считаться нормой в этом сообществе. Что конечно крайне печально, потому что иногда это наследие достается другим(
xadd
По мне идея триггерить события и затем в других местах множественно их отлавливать и на это как-то реагировать конечно интересная, но на практике все это дело дебажить и пытаться читать сам data flow крайне утомляет.
mayorovp
Очень просто: взять и изменить.
Разместить этот метод можно в любом классе, который вы считаете подходящим для этой цели.
Используя observable map, observable object или же можно класс динамически создать (у нас же javascript, в конце концов).
Пишите в разных, никто не запрещает же
dmitryorelopt Автор
получается будут огромные экшены, меняющие разные, не связанные части стора, это не страшно, если кода не много, но на проекте котором я работаю, будут сложности. Хотя, как я понял, можно просто добавлять доп. экшены, которые будут срабатывать на изменения полей.
опять же, придется напрямую вызывать эту логику из экшенов, и вслучае добавление новой, надо опять, залазить в экшен и добавлять вызов, в случае с сагой, обновление стора зависит только от экшенов
mayorovp
Ну а сейчас у вас, видимо, огромные саги, вызывающие разные, не связанные экшены...
Если же огромных саг у вас не наблюдается — почему экшены в MobX должны магическим образом стать таковыми?
Не понимаю о чём вы пишете.
dmitryorelopt Автор
саги не большие, это достигается тем, что на 1 экшен реагирует несколько различных саг, отвечающих за свой кусок логики. И как я написал выше, в случае с mobx, как я понял, будут экшены реагирующие на изменение части стора и изменять свою соответственно
если я правильно понял
Вы имели ввиду, вынести логику в сервисы, и вызывать их из экшенов
и это, мне не очень нравится, я об этом говорил выше
mayorovp
Если вам так нравится слабая связность — не одна только саги её вам могут обеспечить. Простейший EventEmiter пишется за пару минут...
Альтернатива-то какая?
dmitryorelopt Автор
немного сомнительно, я использую только часть возможностей саг, однако, даже эту небольшую часть реализовать руками, займет время, мне проще добавить сахара к имеющемуся, что и вправду быстро.
я без понятия, в сторону mobx только начал смотреть, может Вы знаете решения
MaZaAa
mobx.js.org/refguide/reaction.html
TheShock
Ну, допустим, нам по веб-сокетам инфа какая-то пришла? У вас есть листенер сокетов. В этом листенере должны быть ссылки на 2 нужные части стора. Когда приходит событие — обновляете одну часть стора, а потом вторую часть стора.
Это вообще непонятно. Как работать неизвестно с чем? Вы к каким полям обращаться потом будете? Можно практический пример? Я за свою практику никогда не видел такой странной задачи.
А где, по вашему, обновление стора должно лежать?
dmitryorelopt Автор
я имел ввиду, ввод данных пользователем, и я понимаю, что из одно экшена мы можем изменять весь стор, но это приведет в сильно большим экшенам(action()). Но это вначале были мои мысли, сейчас я разобрался как этого избежать.
куски кода менее понятны, я просто опишу задачу. Нужен инструмент для создания форм, есть набор полей, которые будут почти в каждой форме, однако, он может отличаться, в зависимости от формы, причем формы могут содержать уникальные поля, присущие только ей.
Для решения этого, я собираю имена полей и регистрирую их сторе при старте и работаю с ними по единому интерфейсу для полей, но на этапе разработке их имен я не знаю.
обновление стора правильно лежит, меня смутило, что там не только обновление стора, но и вся логика лежит, в случае с redux-saga, редьюсеры отвечают только за обновление стора, а бизнес логика лежит в сагах
TheShock
dmitryorelopt Автор
если это сделать через массив, очень много придется перебирать эти массивы для поиска, изменения и удаления. Как это выглядит в коде, мне абсолютно не понравилось, а с хэшами, немного приятнее, но тоже не обошлось без неудобств.
TheShock
dmitryorelopt Автор
Вопрос не в производительности, а в читаемости. для обновления значения в сторе используя массив, код будет выглядеть примерно так:
используя хэш так:
на просто примере разница кажется не сильно большой, но в реальном проекте, читаемость сильно отличается
TheShock
Господи, какой этот редакс тошнотворный таки.
А изменение вообще просто делается:
Просто ваш хреновый инструмент заставляет вас крутиться как ужа
mayorovp
В Mobx это пишется куда проще:
Причём это работает одинаково что для массива, что для хеша...
dmitryorelopt Автор
очень странный код
mayorovp
А что странного-то? Ну, за исключением сомнительной необходимости функции changeFieldValue, но её не я придумал.
dmitryorelopt Автор
странно то, что Вы присваиваете значение в пайлоад, беря значение из того-же пайлоада
mayorovp
А что делать, раз пайлоад такой получился? Всего-то
fieldName
(идентификатор модели) наfield
(саму модель) заменил, как это в mobx принято.Я мог бы вовсе убрать функцию
changeFieldValue
, но тогда кто-то мог бы спросить: "а куда функция делась?" :-)TheShock
Вы зря оставили куски говнокода из оригинального примера на редаксе) Просто там так много кода, что то, что он хреновый тяжело увидеть.
В мобиксе, конечно, нету как в Редаксе никаких странных пейлоадов, куда надо впихнуть всё, ибо больше некуда.
faiwer
В защиту redux (да знаю, что меня за одно только это слово побьют) скажу, что нет реально никаких причин использовать
payload
. Это какое-то коллективное помешательство. Единственное зарезервированное поле в action-ах этоtype
. Моя не понимать, зачем все так страдают.mayorovp
От замены payload на action странность не пропадает...
strannik_k
По-вашему, как надо, вместо использования payload?
Разок пришлось писать на redux вместе с другой командой. Местами там писали примерно такое:
Как по мне, чем меньше участков кода знает о структуре передаваемых данных, тем лучше. Меньше кода и меньше вероятность ошибок. Вариант с payload:
MaZaAa
Аж глаз дергается видя это
faiwer
Да я абсолютно убеждён, что большая часть classic-redux way это мусор. И слово payload в том числе.
У нас TypeScript. Каждый участок знает ровно то, что ему положено знать и ни на грамм больше. И всё статически завязано и гарантировано. Спасибо структурной типизации и генерикам.
Вы кстати хороший такой антипаттерн нарисовали. У вас всё пробрасывается вслепую вплоть до api. Это очень плохо. Особенно без TypeScript-а. По сути у вас бомба замедленного действия. Вы напрямую связали удалённые участки кода из разных слоёв. Малейшее изменение с одной стороны приводит к непредсказуемым (и возможно трудноотлавливаемым) багам в конце цепочки.
MaZaAa
Более того redux way уже давно сам по себе мусор) Но народ до сих пор живет в прошлом, это как попасть в страну где до сих пор советский союз, еда по талонам и дефицит товаров)
strannik_k
Мне тоже оба варианта не нравятся, оба плохие. Особенно без типизации, как в моем примере. Но иногда приходиться из двух зол выбирать меньшее.
Кстати, вы не сказали, что вместо payload использовали бы в redux, если бы пришлось на нем писать? Или вы имели ввиду просто давать этому объекту более подходящее именование?
faiwer
Просто все необходимые поля располагаю прямо внутри action-а. На 1-м его уровне. Там же где и поле type. К чёрту бюрократию.
TheShock
Ну не знаю. Как на меня — в этот момент нарушается типизация. То есть Update должен быть формата
Эти комбинированные типы — как-то пошло. Всё-таки тип на более высоком уровне иерархии.
Просто не должно быть дурацких абстрактных названий типа «payload», дурацкой динамической типизации с совершенно непонятными и неконтролируемыми структурами и, конечно, дурацкого редакса
faiwer
Дык в TS структурная типизация, а не номинативная. Не испытываю решительно никаких проблем с этим. Все типы как нужно выводятся и никакой
type
где попало не всплывает. Правда я забыл уточнить что у нас 100500 своих велосипедов и суммарно это всё не похоже на redux (хотя он всё ещё под капотом). Это несколько меняет дело. Если делать всё по заветам Ильич… Абрамова, то там везде грусть конечно. Но грустить некогда, надо писать копипасту во имя ынтерпрайзаНу я и не предлагал поля Unit пихать на 1-й уровень action-а. Но помимо
value: Partial<Unit>
там ещё могут быть какие-нибудь дополнительные поля, не из Unit. И всё это будет на верхнем уровне вместе сvalue
(maybediff
?). Т.е. я не предлагаю уплощать всё до неадеквата. Я просто предлагаю избавиться от лишнего уровня иерархии в виде некоегоpayload
-аTheShock
Опс. Сам критиковал и механически написал `value` вместо `unit`.
Да, в таком виде согласен, пейлоад — просто мусор.
TheShock
Откуда у фанатов редакса страсть к отвратительным названиям? Почему когда вы хотите передать конкретную переменную — вы называете её неправильно? Почему payload, есть внутри лежит вполне себе user? У вас и так код неподдерживаемый, а вы ещё и усложняете.
strannik_k
Я сейчас на скорую руку пример написал, не заботясь об именовании. Написал примерно, что видел в коде redux проекта. Остальные вопросы к фанатам редакса, а я не фанат и стараюсь избегать проектов на нем.
VolCh
Вечная борьба между связанностью и связностью.
mayorovp
Массивы перебирать вам не придётся если вы сохраните ссылку на элемент вместо имени поля.
dmitryorelopt Автор
не совсем понял Вашу мысль, я инициализировал список полей как массив, потом пользователь вводит данные в одно из полей и мне надо в торе изменить значение value. Как это сделать, не перебирая массив в поисках нужного поля?
mayorovp
Для производительности тут надо обязательно отдельный компонент выделить (или хотя бы Observer), но это уже детали.
dmitryorelopt Автор
прикольно конечно, это же в рендере, а как тут добавить серверную валидацию по мимо обновления значения?
mayorovp
У вас же есть метод
setRawValue
. В него любую валидацию можно добавить...dmitryorelopt Автор
Вы наверное путаете валидацию на фронте, с серверной, она будет асинхронна, и влаг валидности поля будет установлен после ответа сервера, а значение в поле установить сразу надо
mayorovp
Ничего я не путаю. Что вам мешает сразу установить значение в поле, и одновременно инициировать запрос на сервер?
dmitryorelopt Автор
все, я понял что вы имели виду, спасибо за ваше потраченное время ;)
strannik_k
Фактически этим и занимается mobx, vuex и даже redux. Только в redux это усложнено и размыто по куче ненужных сущностей.
Еще недавно стал считать нормальным подход (но не пробовал его), где api метод (или посредник с сайд-эффектами, вроде redux-saga) вызывается в методах стора и после выполнения стор сохраняет пришедшие данные. Но только вызов, преобразование и сохранение данных. Никакой сетевой логики, сайд эффектов в коде стора.
Наличие другой логики, кроме описанной (но я может что-то и упустил), превращает стор в какой-то контроллер.
Может в примерах mobx и не сделано, как я описал. Но там можно так организовать архитектуру. Конечно, минус mobx-у за то, что нет рекомендаций (я не видел), best practices по архитектуре.
dmitryorelopt Автор
Я как раз люблю слабую связанность кода, и разбитие на слои, мне больше нравится, чем иметь только контроллер и сервисы
strannik_k
Ну, я как раз описал один из слоев и упомянул пару других.
В более полном виде выглядело бы так:
* слой взаимодействия с сервером
* слой сайд-эффектов
* слой стора (хранения глобальных данных и уведомление об изменениях в этих данных)
* слой логики компонента (custom hooks)
// я тоже предпочитаю разбивать на слои и не считаю нормальным, что одну и же логику можно писать в двух разных сущностях (в компоненте и в custom hook)
* слой view (jsx)
pawlo16
зачем mobx, если есть реакт хуки? Забыть вообще о реактивных стейт-контейнерах
VolCh
Зачем о них забывать?
pawlo16
Затем, что useState и useReducer предоставляют те же опции, что стейт контейнеры. При этом не создают гемороя и идиотизма, свойственного приложениям со стейт контейнерами
MaZaAa
1) Гемороя? Идиотизма? Можно по подробнее пожалуйста.
2) Быть может это просто вы пока слишком далеки от этого из-за нехватки квалификации, не понимая асинхронности, не понимая реактивности?
gnaeus
Но хуки из коробки не предоставляют никакого механизма подписки на изменения частичного стейта из
useReducer
для компонентов-потомков в глубине дерева.pawlo16
Это и не нужно, прямая передача колбэков в пропсах в разы проще. А для глобальных сущностей есть контекст
MaZaAa
Мда… Тяжелый случай.
romanovAA
MobX один из худших Стейт менеджеров. Низкая производительность, плохая масштабируемость, сложный код, лишние конструкции, которые провоцируют писать лишнее.
MaZaAa
Хорошая шутка
unel
о, а можно пруфы про низкую производительность, сравнения какие-нибудь?
MaZaAa
Какие пруфы? Ссылки на статьи чтоли?) Вы тоже шутник походу) Единственный пруф это самостоятельные тесты или исходники которые можно запустить любому и убедится в цифрах и правильности этих сравнений. Все остальное это пустые слова и брехня.