Хуки и функции JS, о которых вы не знали — устаревшие переменные

JavaScript
Хуки и функции JS, о которых вы не знали — устаревшие переменные

предисловие

С ростом популярности хуков React и популярности различных функциональных компонентов, таких как Vue3, во фронтенде, разработка каждого постепенно начала переходить в более облегченный режим, В этом процессе функциональное программирование снова отодвинулось на передний план. -конечные инженеры. поле зрения, например, мы бы использовалиcomposeДавайте напишем HOC больше как Decorator:

// with compose 
compose(
  withRouter, 
  LoadingHOC,
  React.memo
)(FuncComponent)

// without compose
withRouter(LoadingHOC(React.memo(FuncComponent)))

Или мы напишем больше чистых функций, чтобы нашу логику можно было повторно использовать более абстрактно, и мы удалим множество различных операций, которые мы, возможно, изучали четыре или пять лет назад, чтобы придать новую логику логике, которую мы пишем сегодня. изменения. Но эти вещи, с которыми мы были знакомы, теперь кажутся немного незнакомыми после того, как мы привыкли к объектно-ориентированному стилю. Конечно, наша сегодняшняя тема, естественно, не функциональное программирование.

想不到吧

После стольких лет перемещения кирпичей внешнего интерфейса вы должны знать продвижение объявления переменных и цепочку областей действия функций JS.Сегодня я хочу поговорить о тех функциях JS и их цепочках областей видимости, с которыми вы раньше думали, что знакомы, но их вам сейчас скинули.Много чего смущает в необычной подаче. Давайте сначала рассмотрим типичный случай.С такой проблемой я столкнулся при написании распространенного и ничем не примечательного функционального компонента React hooks.Позвольте сначала показать вам демонстрационный код:

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

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Компонент выполнит асинхронный обратный вызов в useEffect.Асинхронные задачи — распространенный сценарий в бизнес-разработке, но сейчас произошло что-то, что противоречит нашим привычкам кода.Например, если я нажму кнопку пять раз подряд, что вы думаете Что будет вывод консоли? Я не буду продавать это здесь Мы, привыкшие к объектно-ориентированному письму, можем подсознательно думать, что конечный вывод должен быть 5 5, потому что асинхронная задача проталкивается в асинхронную очередь, но рендеринг компонента выполняется синхронно Естественно, полученный окончательный результат должен соответствовать рендерингу страницы. Но окончательный вывод не совсем соответствует нашему обычному мышлению, результаты говорят нам, что React, похоже, сохраняет состояние объявления асинхронной функции или что сохраняется снимок.

function-component

Я нашел много статей,который также включает блог Дэна Полное руководство по использованию эффектовПодождите (вы можете увидеть это в справочной статье в конце статьи), большинство статей выбрасывают какие-то практические решения, но также дают мне более странные сценарии, чтобы запутать меня больше (я пытался решить проблему), но что смущает я больше всего,Не объяснили причину такой проблемы, Либо все это только из конструкции фреймворка, либо выкидывает очень многословное и распространенное понятие - замыкание. Как ByteDancer, который преследует окончательное, обсессивно-компульсивное расстройство не позволяет мне просто отпустить его, поэтому я решил достать маленькую красную книжку, которая была запечатана в течение многих лет, а затем вернуться и глубоко изучить ее. функции JS, что это за механизм реализации?

Итак, вернемся к первоначальному вопросу, почему это происходит? История должна начаться с брата по имени В.О.

Вы слышали о VO и AO, когда изучали объявления переменных?

Как фронтенд-инженер вы должны быть знакомы с продвижением декларации JS, например, со следующим кодом:

function test(x) {
  var b = 20; // local variable of the function context
}

test(30)
  
alert(a) // undefined
alert(b) // "b" is not defined
alert(c) // "c" is not defined

var a = 10 // variable of the global context
c = 40

"b" is not definedЭто надо хорошо понимать, тогда тут будет кинут вопрос, раз декларация будет выдвинута, почему ц будетis not defined, все точно скажут, что "потому что в c нет var, значит и объявления нет" тоже резонно, тут я не буду закругляться, а сразу к делу.

Поскольку переменные связаны с контекстом выполнения, механизм интерпретации должен знать, где хранятся его данные и как их получить. ) объект. ——《Подробнее о ECMA-262-3. Глава 2. Переменный объект.》

Таким образом, процесс запуска JS-скрипта полностью можно разделить на два этапа, а именно этап объявления и этап выполнения. Этап объявления отвечает за сбор переменных для построения ВО и контрольных точек между ВО.Фаза выполнения отвечает за передачу контекста выполнения в ВО для генерации АО и присвоение значений переменным, объявленным в ВО. или другие процессы изменения.

Поскольку мы все знаем, что выполнение функции JS будет генерировать независимую область видимости, поэтому каждый раз, когда будет объявлена ​​функция, будет создан новый VO для хранения переменных в области видимости. Таким образом, код JS, который мы только что написали, после того, как механизм интерпретации переходит к этапу объявления, полученный VO выглядит так, как показано в следующем коде:

// Variable object of the global context
VO(globalContext) = {
  a: undefined,
  test: <reference to FunctionDeclaration 'test'>
};
  
// Variable object of the "test" function context
VO(test functionContext) = {
  x: undefined,
  b: undefined
};

Естественно, механизм интерпретации решаетc = 40, будет считать это оператором присваивания, поэтому не будет висеть на ВО, когда выполнение дойдетalert(c)Когда механизм синтаксического анализа не может найти определение переменной c в VO(globalContext), он, естественно, выдает"c" is not definedОшибка при выполненииc = 40В этом коде VO перезаписывается выполнением инструкции.В это время общий VO становится следующим:

// Variable object of the global context
VO(globalContext) = {
  a: 10,
  c: 40,
  test: <reference to FunctionDeclaration 'test'>
};
  
// Variable object of the "test" function context
VO(test functionContext) = {
  x: 30,
  b: 20
};

Таким образом, вы можете понять, почему объявление JS расширено, почему объявление функции переопределяет объявление переменной и почему иногда выдаетXXX is not defined, собственно, суть в том, что они монтируются на ВО в другом порядке, или уже монтируются на ВО при выполнении.

Лексические среды и стек вызовов

Только что закончил рассказывать про ВО и АО.Для того, чтобы понятия были понятнее и проще для понимания (на самом деле мне понадобилось почти пол года, чтобы полностью понять ВО) спецификация ES5 также ввела лексическое окружение (Lexical environments) такое концепция, не смотрите на слово особенно высоко, на самом деле то, что он хочет решить, это то, что мы обычно понимаем, проблема, что дочерняя область может получить переменные родительской области.

Чтобы проиллюстрировать этот механизм с помощью структуры синтаксиса JS, с которой мы наиболее знакомы, давайте возьмем относительно простой пример:

var x = 10;
 
function foo() {
  var y = 20;
}

Например, в таком фрагменте кода мы все знаем, что функция foo будет формировать независимую область видимости, потому что это функция, и он также может получить переменные в своем родительском контексте, поэтому результат, полученный после перевода в лексическое окружение Именно так:

// environment of the global context
globalEnvironment = {
  environmentRecord: {
    // built-ins:
    Object: function,
    Array: function,
    // etc ...
    // our bindings:
    x: 10
  },
  outer: null // no parent environment
};
 
// environment of the "foo" function
fooEnvironment = {
  environmentRecord: {
    y: 20
  },
  outer: globalEnvironment
};

Здесь мы видим, что в каждомEnvironmentСуществует внешнее поле для обозначения родительского контекста текущей области, поэтому для переменных, которые не могут быть найдены в текущем контексте, механизм интерпретации будет продолжать рекурсивный поиск вверх по внешнему полю, пока переменная не будет найдена. Следовательно, мы можем знать, что при объявлении функции переменные, которые она может получить, уже определены.

Теперь мы знаем, откуда функция получает свои собственные переменные, а до этого вы должны знать, что при выполнении функции она будет помещена в стек вызовов JS, и такую ​​структуру стека можно использовать для хранения значения конкретного переменная при выполнении функции.

Значение Call-Stack: всякий раз, когда функция выполняется, запись о ее выполнении (включая ее формальные параметры и локальные переменные) будет помещена в стек вызовов. Таким образом, если функция вызывает другую функцию (или рекурсивно вызывает саму себя), другой стек помещается в стек текущей функции. После выполнения контекста функции механизм интерпретации удалит запись выполнения из стека (поп) — «ECMA-262-5 в деталях. Глава 3.1. Лексические среды: Общая теория».

Вот пример кода, чтобы объяснить это вамCall-StackПроцесс помещения функции в стек:

var bar = (function foo() {
  var x = 10;
  var y = 20;
  return function bar() {
    return x + y;
  };
})();
 
bar(); // 30

Это тип закрытия, который мы можем часто писать, это действительно так.Call-StackПроцесс существования показан на следующем рисунке:

  1. Во-первых, входя в IIFE, объявляется функция foo, согласно тому, что мы знали ранееEnvironmentМетод генерации, он знает, что внутри две локальные переменные x и y, а также панель функций, функция имеет независимую область видимости и должна генерировать отдельнуюEnvironment, функция bar использует две переменные, x и y, доступ к которым можно получить с помощьюEnvironmentВнешнее свойство foo находится вEnvironmentнайти в
  2. Далее выполняется функция foo и запускаетсяCall-Stack, давая во время выполненияEnvironmentприсвоение производитEnvironmentRecordЗапишите в память, сформировав статическую ссылку, содержащую текущую функцию.EnvironmentИнформация в памяти сохраняется в виде «снимка», а полоса возврата после выполнения функции указывает на текущую ссылку
  3. bar выполняется, foo найденEnvironmentRecordИ прочитать значения искомых переменных x и y, выполнить возврат

Это функция, которую мы обычно пишем вCall-Stackпроцесс существования. И нетрудно найти на рисунке, что глобальная функция бара здесь имеетEnvironmentRecordСсылка всегда существовала.Если ссылка не нарушена, механизм интерпретации не может знать, когда функция bar будет вызвана снова и когда она будет использована снова.EnvironmentRecordпеременные внутри, поэтомуEnvironmentRecordВ памяти он никогда не будет восстановлен сборщиком мусора, и память не будет освобождена, чтоутечка памяти, что является фатальным для сред выполнения узлов или серверов. Конечно, решение тоже очень простое, требуется всего одна строчка кода:

bar = null

Если мы укажем сборщику мусора вовремя перезапустить ссылку, вышеуказанная проблема не возникнет.

Вот вопрос, над которым следует подумать всем, и еще один пример, который мы чаще встречаем в бизнес-кодировании. Вы можете попробовать сами, чтобы проверить наши выводы, сделанные выше, на собственном понимании:

function createIncrement(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    const message = `Current value is ${value}`;
    return function logValue() {
      console.log(message);
    };
  }
  return increment;
}

const inc = createIncrement(1);
const log = inc(); // 打印 1
inc();             // 打印 2
inc();             // 打印 3
log();             // 打印 "Current value is 1"

Вы можете разобраться с процессом выполнения этой функции в соответствии с нашими идеями прямо сейчас.Call-Stackсуществуют в процессе? Если вы это сделаете, я думаю, вы легко поймете, почему вывод такой.

Вернемся к объяснению устаревших переменных в хуках

Вернемся к функциональному компоненту, который мы бросили в начале:

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

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Давайте реорганизуем результаты после пяти кликов в соответствии с только что сделанным выводом.Environmentгенерировать иCall-Stackпроцесс укладки.

  1. Первый щелчок, триггерsetCount,stateобновление, компонент повторно визуализируется, и функциональный компонент повторно выполняется
  2. Выполнение функции счетчика: функция нажатаCall-Stack,EnvironmentRecordопределяется для формирования ссылки,countдляsetCountпереданное значение,setTimeoutОбъявлен callback и нашел в нем ссылку на Countercountлокальная переменная, указывающая на счетчикEnvironmentRecordЦитировать,setTimeoutОбратный вызов помещается в асинхронную очередь, ожидая запуска
  3. нажмите еще раз,stateобновляется, счетчик повторно выполняется, и новыйEnvironmentRecord, также генерирует новыйsetTimeoutОбратный вызов, обратный вызов в это время указывает на текущийEnvironmentRecord, и помещается в асинхронную очередь для ожидания триггера
  4. Запускается первый обратный вызов setTimeout, найдите соответствующийEnvironmentRecordцитировать, получитьEnvironmentRecordвнутриcountзначение, выполнитьconsole.log

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

С быстрым увеличением количества функциональных компонентов, которые мы пишем, мы можем писать много замыканий, случайно или нет, поэтому в процессе, когда мы понимаем природу «моментальных снимков» функций JS, мы, естественно, будьте более осторожны с этими переменными, которые могут быть «устаревшими», или, когда мы обнаруживаем, что возвращаемые результаты выполнения не такие, как ожидалось, мы также можем разумно объяснить, почему возникают такие эффекты и как их избежать.

Эпилог

Благодаря такому полному обзору мы не только решили проблему, которая была у нас в начале — почему эти функции имеют «устаревшие» состояния или переменные, мы также поняли механизм реализации продвижения переменных JS и почему его можно найти через цепочку областей видимости. Переменные верхнего уровня, почему происходят утечки памяти и каковы решения этих проблем.

Так что все это не новая фича, привнесенная хуками React или какими-то появляющимися фреймворками функционального программирования, На самом деле все это всегда было грамматической фичей, которая существовала с момента рождения языка JS, но раньше мы были нестабильны, потому что замыканий, есть проблемы с производительностью и т.д., их мало используют, а в процессе разработки JS последние несколько лет все гоняются за объектно-ориентированным программированием, механизмом ссылок собственных объектов, Причина, по которой они не «устарели», заключается именно в том, что они всегда являются просто ссылкой, они просто представляют собой указатель, который больше соответствует традиционному мышлению программирования и нашему последовательному мышлению в кодировании. вернулся в поле нашего зрения. Но причина, по которой нам нужно приспосабливаться к такому процессу преобразования, на самом деле просто процесс возвращения к основам, позволяющий нам снова увидеть характеристики самого языка JS.

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

Так что волшебного дизайна фреймворка не существует, и это не новый шаблон проектирования, который нельзя выучить, это, наверное, просто фичи, оставленные языком JS в начале его проектирования! Все началось с этого замечательного начала — функции имеют независимую область видимости.

Итак, после прочтения этой статьи вы думаете, что все еще знаете функции JS?

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

Некоторое содержание и случаи в статье взяты из следующих статей.После прочтения этой статьи рекомендуется поесть вместе:

Полное руководство по использованию эффектов

При использовании JS и React Hook нужно обращать внимание на яму устаревших замыканий (решения есть в тексте)

Чего ты хочешь от меня Серия JS (3) -- VO

ECMA-262-3 in detail. Chapter 2. Variable object.

ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.

ECMA-262-5 in detail. Chapter 3.1. Lexical environments: Common Theory.

жесткий широкий

Наша команда набирает сотрудников! ! ! Добро пожаловать в команду внешнего интерфейса ByteDance для монетизации бизнеса.Техническое строительство, которое мы делаем, включает в себя: обновление системы внешнего интерфейса, построение инфраструктуры командного узла, инструменты для публикации CI одним щелчком мыши, поддержку обслуживания компонентов, интернационализированный интерфейс. Общие решения, сверхмощные Опираясь на трансформацию микроинтерфейса бизнес-системы, систему визуального построения страниц, систему бизнес-аналитики BI, автоматическое тестирование интерфейса и т. д., команда разработчиков интерфейса Пекина, Шанхая и Ханчжоу 100 человек точно найдут для вас области интереса. Если вы хотите присоединиться к нам, добро пожаловать, нажмите на мой внутренний push-канал:

✨✨✨✨✨

Пуш-портал (Золотой сезон найма, нажмите, чтобы получить возможности внутреннего продвижения ByteDance!)

Эксклюзивный вход для набора в школу (код ByteDance для набора в школу: HTZYCHN, ссылка для доставки:Присоединяйтесь к ByteDance-Рекрутмент)

✨✨✨✨✨

Если вы хотите узнать о повседневной жизни (ду), жизни (би) и рабочей среде (ли) нашего отдела, вы также можетекликните сюдаЯ понимаю~