«Фронт-конечный двигатель» идет глубоко в реактивных крючках - принцип и реализация

внешний интерфейс React.js
«Фронт-конечный двигатель» идет глубоко в реактивных крючках - принцип и реализация

предисловие

Базовое использование React Hooks,официальная документацияуже очень подробно. Это третья статья из серии, посвященной механизму реализации хуков.

useState

В первых двух статьях был проанализирован механизм выполнения useState и useEffect.Если вы хотите узнать больше о хуках, вы можете смоделировать и реализовать связанные функции в соответствии с соответствующими характеристиками хуков, или вы можете перейти непосредственно к просмотруисходный код. Студенты, которые не понимают некоторых основных особенностей хуков, могут обратиться к документации или моим первым двум статьям.

useState — это основа схемы хуков, которая позволяет нам создавать у компонентов собственное состояние, не полагаясь на классы. Его использование простое:

const [count, setCount] = useState(0);

И мы всегда должны помнить две вещи:1. useState — это функция 2. Каждое обновление фактически запускает ее повторное выполнение (как упоминалось в предыдущей статье, потому что весь функциональный компонент выполняется повторно).

Это так просто:

function useState(initialState) {
      const state = initialState;
      const setState = newState => state = newState;
      return [state, setState];
}

Конечно, сейчас он неполный, потому что просто не может обновлять данные. Даже если мы инициируем обновление компонента, когда мы устанавливаем состояние, а затем обновляем состояние использования, в конце концов, это просто начальное состояние, полученное при повторной инициализации. Поскольку состояние каждого useState является локальной переменной, обновление эквивалентно его повторному созданию один раз, и последнее значение не может быть получено. Итак, нам нужно сохранить государство.

Самое простое — это глобальные переменные, конечно React этого не реализует. Для хуков React использует связанный список.Если вам интересно, вы можете пойти и посмотреть сами.Это всего лишь имитационная реализация.

const HOOKS = []; // 全局的存储 hook 的变量
let currentIndex = 0; // 全局的 依赖 hook 执行顺序的下标
function useState(initialState) {
   HOOKS[currentIndex] = HOOKS[currentIndex] || initialState; // 判断一下是否需要初始化
   const memoryCurrentIndex = currentIndex; // currentIndex 是全局可变的,需要保存本次的
   const setState = newState => HOOKS[memoryCurrentIndex] = newState;
   return [HOOKS[currentIndex++], setState]; // 为了多次调用 hook,每次执行 index 需要 +1
}

Мы сохраняем все хуки в переменной глобального массива и просто ищем соответствующий индекс при каждом обновлении.Вот почему необходимо следить за тем, чтобы порядок выполнения хуков был одинаковым до и после обновления.. Представим, что в первом прогоне было выполнено четыре хука, а индекс был увеличен до 3, а при обновлении, из-за суждений и других причин, исходный третий хук не был выполнен, поэтому значение, полученное четвертым хуком, не равно то же неправильно? .

Давайте расширим немного дальше, инициализацию useState можно передать функциям, а setState — то же самое, так что вы можете добавить немного больше суждений.

function useState(initialState) {
   HOOKS[currentIndex] = HOOKS[currentIndex]
       || (typeof initialState === 'function' ? initialState() : initialState);
   const memoryCurrentIndex = currentIndex; // currentIndex 是全局可变的,需要保存本次的
   const setState = p => {
       let newState = p;
       // setCount(count => count + 1)  判断这种用法
       if (typeof p === 'function') newState = p(HOOKS[memoryCurrentIndex]);
       // 如果设置前后的值一样,就不更新了
       if (newState === HOOKS[memoryCurrentIndex]) return;
       HOOKS[memoryCurrentIndex] = newState;
   };
   return [HOOKS[currentIndex++], setState];
}

Tick

Конечно, теперь мы просто обновляем соответствующее состояние, не уведомляя функцию обновления рендера, мы можем написать простой инструмент.

const HOOKS = []; // 全局的存储 hook 的变量
let currentIndex = 0; // 全局的 依赖 hook 执行顺序的下标
const Tick = {
 render: null,
 queue: [],
 push: function(task) {
     this.queue.push(task);
 },
 nextTick: function(update) {
     this.push(update);
     Promise.resolve(() => {
         if (this.queue.length) { // 一次循环后,全部出栈,确保单次事件循环不会重复渲染
             this.queue.forEach(f => f()); // 依次执行队列中所有任务
             currentIndex = 0; // 重置计数
             this.queue = []; // 清空队列
             this.render && this.render(); // 更新dom
         }
     }).then(f => f());
 }
};

В React обновление setState синхронное, но мы его не воспринимаем, по крайней мере кажется, что оно асинхронное. Это связано с тем, что React сам реализует набор управления транзакциями. Возможности ограничены, поэтому используйте Promise, чтобы заменить его здесь, аналогично nextTick в Vue.

const setState = p => {
   // ···
   Tick.nextTick(() => {
       HOOKS[memoryCurrentIndex] = newState;
   });
 };

useEffect

useEffect действует как роль жизненного цикла в классе, и мы используем его больше для уведомления об обновлениях и устранения побочных эффектов. При понимании useState общий принцип тот же. Есть только еще один массив зависимостей, и если каждый элемент в массиве зависимостей такой же, как и последний, его не нужно обновлять, и наоборот.

function useEffect(fn, deps) {
   const hook = HOOKS[currentIndex];
   const _deps = hook && hook._deps;
   // 判断是否传了依赖,没传默认每次更新
   // 判断本次依赖和上次的是否全部一样
   const hasChange = _deps ? !deps.every((v, i) => _deps[i] === v) : true;
   const memoryCurrentIndex = currentIndex; // currentIndex 是全局可变的
   if (hasChange) {
       const _effect = hook && hook._effect;
       setTimeout(() => {
           // 每次先判断一下有没有上一次的副作用需要卸载
           typeof _effect === 'function' && _effect();
           // 执行本次的
           const ef = fn();
           // 更新effects
           HOOKS[memoryCurrentIndex] = {...HOOKS[memoryCurrentIndex], _effect: ef};
       })
   }
   // 更新依赖
   HOOKS[currentIndex++] = {_deps: deps, _effect: null};
}

Вы можете видеть, что useEffect сохраняет два значения в HOOKS, одно является зависимостью, а другое побочным эффектом, Время сохранения соответствует порядку его выполнения. Поскольку useEffect необходимо выполнить после монтирования dom, setTimeout используется для простой симуляции, чего нет в React.

useReducer

useReducer хорошо понятен и может рассматриваться как оболочка вокруг useState, позволяющая нам более интуитивно управлять состоянием. Сначала вспомним, как его использовать:

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return {total: state.total + 1};
        case 'decrement':
            return {total: state.total - 1};
        default:
            throw new Error();
    }
}
const [state, dispatch] = useReducer(reducer, { count: 0});
// state.count ...

То есть нам на самом деле просто нужно совместить useState и редюсер.

function useReducer(reducer, initialState) {
    const [state, setState] = useState(initialState);
    const update = (state, action) => {
      const result = reducer(state, action);
      setState(result);
    }
    const dispatch = update.bind(null, state);
    return [state, dispatch];
}

useMemo & useCallback

Оба они используются для оптимизации производительности React за счет зависимостей и предназначены для обработки некоторых дорогостоящих вычислений. useMemo возвращает значение, а useCallback возвращает функцию.

useMemo

function useMemo(fn, deps) {
    const hook = HOOKS[currentIndex];
    const _deps = hook && hook._deps;
    const hasChange = _deps ? !deps.every((v, i) => _deps[i] === v) : true;
    const memo = hasChange ? fn() : hook.memo;
    HOOKS[currentIndex++] = {_deps: deps, memo};
    return memo;
}

useCallback

function useCallback(fn, deps) {
    return useMemo(() => fn, deps);
}

резюме

В этом простом симуляторе реализованы некоторые хуки React. Если вам интересно, вы можете улучшить его самостоятельно. Если вы обнаружите какие-либо проблемы, вы можете указать на это в комментариях, и я обновлю его со временем.

  • полный код здесьhook.js
  • Способ применения следующий:
function render() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        const time = setInterval(() => {
            setCount(count => count + 1);
        }, 1000)
        // 清除副作用
        return () => {
            clearInterval(time);
        }
    }, [count]);
    document.querySelector('.add').onclick = () => {
        setCount(count + 1);
    };
    document.querySelector('#count').innerHTML = count;
}
// 绑定 render
Tick.render = render;
render();

Справочная статья

группа обмена

Группа WeChat: Отсканируйте код, чтобы ответить, чтобы присоединиться к группе.

mmqrcode1566432627920.png

постскриптум

Если вы это видите, и эта статья вам полезна, надеюсь, вы сможете поддержать автора своими ручонками, спасибо 🍻. Если в тексте что-то не так, укажите на это и поделитесь. Хорошо, я снова потратил впустую время всех, спасибо за чтение, увидимся в следующий раз!

Заинтересованные студенты могут обратить внимание на мой публичный номерпередний двигатель, весело и познавательно.