Узнайте о троттлинге и анти-тряске из исходного кода lodash

внешний интерфейс исходный код JavaScript внешний фреймворк
Узнайте о троттлинге и анти-тряске из исходного кода lodash

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

   В реальном процессе разработки на самом деле бывает много таких ситуаций, как:

  • событие прокрутки страницы
  • События ввода, такие как поля ввода
  • mousemove и т. д., используемые для событий перетаскивания

   Давайте сначала поговорим об анти-тряске и троттлинге, в чем разница

Anti-shake: установите временной интервал.Если часто срабатывающая функция выполняется один раз, она не будет срабатывать снова в течение этого временного интервала.Если вы попытаетесь вызвать эту функцию в течение этого периода, временной интервал перезапустится.

Регулирование: установите временной интервал, и часто запускаемая функция будет выполняться только один раз в течение этого временного интервала. То есть эта часто запускаемая функция будет выполняться в фиксированном цикле.

debounce (функция устранения дребезга)

   Приблизительно просмотрите структуру кода. Для простоты чтения давайте закомментируем функцию в исходном коде.

function debounce(func, wait, options) {
    // 代码一开始,以闭包的形式定义了一些变量
      var lastArgs,  //  最后一次debounce的arguments,它其实起一个标记位的作用,后面会提到
          lastThis,  //  就是last this,用来修正this指向
          maxWait,   //  存储option里面传入的maxWait值,最大等待时间
          result,    //  其实这个result始终都是undefined
          timerId,   // setTimeout赋给它,用于表示当前定时器
          lastCallTime,   // 最后一次调用debounce的时刻
          lastInvokeTime = 0,    //  最后一次调用用户传入函数的时刻
          leading = false,   //  是否在一开始就执行用户传入的函数
          maxing = false,    //  是否有最大等待时间
          trailing = true;   //  是否在等待周期结束后执行用户传入的函数

    //  用户传入的fun必须是个函数,否则报错
      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      
    //  toNumber是lodash封装的一个转类型的方法
      wait = toNumber(wait) || 0;
      
    //  获取用户传入的配置
      if (isObject(options)) {
        leading = !!options.leading;
        maxing = 'maxWait' in options;
        maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
      }

    //  执行用户传入的函数
      function invokeFunc(time) {
        // ......
      }

    //  防抖开始时执行的操作
      function leadingEdge(time) {)
        // ......
      }

    //  计算仍然需要等待的时间
      function remainingWait(time) {
        // ......
      }

    //  判断此时是否应该执行用户传入的函数
      function shouldInvoke(time) {
        // ......
      }

    //  等待时间结束后的操作
      function timerExpired() {
        // ......
      }

    //  执行用户传入的函数
      function trailingEdge(time) {
        // ......
      }

    //  取消防抖
      function cancel() {
        // ......
      }

     //  立即执行用户传入的函数
      function flush() {
        // ......
      }

    // 防抖开始的入口
      function debounced() {
        // ......
      }
      
      
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
    }

Начнем с функции входа. После того, как функция начинает выполняться, сначала возникают три ситуации:

  • Условия для исполнения достигнуты вовремя;
  • Время не соответствует условиям, но таймер в это время не запускается;
  • Если условие не выполнено, вернуть undefined

   Честно говоря, не ожидал сцены во втором случае, кто бы её добавил.

   в кодеtimerId = setTimeout(timerExpired, wait);Он используется для установки таймера, который срабатывает, когда время истекло.trailingEdgeэта функция.

function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);   // 判断此时是否可以开始执行用户传入的函数
        
        lastArgs = arguments;
        lastThis = this;
        lastCallTime = time;

        if (isInvoking) {
          // 如果此时并没有定时器存在,就开始进入防抖阶段
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          //  如果设置了最大等待时间,便立即执行用户传入的函数
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }
        
        //  不满足条件,return undefined
        return result;
      }

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

      function shouldInvoke(time) {
        //  lastCallTime初始值是undefined,lastInvokeTime初始值是0,
        //  防抖函数被手动取消后,这两个值会被设为初始值
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        // Either this is the first call, activity has stopped and we're at the
        // trailing edge, the system time has gone backwards and we're treating
        // it as the trailing edge, or we've hit the `maxWait` limit.
        return (
                lastCallTime === undefined ||   //  初次执行
                (timeSinceLastCall >= wait) ||  //  上次调用时刻距离现在已经大于wait值
                (timeSinceLastCall < 0) ||      //  当前时间-上次调用时间小于0,应该只可能是手动修改了系统时间吧
                (maxing && timeSinceLastInvoke >= maxWait)  //  设置了最大等待时间,且已超时
            );
      }

  Продолжаем анализ стадии запуска функцииleadingEdge. Сначала сбросьте время последнего вызова функции защиты от сотрясений, а затем запустите таймер, чтобы гарантировать следующее выполнение после ожидания. Наконец, еслиleadingдаtrueЕсли это так, немедленно выполните функцию, переданную пользователем:

      function leadingEdge(time) {
        // Reset any `maxWait` timer.
        lastInvokeTime = time;
        // Start the timer for the trailing edge.
        timerId = setTimeout(timerExpired, wait);
        // Invoke the leading edge.
        return leading ? invokeFunc(time) : result;
      }

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

      function timerExpired() {
        var time = now();
        if (shouldInvoke(time)) {
          return trailingEdge(time);
        }
        // Restart the timer.
        timerId = setTimeout(timerExpired, remainingWait(time));
      }

Что касается того, как пересчитать оставшееся время, тут особо и объяснять нечего, видим с первого взгляда.

      function remainingWait(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime,
            timeWaiting = wait - timeSinceLastCall;

        return maxing
          ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting;
      }

   Давайте поговорим об операции по истечении времени ожидания. Некоторые переменные этого цикла сбрасываются. и еслиtrailingдаtrueа такжеlastArgsКогда он существует, параметры, переданные пользователем, будут выполняться снова. Здесь объясняется то, что было упомянуто в начале статьиlastArgsЭто просто бит флага, как говорится в комментарии, это означает, что debounce выполняется хотя бы один раз.

      function trailingEdge(time) {
        timerId = undefined;

        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        if (trailing && lastArgs) {
          return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
      }

   Выполнить функцию, переданную пользователем, относительно просто, мы знаемcallа такжеapplyбудет выполнен сразу же, фактически последнийresultещеundefined.

    function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;
        //  重置了一些条件
        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        //  执行用户传入函数
        result = func.apply(thisArg, args);
        return result;
      }

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

      function cancel() {
        if (timerId !== undefined) {
          clearTimeout(timerId);
        }
        lastInvokeTime = 0;
        lastArgs = lastCallTime = lastThis = timerId = undefined;
      }

      function flush() {
        return timerId === undefined ? result : trailingEdge(now());
      }

дроссель (функция дросселя)

   По сути, принцип троттлинга тот же, что и у антишейка, но условия срабатывания другие.maxWaitдляwaitантивибрационная функция.

    function throttle(func, wait, options) {
      var leading = true,
          trailing = true;

      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      if (isObject(options)) {
        leading = 'leading' in options ? !!options.leading : leading;
        trailing = 'trailing' in options ? !!options.trailing : trailing;
      }
      return debounce(func, wait, {
        'leading': leading,
        'maxWait': wait,
        'trailing': trailing
      });
    }

Суммировать

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

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