Священные Писания больших заводов на 25 000 слов | Технические очерки Nuggets

алгоритм JavaScript Vue.js React.js

Следующие вопросы интервью от Tencent, Ali, NetEase, Ele.me, Meituan, Pinduoduo, Baidu и других крупных компаний, которые часто тестируются.

Как написать красивое резюме

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

Я обычно оказываю некоторые платные услуги по пересмотру резюме, и я видел довольно много резюме. Многие резюме имеют следующие характеристики

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

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

Вот комментарии, которые я часто даю другим, чтобы пересмотреть их резюме:

Страницы резюме ограничены менее чем 2 страницами

  • Обратите внимание на использование заглавных букв в технических терминах.
  • Выделите личные моменты и расширьте содержание. Например, как найти ошибки в проекте и процесс их решения, например, как найти проблемы с производительностью, как решить проблемы с производительностью и насколько производительность в итоге улучшится, например, почему выбрана эта модель, что какова цель, и каковы преимущества перед другими и т. д. Общая идея состоит не в том, чтобы написать текущий счет, а в том, чтобы подчеркнуть свою способность решать проблемы и мыслить самостоятельно в проекте.
  • Считайте слова знакомыми и опытными, и не копайте себе яму
  • Убедитесь, что вы можете что-то сказать о каждом техническом пункте, написанном на нем, и не позволяйте интервьюеру спрашивать вас о техническом пункте, вы можете ответить только на ситуацию, что вы будете использовать API, что снизит балл

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

связанные с JS

Говорите о переменной бустинге?

При выполнении кода JS будет сгенерирована среда выполнения. Пока код не написан в функции, он находится в глобальной среде выполнения. Код в функции будет генерировать среду выполнения функции. Есть только два среды выполнения.

Далее давайте рассмотрим шаблонный пример,var

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}

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

Во время продвижения та же функция перезаписывает предыдущую функцию, и функция имеет приоритет над переменной продвижением.

b() // call b second

function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'

varбудет генерировать много ошибок, поэтому это было введено в ES6let.letнельзя использовать перед объявлением, но об этом не часто говорятletне увеличится,letОн был улучшен, и память также открыла для него место на первом этапе, но из-за особенностей этого объявления его нельзя использовать до объявления.

Разница между связыванием, вызовом и применением

Во-первых, разница между двумя верхними.

callа такжеapplyЭто все об измененииthisуказывает на. Функция та же, но способ передачи параметров другой.

За исключением первого параметра,callможет получить список параметров,applyПринимает только массив параметров.

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

bindИ роль двух других методов такая же, но метод возвращает функцию. И мы можемbindРеализовать каррирование.

Как реализовать функцию привязки

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

  • Если первый параметр не передан, по умолчаниюwindow
  • Изменен указатель this, чтобы новый объект мог выполнять функцию. Так может ли идея заключаться в том, чтобы добавить функцию к новому объекту, а затем удалить ее после выполнения?
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

Как реализовать функцию вызова

Function.prototype.myCall = function (context) {
  var context = context || window
  // 给 context 添加一个属性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 将 context 后面的参数取出来
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 删除 fn
  delete context.fn
  return result
}

Как реализовать функцию применения

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 需要判断是否存储第二个参数
  // 如果存在,就将第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}

Кратко рассказать о цепочке прототипов?

prototype

Каждая функция имеетprototypeсвойства, кромеFunction.prototype.bind(), что указывает на прототип.

Каждый объект имеет__proto__Свойство, указывающее на прототип конструктора, создавшего объект. На самом деле это свойство указывает на[[prototype]],но[[prototype]]является внутренним свойством, мы не можем получить к нему доступ, поэтому используйте_proto_посетить.

объекты могут быть__proto__найти свойства, которые не принадлежат объекту,__proto__Соединение объектов вместе образует цепочку прототипов.

Если вы хотите узнать больше о прототипах, вы можете внимательно прочитатьУглубленный анализ трудностей в прототипе.

Как определить тип объекта?

  • в состоянии пройтиObject.prototype.toString.call(xx). Таким образом, мы можем получить что-то вроде[object Type]Нить.
  • instanceofТип объекта может быть правильно оценен, потому что внутренний механизм заключается в оценке того, можно ли найти тип в цепочке прототипов объекта.prototype.

Особенности стрелки функция

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())

Стрелочные функции неthisДа, в этой функцииthisзависит только от первой функции вне его, которая не является стрелочной функциейthis. В этом примере, поскольку вызовaЭто соответствует первому случаю в предыдущем коде, поэтомуthisдаwindow. а такжеthisКак только контекст привязан, он не может быть изменен никаким кодом.

This

thisЭто концепция, которая многих может сбить с толку, но это совсем не сложно, вам нужно только запомнить несколько правил.

function foo() {
	console.log(this.a)
}
var a = 1
foo()

var obj = {
	a: 2,
	foo: foo
}
obj.foo()

// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况

// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new

Преимущества и недостатки асинхронности и ожидания

async 和 awaitпо сравнению с прямым использованиемPromise, преимущество заключается в обработкеthenЦепочку вызовов можно написать более четко и точно. Недостатком является злоупотреблениеawaitможет вызвать проблемы с производительностью, потому чтоawaitЭто заблокирует код.Возможно, асинхронный код после этого не зависит от первого, но все же должен дождаться завершения первого, что приведет к потере параллелизма кода.

Давайте посмотрим на использованиеawaitкод.

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

У вас могут возникнуть сомнения по поводу приведенного выше кода, вот принцип

  • первая функцияbВыполнить сначала, затем выполнять до тех пор, покаawait 10предыдущая переменнаяaпо-прежнему равно 0, потому что вawaitреализовано внутриgenerators,generatorsБуду держать вещи в стеке, так что на этот разa = 0был сохранен
  • потому чтоawaitэто асинхронная операция, встречающаяawaitнемедленно вернетpendingсостояниеPromiseОбъект временно возвращает контроль над исполняемым кодом, так что код вне функции может продолжать выполняться, поэтому он будет выполняться первымconsole.log('1', a)
  • В это время выполняется синхронный код, выполняется асинхронный код и используется сохраненное значение.a = 10
  • Затем идет обычный код выполнения.

принцип генератора

Generator — это новый синтаксис в ES6, который, как и Promise, можно использовать для асинхронного программирования.

// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

Из вышесказанного его можно найти в коде, плюс*После выполнения функции она имеетnextФункция, то есть функция возвращает объект после выполнения. каждый звонокnextФункция может продолжить выполнение кода, который был приостановлен. Ниже приведена простая реализация функции генератора.

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以发现通过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 并且表明下次需要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
		// 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

Promise

Промисы — это новый синтаксис в ES6, который решает проблему ада обратных вызовов.

Вы можете думать о Promise как о машине состояний. Изначальноpendingсостояние, доступ к которому можно получить через функциюresolveа такжеrejectПреобразование статусаresolvedилиrejectedСостояние, как только состояние изменилось, оно не может измениться снова.

thenФункция возвращает экземпляр Promise, а возвращаемое значение — новый экземпляр вместо предыдущего. Поскольку в спецификации Promise указано, что в дополнение кpendingсостояние, другие состояния не могут быть изменены, если возвращается один и тот же экземпляр, несколькоthenЗвонок бессмысленный.

дляthenПо сути, это можно рассматривать какflatMap

Как реализовать обещание

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决以下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 如果类型不是函数需要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,如果是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其他的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 如果 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}

Разница между == и ===, когда использовать ==

на картинке вышеtoPrimitiveЭто объект базового типа.

Вот анализ темы[] == ![] // -> true, вот почему это выражениеtrueШаг

// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true

===Используется для определения того, совпадают ли два типа и значения. В разработке, для бэкенда вернулсяcode, в состоянии пройти==судить.

вывоз мусора

V8 реализует точную сборку мусора, а алгоритм сборки мусора использует механизм сборки мусора на основе поколений. Поэтому V8 делит память (кучу) на две части: молодое поколение и старое поколение.

алгоритм нового поколения

Объекты нового поколения обычно имеют короткое время выживания и используют алгоритм Scavenge GC.

В пространстве нового поколения пространство памяти разделено на две части: пространство «От» и пространство «В». Из этих двух пространств одно должно быть использовано, а другое свободно. Вновь выделенные объекты будут помещены в пространство From, и когда пространство From будет заполнено, запустится GC нового поколения. Алгоритм проверит уцелевшие объекты в пространстве From и скопирует их в пространство To, а если есть мертвые объекты, то они будут уничтожены. Когда копирование завершено, пространство From и пространство To меняются местами, и GC завершается.

Алгоритмы старого поколения

Объекты в старом поколении обычно сохраняются в течение длительного времени и имеют большое количество.Используются два алгоритма, а именно алгоритм удаления меток и алгоритм сжатия меток.

Прежде чем говорить об алгоритме, поговорим о том, при каких обстоятельствах объект появится в пространстве старой генерации:

  • Независимо от того, подвергались ли объекты нового поколения алгоритму очистки один раз, если да, то объекты будут перемещены из пространства нового поколения в пространство старого поколения.
  • Объекты в пространстве To составляют более 25% размера. В этом случае, чтобы не влиять на выделение памяти, объект будет перемещен из пространства молодого поколения в пространство старого поколения.

Пространство в старом поколении очень сложное, есть следующие пробелы

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

В старом поколении алгоритм маркировки-развертки запускается первым в следующих случаях:

  • Когда пространство не разделено на блоки
  • Объект в пространстве превышает определенный предел
  • Пространство не гарантирует, что объекты молодого поколения будут перемещены в старое поколение.

На этом этапе все объекты в куче просматриваются, и живые объекты помечаются.После завершения маркировки все неотмеченные объекты уничтожаются. При маркировке больших пар памяти на завершение маркировки может уйти несколько сотен миллисекунд. Это может вызвать некоторые проблемы с производительностью. Чтобы исправить это, в 2011 году V8 переключился с флагов остановки мира на добавочные флаги. Во время инкрементной маркировки GC разбивает работу по маркировке на более мелкие модули, позволяя логике приложения JS выполняться какое-то время между модулями, чтобы приложение не зависало. Но в 2018 году произошел еще один крупный прорыв в технологии GC, который называется параллельной маркировкой. Этот метод позволяет запускать JS, когда сборщик мусора сканирует и помечает объекты, и вы можете щелкнутьблогчитай внимательно.

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

Закрытие

Определение замыкания простое: функция A возвращает функцию B, а функция B использует переменные функции A, функция B называется замыканием.

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

Вам интересно, почему функция А удалила стек вызовов и почему функция Б все еще может ссылаться на переменные в функции А. Потому что переменные в функции A в это время хранятся в куче. Современные JS-движки могут определить, какие переменные нужно хранить в куче, а какие — в стеке, с помощью escape-анализа.

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

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

Во-первых, потому чтоsetTimeoutЭто асинхронная функция, и все циклы будут выполняться первыми.iЭто 6, поэтому он выведет кучу из 6.

Есть два решения, первое — использовать замыкания

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

Второй заключается в использованииsetTimeoutтретий параметр

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}

В-третьих, использоватьletопределениеiохватывать

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

потому что дляletНапример, он создает область действия блока, эквивалентную

{ // 形成块级作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}

Различия между базовыми типами данных и ссылочными типами в хранилище

Первое хранится в стеке, второе — в куче.

В чем разница между браузером Eventloop и Node

Как мы все знаем, JS — это неблокирующий однопоточный язык, потому что изначально JS был создан для взаимодействия с браузером. Если JS — многопоточный язык, могут возникнуть проблемы при обработке DOM в несколько потоков (новые узлы добавляются в одном потоке, а узлы удаляются в другом).Конечно, мы можем ввести блокировки чтения-записи на решить эту проблему.

JS генерирует среду выполнения во время выполнения, которая будет добавлена ​​в стек выполнения. Если вы столкнетесь с асинхронным кодом, он будет зависать и присоединяться к очередям задач (с различными задачами). Как только стек опустеет, Event Loop достанет код, который вам нужно выполнить, из очереди TASK и поместит его в стек выполнения, поэтому в природе асинхронное или синхронное поведение в JS.

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');

Хотя приведенный выше кодsetTimeoutЗадержка равна 0, что по-прежнему является асинхронным. Это связано с тем, что стандарт HTML5 предусматривает, что второй параметр этой функции не должен быть меньше 4 миллисекунд, и нехватка будет автоматически увеличиваться. такsetTimeoutвсе еще будетscript endраспечатать потом.

Различные источники задач будут назначены разным очередям задач, а источники задач можно разделить на микрозадачи и макрозадачи. В спецификации ES6 микрозадачи называютсяjobs, макрозадача называетсяtask.

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

Хотя приведенный выше кодsetTimeoutнаписать наPromiseраньше, а потомуPromiseэто микрозадачиsetTimeoutЭто макрозадача, поэтому будет указанная выше печать.

Микрозадачи включаютprocess.nextTick,promise,Object.observe,MutationObserver

Макрозадачи включаютscript,setTimeout,setInterval,setImmediate,I/O,UI rendering

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

Итак, правильная последовательность цикла событий выглядит так

  1. Выполнение синхронного кода, который является задачей макроса
  2. Стек выполнения пуст, проверьте, есть ли микрозадачи для выполнения
  3. Выполнить все микрозадачи
  4. Отрисовка пользовательского интерфейса при необходимости
  5. Затем запустите следующий раунд цикла событий и выполните асинхронный код в задаче макроса.

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

Цикл событий в узле

Цикл событий в Node не такой, как в браузере.

Цикл событий Node разделен на 6 этапов, которые последовательно выполняются многократно.

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

timer

Фаза таймеров будет выполнятьсяsetTimeoutа такжеsetInterval

ОдинtimerУказанное время не является точным, но обратный вызов выполняется как можно скорее после достижения времени, возможно, с задержкой, поскольку система выполняет другие транзакции.

Время для нижней границы имеет диапазон:[1, 2147483647], если установленное время не находится в этом диапазоне, оно будет установлено на 1.

**I/O **

Фаза ввода-вывода будет выполняться в дополнение к событию закрытия, таймерам иsetImmediateПерезвоните

idle, prepare

Внутренняя реализация фазы ожидания, подготовки

poll

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

  1. Исполненный таймер
  2. Выполнение событий в очереди опроса

И когда в опросе нет таймера, будут найдены следующие две вещи

  • Если очередь опроса не пуста, она будет проходить через очередь обратного вызова и выполняться синхронно до тех пор, пока очередь не станет пустой или не будет достигнуто системное ограничение.
  • Если очередь опроса пуста, происходят две вещи
    • Если естьsetImmediateНеобходимо выполнить, фаза опроса остановится и перейдет к фазе проверки для выполненияsetImmediate
    • если нетsetImmediateНеобходимо выполнить, будет ждать добавления обратного вызова в очередь и немедленно выполнить обратный вызов

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

check

проверка выполнения фазыsetImmediate

close callbacks

Фаза закрытых обратных вызовов выполняет событие закрытия

А в Node порядок выполнения таймера в некоторых случаях случайный

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

Конечно, в этом случае порядок выполнения тот же

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输出一定是 setImmediate,setTimeout

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

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2

в узлеprocess.nextTickБудет выполняться перед другими микрозадачами.

setTimeout(() => {
  console.log("timer1");

  Promise.resolve().then(function() {
    console.log("promise1");
  });
}, 0);

process.nextTick(() => {
  console.log("nextTick");
});
// nextTick, timer1, promise1

Ошибка обратного отсчета setTimeout

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

Ниже приведена относительно подготовленная реализация обратного отсчета.

var period = 60 * 1000 * 60 * 2
var startTime = new Date().getTime();
var count = 0
var end = new Date().getTime() + period
var interval = 1000
var currentInterval = interval

function loop() {
  count++
  var offset = new Date().getTime() - (startTime + count * interval); // 代码执行所消耗的时间
  var diff = end - new Date().getTime()
  var h = Math.floor(diff / (60 * 1000 * 60))
  var hdiff = diff % (60 * 1000 * 60)
  var m = Math.floor(hdiff / (60 * 1000))
  var mdiff = hdiff % (60 * 1000)
  var s = mdiff / (1000)
  var sCeil = Math.ceil(s)
  var sFloor = Math.floor(s)
  currentInterval = interval - offset // 得到下一次循环所消耗的时间
  console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) // 打印 时 分 秒 代码执行时间 下次循环间隔

  setTimeout(loop, currentInterval)
}

setTimeout(loop, currentInterval)

Стабилизатор

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

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

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

Давайте сначала посмотрим на карманную версию анти-тряски, чтобы понять реализацию анти-тряски:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数

Это простой вариант антишейка, но он ущербен, этот антишейк можно назвать только в конце. Общий анти-встряска будет иметь немедленную опцию, указывающую, следует ли немедленно звонить. Разница между ними, например:

  • Например, когда поисковая система ищет проблему, мы, конечно, надеемся, что пользователь вызовет интерфейс запроса только после ввода последнего слова.延迟执行Функция debounce, которая всегда вызывается после срабатывания серии функций (интервалы меньше ожидания).
  • Например, когда пользователь щелкает звездочку на карте интервью, мы надеемся, что пользователь вызовет интерфейс, когда пользователь нажмет первый щелчок, и изменит внешний вид кнопки звездочки после успеха, и пользователь сможет немедленно получить обратную связь о том, звездочка успешна. Эта ситуация применима.立即执行Функция защиты от сотрясения, она всегда вызывается в первый раз, и следующий вызов должен иметь интервал времени больше, чем ожидание от предыдущего вызова до срабатывания.

Реализуем функцию защиты от встряски с возможностью немедленного исполнения.

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的情况下,函数会在延迟函数中执行
    // 使用到之前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 如果没有创建延迟执行函数(later),就创建一个
    if (!timer) {
      timer = later()
      // 如果是立即执行,调用函数
      // 否则缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    // 这样做延迟函数会重新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

Общая функция не трудно достичь, чтобы обобщить.

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

уменьшение размерности массива

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]

Если вы хотите полностью уменьшить размер многомерного массива, вы можете добиться этого

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
  : [arr]

flattenDeep([1, [[2], [3, [4]], 5]])

глубокая копия

Эта проблема обычно может быть решена путемJSON.parse(JSON.stringify(object))решать.

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

Но этот метод также имеет ограничения:

  • будет игнорироватьundefined
  • будет игнорироватьsymbol
  • не могу сериализовать функцию
  • не может разрешать объекты с циклическими ссылками
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

Если у вас есть такой циклический объект ссылки, вы обнаружите, что вы не можете выполнить глубокое копирование с помощью этого метода.

При встрече с функциямиundefinedилиsymbol, объект не может быть сериализован нормально

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

Вы обнаружите, что в приведенном выше случае метод игнорирует функцию иundefined.

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

Если объект, который вы хотите скопировать, имеет встроенные типы и не имеет функций, вы можете использоватьMessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
  const clone = await structuralClone(obj)
})()

Разница между typeof и instanceof

typeofДля примитивных типов, кромеnullоба отображают правильный тип

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined

typeofДля объектов отображаются все, кроме функцийobject

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

дляnullНапример, хотя это примитивный тип, он покажетobject, это давний баг

typeof null // 'object'

ПС: Почему так происходит? Потому что в начальной версии JS использовалась 32-битная система, и информация о типе переменной хранилась в младших битах из соображений производительности.000Однако, начиная представлять объектnullпредставлен как все нули, поэтому он ошибочно оценивается какobject. Хотя текущий внутренний код оценки типа изменился, ошибка была передана.

instanceofТип объекта может быть правильно оценен, потому что внутренний механизм заключается в оценке того, можно ли найти тип в цепочке прототипов объекта.prototype.

Мы также можем попытаться достичьinstanceof

function instanceof(left, right) {
    // 获得类型的原型
    let prototype = right.prototype
    // 获得对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}

Связанные с браузером

Разница между cookie и localSrorage, session, indexDB

характеристика cookie localStorage sessionStorage indexDB
жизненный цикл данных Обычно генерируется сервером, время истечения срока действия может быть установлено сохраняется, если не очистить Очистить при закрытии страницы сохраняется, если не очистить
размер хранилища данных 4K 5M 5M неограниченный
Связь с сервером Он будет каждый раз переноситься в заголовок, что влияет на производительность запроса. не присоединяюсь не присоединяюсь не присоединяюсь

Как видно из вышеприведенной таблицы,cookieОн устарел для хранения. Если вам не нужно много места для хранения данных, вы можете использоватьlocalStorageа такжеsessionStorage. Попробуйте использовать данные, которые не сильно меняютсяlocalStorageхранения, в противном случае вы можете использоватьsessionStorageместо хранения.

дляcookie, мы также должны обратить внимание на безопасность.

Атрибуты эффект
value Если он используется для сохранения состояния входа пользователя, значение должно быть зашифровано, а идентификатор пользователя в виде обычного текста использовать нельзя.
http-only Доступ к файлам cookie через JS невозможен, что снижает вероятность XSS-атак.
secure Может передаваться только в запросах с протоколом HTTPS.
same-site Оговорено, что браузеры не могут хранить файлы cookie в междоменных запросах, чтобы уменьшить атаки CSRF.

Как узнать, закончилась ли загрузка страницы?

Событие Load запускается, чтобы указать, что DOM, CSS, JS и изображения на странице загружены.

Событие DOMContentLoaded запускается, чтобы указать, что исходный HTML-код полностью загружен и проанализирован, не дожидаясь загрузки CSS, JS и изображений.

Как решить междоменный

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

Мы можем решить междоменные проблемы с помощью следующих общих методов.

JSONP

Принцип JSONP очень прост, то есть использовать<script>Ярлыки не имеют междоменных ограничений. пройти через<script>Тег указывает на адрес, к которому необходимо получить доступ, и обеспечивает функцию обратного вызова для получения данных, когда требуется связь.

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>    

JSONP прост в использовании и имеет хорошую совместимость, но ограничен толькоgetпросить.

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

function jsonp(url, jsonpCallback, success) {
  let script = document.createElement("script");
  script.src = url;
  script.async = true;
  script.type = "text/javascript";
  window[jsonpCallback] = function(data) {
    success && success(data);
  };
  document.body.appendChild(script);
}
jsonp(
  "http://xxx",
  "callback",
  function(value) {
    console.log(value);
  }
);

CORS

CORS требует поддержки как браузера, так и серверной части. IE 8 и 9 нужно пройтиXDomainRequestреализовать.

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

Настройки сервераAccess-Control-Allow-OriginCORS можно включить. Этот атрибут указывает, какие доменные имена могут получить доступ к ресурсу. Если установлен подстановочный знак, это означает, что все веб-сайты могут получить доступ к ресурсу.

document.domain

Этот метод можно использовать только в том случае, если доменные имена второго уровня совпадают, напримерa.test.comа такжеb.test.comотносится к этому методу.

Просто добавьте на страницуdocument.domain = 'test.com'Указывает, что доменные имена второго уровня одинаковы для достижения междоменного

postMessage

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

// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.origin; 
    if (origin === 'http://test.com') {
        console.log('验证通过')
    }
});

Что такое брокер событий

Если дочерние узлы в узле генерируются динамически, то дочерний узел должен зарегистрировать событие на родительском узле, если ему необходимо зарегистрироваться.

<ul id="ul">
	<li>1</li>
    <li>2</li>
	<li>3</li>
	<li>4</li>
	<li>5</li>
</ul>
<script>
	let ul = document.querySelector('#ul')
	ul.addEventListener('click', (event) => {
		console.log(event.target);
	})
</script>

По сравнению с регистрацией событий непосредственно в цели метод делегирования событий имеет следующие преимущества.

  • сохранить память
  • Нет необходимости отменять регистрацию событий для дочерних узлов

Service worker

Сервисные работники по сути действуют как прокси-сервер между веб-приложением и браузером, а также могут действовать как прокси-сервер между браузером и сетью, когда сеть доступна. Они предназначены (среди прочего) для обеспечения эффективного автономного взаимодействия, перехвата сетевых запросов и выполнения соответствующих действий в зависимости от того, доступна ли сеть и находится ли обновленный ресурс на сервере. Они также предоставляют доступ к push-уведомлениям и API фоновой синхронизации.

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

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register("sw.js")
    .then(function(registration) {
      console.log("service worker 注册成功");
    })
    .catch(function(err) {
      console.log("servcie worker 注册失败");
    });
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
  e.waitUntil(
    caches.open("my-cache").then(function(cache) {
      return cache.addAll(["./index.html", "./index.js"]);
    })
  );
});

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener("fetch", e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response;
      }
      console.log("fetch source");
    })
  );
});

Откройте страницу, которую можно найти в инструментах разработчикаApplicationУбедитесь, что Service Worker запущен.

кеш браузера

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

Как правило, существует два типа стратегий кэширования браузера: сильное кэширование и согласованное кэширование.

Сильный кеш

Реализовать сильное кэширование можно с помощью двух заголовков ответа:Expiresа такжеCache-Control. Сильное кэширование означает, что запросы не требуются во время кэширования,state codeза 200

Expires: Wed, 22 Oct 2018 08:41:00 GMT

Expiresявляется артефактом HTTP/1.0, что означает, что ресурсы будутWed, 22 Oct 2018 08:41:00 GMTПосле истечения срока его необходимо запросить снова. а такжеExpiresОграничено местным временем, если местное время изменено, кеш может стать недействительным.

Cache-control: max-age=30

Cache-ControlПоявился в HTTP/1.1 с более высоким приоритетом, чемExpires. Это свойство указывает, что срок действия ресурса истечет через 30 секунд, и его необходимо будет запросить снова.

Согласовать кеш

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

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

Last-Modified и If-Modified-Since

Last-ModifiedУказывает дату последнего изменения локального файла,If-Modified-SinceбудуLast-ModifiedЗначение отправляется на сервер, запрашивая сервер, был ли ресурс обновлен после даты, и если есть обновление, новый ресурс будет отправлен обратно.

Но если вы откроете файл кеша локально, это вызоветLast-Modifiedбыл изменен, поэтому появился в HTTP/1.1ETag.

ETag и If-None-Match

ETagПодобно снятию отпечатков файлов,If-None-Matchбудет текущимETagОтправлено на сервер для запроса ресурсаETagЕсть ли изменение, если есть изменение, новый ресурс будет отправлен обратно. а такжеETagсоотношение приоритетовLast-Modifiedвысоко.

Выберите подходящую стратегию кэширования

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

  • Для некоторых ресурсов, которые не нужно кэшировать, вы можете использоватьCache-control: no-store, указывая на то, что ресурс не нужно кэшировать
  • Для часто меняющихся ресурсов можно использоватьCache-Control: no-cacheи сотрудничатьETagПри использовании это означает, что ресурс кэшируется, но каждый раз отправляется запрос с вопросом, обновлен ли ресурс.
  • Для файлов кода обычно используютCache-Control: max-age=31536000И используйте его с кешем политики, а затем отпечатайте файл.Как только имя файла изменится, новый файл будет загружен немедленно.

Проблемы с производительностью браузера

Перекрасить и переплавить

Перерисовка и перекомпоновка — небольшая часть этапа рендеринга, но эти два этапа оказывают большое влияние на производительность.

  • Перерисовка — это когда узел должен изменить свой внешний вид, не влияя на макет, например, изменитьcolorэто называется перекраска
  • Перекомпоновка — это когда необходимо изменить макет или геометрические свойства, и это называется перекомпоновкой.

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

Таким образом, следующие действия могут вызвать проблемы с производительностью:

  • изменить размер окна
  • изменить шрифт
  • Добавить или удалить стили
  • изменение текста
  • положение или поплавок
  • коробочная модель

Чего многие люди не знают, так это того, что перерисовка и перекомпоновка на самом деле связаны с циклом событий.

  1. Когда цикл событий завершит выполнение микрозадач, он определит, нужно ли обновлять документ. Поскольку браузер имеет частоту обновления 60 Гц, он обновляется только каждые 16 мс.
  2. а затем определить, является лиresizeилиscroll, если есть, это вызовет событие, поэтомуresizeа такжеscrollСобытие также запускается один раз не менее 16 мс и имеет собственную функцию регулирования.
  3. Определить, запущен ли медиа-запрос
  4. Обновить анимацию и отправить событие
  5. Определить, есть ли событие полноэкранного режима
  6. воплощать в жизньrequestAnimationFrameПерезвоните
  7. воплощать в жизньIntersectionObserverОбратный вызов, этот метод используется для определения того, виден ли элемент, его можно использовать для ленивой загрузки, но совместимость плохая
  8. обновить интерфейс
  9. Это то, что можно сделать в одном кадре. Если в кадре есть свободное время, он выполнитrequestIdleCallbackПерезвоните.

Вышеприведенный контент взят изHTML-документ

Уменьшите количество перерисовок и перекомпоновок
  • использоватьtranslateзаменятьtop

    <div class="test"></div>
    <style>
    	.test {
    		position: absolute;
    		top: 10px;
    		width: 100px;
    		height: 100px;
    		background: red;
    	}
    </style>
    <script>
    	setTimeout(() => {
            // 引起回流
    		document.querySelector('.test').style.top = '100px'
    	}, 1000)
    </script>
    
  • использоватьvisibilityзаменятьdisplay: none, потому что первое приведет только к перерисовке, второе вызовет перекомпоновку (изменение макета)

  • После изменения в автономном режиме, например: сначала до DOMdisplay:none(с перекомпоновкой), затем вы изменяете его 100 раз, прежде чем показывать

  • Не помещайте значение свойства узла DOM в цикл как переменную в цикле.

    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会导致回流,因为需要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
    
  • Не используйте макет таблицы, небольшое изменение может привести к перестановке всей таблицы.

  • Выбор скорости реализации анимации, чем быстрее скорость анимации, тем больше время перекомпоновки, вы также можете использоватьrequestAnimationFrame

  • Селекторы CSS ищутся справа налево, чтобы избежать слишком глубокой глубины DOM.

  • Превратите часто выполняемые анимации в слои, которые предотвратят перекомпоновку узла и влияние на другие элементы. например, дляvideoТег, браузер автоматически включает узел в слой.

Оптимизация изображения

Рассчитать размер изображения

Для изображения 100 * 100 пикселей на изображении 10 000 пикселей.Если значение каждого пикселя хранится в RGBA, то каждый пиксель имеет 4 канала, и каждый канал имеет 1 байт (8 бит = 1 байт), поэтому размер изображения около 39КБ (10000*1*4/1024).

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

Зная, как рассчитать размер изображения, вы должны иметь 2 идеи, как оптимизировать изображение:

  • уменьшить количество пикселей
  • Уменьшите цвет, который может отображать каждый пиксель
Оптимизация загрузки изображений
  1. Нет фотографий. Много раз используется множество декоративных изображений, фактически такие декоративные изображения могут быть заменены CSS.
  2. Для мобильного терминала ширина экрана настолько мала, что нет необходимости загружать исходное изображение, чтобы тратить пропускную способность. Как правило, изображения загружаются через CDN, и можно рассчитать ширину экрана, а затем запросить соответствующие обрезанные изображения.
  3. Миниатюры используют формат base64.
  4. Объединение нескольких файлов значков в одно изображение (изображение спрайта)
  5. Выберите правильный формат изображения:
    • Для браузеров, которые могут отображать формат WebP, попробуйте использовать формат WebP. Поскольку формат WebP имеет лучший алгоритм сжатия данных изображения, он может обеспечить меньший размер изображения и качество изображения, неразличимое невооруженным глазом.Недостатком является плохая совместимость.
    • Для небольших изображений используется формат PNG, но для большинства изображений, таких как значки, можно использовать формат SVG.
    • Фотографии используют JPEG

Другие оптимизации файлов

  • CSS-файл вheadсередина
  • Включить сжатие файлов на сервере
  • Будуscriptэтикетка наbodyвнизу, потому что выполнение файла JS блокирует рендеринг. Конечно, вы также можетеscriptотметьте где угодно и добавьтеdefer, указывающее, что файл будет загружаться параллельно, но будет выполняться последовательно после завершения синтаксического анализа HTML. Для файлов JS без каких-либо зависимостей вы можете добавитьasync, что указывает на то, что процесс загрузки и рендеринга последующих элементов документа будет выполняться параллельно и не по порядку с загрузкой и выполнением JS-файлов.
  • Выполнение слишком длинного JS-кода приведет к блокировке рендеринга. Для кода, который требует много времени для расчета, рассмотрите возможность использованияWebworker.WebworkerЭто позволяет нам открыть отдельный поток для выполнения скрипта, не влияя на рендеринг.

CDN

Статические ресурсы должны быть максимально загружены с использованием CDN.Поскольку браузеры имеют верхний предел одновременных запросов для одного доменного имени, вы можете рассмотреть возможность использования нескольких доменных имен CDN. Для загрузки статических ресурсов CDN следует учитывать, что доменное имя CDN должно отличаться от имени основного сайта, иначе каждый запрос будет приносить cookie основного сайта.

Оптимизация вашего проекта с помощью Webpack

  • Для Webpack4 используйте производственный режим для проектов упаковки, который автоматически включает сжатие кода.
  • Используйте модули ES6, чтобы включить встряхивание дерева, метод, удаляющий неиспользуемый код.
  • Оптимизируйте изображение, для маленького изображения вы можете использовать метод base64 для записи в файл
  • Разделите код в соответствии с маршрутом, чтобы обеспечить загрузку по требованию.
  • Добавьте хэш к имени упакованного файла, чтобы реализовать файл кеша браузера.

Webpack

Оптимизация скорости упаковки

  • Уменьшить область поиска файлов
    • например по псевдониму
    • тест загрузчика, включить и исключить
  • Webpack4 сжимает параллелизм по умолчанию
  • Параллельные вызовы Happypack
  • babel также может кэшировать компиляцию

Принцип Бабеля

Суть компилятора, когда код конвертируется в строку для генерации AST, AST трансформируется и в итоге генерируется новый код

  • Разделено на три этапа: лексический анализ генерирует токен, синтаксический анализ генерирует AST, проходит AST, преобразует соответствующие узлы в соответствии с плагинами и, наконец, преобразует AST в код.

Как реализовать плагин

  • Вызовите функцию применения плагина и передайте объект компилятора
  • Прослушивание событий через объект Compiler

Например, если вы хотите реализовать плагин, который завершает конец команды

class BuildEndPlugin {
  apply (compiler) {
    const afterEmit = (compilation, cb) => {
      cb()
      setTimeout(function () {
        process.exit(0)
      }, 1000)
    }

    compiler.plugin('after-emit', afterEmit)
  }
}

module.exports = BuildEndPlugin

Рамка

Жизненный цикл реакции

Механизм Fiber был представлен в версии V16. Этот механизм в определенной степени влияет на некоторые вызовы жизненного цикла, а также вводит два новых API для решения проблемы.

В предыдущих версиях, если у вас был очень сложный составной компонент, а затем вы изменилиstate, то стек вызовов может быть очень длинным

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

Fiber — это, по сути, кадр виртуального стека, и новый планировщик будет свободно планировать эти кадры в соответствии с приоритетом, тем самым изменяя предыдущий синхронный рендеринг на асинхронный рендеринг и вычисляя обновления в сегментах, не влияя на работу.

В React есть собственный набор логики для того, как различать приоритеты. Для чего-то, что работает в режиме реального времени, например анимации, то есть, когда она должна рендериться каждые 16 мс, чтобы гарантировать, что она не зависнет, React будет приостанавливать обновление каждые 16 мс (в пределах) и возвращаться, чтобы продолжить рендеринг анимации.

Для асинхронного рендеринга теперь есть два этапа рендеринга:reconciliationа такжеcommit. Первый процесс можно прервать, второй нельзя поставить на паузу, и интерфейс будет обновляться до его завершения.

Reconciliationсцена

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commitсцена

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

потому чтоreconciliationФазы могут быть прерваны, поэтомуreconciliationФункции жизненного цикла, которые будут выполняться на этапе, могут вызываться несколько раз, вызывая ошибки. Таким образом, дляreconciliationНесколько функций, вызываемых сценой, кромеshouldComponentUpdateДругих следует избегать, и для решения этой проблемы в V16 были введены новые API.

getDerivedStateFromPropsзаменитьcomponentWillReceiveProps, функция инициализируется иupdateназывается, когда

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

getSnapshotBeforeUpdateзаменитьcomponentWillUpdate, функция будет вupdateВызывается после обновления DOM для чтения последних данных DOM.

Рекомендации по использованию функции жизненного цикла V16

class ExampleComponent extends React.Component {
  // 用于初始化 state
  constructor() {}
  // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
  // 因为该函数是静态函数,所以取不到 `this`
  // 如果需要对比 `prevProps` 需要单独在 `state` 中维护
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判断是否需要更新组件,多用于组件性能优化
  shouldComponentUpdate(nextProps, nextState) {}
  // 组件挂载后调用
  // 可以在该函数中进行请求或者订阅
  componentDidMount() {}
  // 用于获得最新的 DOM 数据
  getSnapshotBeforeUpdate() {}
  // 组件即将销毁
  // 可以在此处移除订阅,定时器等等
  componentWillUnmount() {}
  // 组件销毁后调用
  componentDidUnMount() {}
  // 组件更新后调用
  componentDidUpdate() {}
  // 渲染组件函数
  render() {}
  // 以下函数不建议使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}

setState

setStateЭто API, который часто используется в React, но имеет некоторые проблемы и может привести к ошибкам, основная причина в том, что этот API является асинхронным.

первыйsetStateзвонок не сразу вызываетstateизменения, и если вы звоните более чем по одному за разsetState, результат может быть не таким, как вы ожидаете.

handle() {
  // 初始化 `count` 为 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}

Во-первых, он печатает 0 оба раза, потому чтоsetStateЭто асинхронный API, который будет выполняться только после завершения выполнения синхронного кода.setStateПричина асинхронности, я думаю,setStateЭто может привести к перерисовке DOM. Если вы вызываете его один раз и сразу же перерисовываете, то вызов его несколько раз приведет к ненужной потере производительности. Если он предназначен для асинхронности, несколько вызовов могут быть поставлены в очередь, а процесс обновления может быть унифицирован в нужное время.

Второй, хотя звонил триждыsetState,ноcountвсе еще 1. Потому что несколько вызовов будут объединены в один, только когда обновление закончитсяstateизменится, три вызова эквивалентны следующему коду

Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)

Конечно, вы также можете позвонить три раза следующими способами:setStateсделатьcountна 3

handle() {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}

Если вы хотите звонить каждый разsetStateпосле получения правильногоstate, что может быть достигнуто с помощью следующего кода

handle() {
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
        console.log(this.state)
    })
}

Принцип nextTick Vue

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

До Vue 2.4 использовались микрозадачи, но приоритет микрозадач слишком высок, в некоторых случаях это может быть быстрее, чем всплытие событий, но если используются макрозадачи, могут быть проблемы с производительностью рендеринга. Так что в новой версии микрозадачи будут использоваться по умолчанию, а вот макрозадачи будут использоваться в особых случаях, например v-on.

Для реализации макрозадач он сначала определит, можно ли его использоватьsetImmediate, если нет, перейти наMessageChannel, если ничего из вышеперечисленного не работает, используйтеsetTimeout

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (
  typeof MessageChannel !== 'undefined' &&
  (isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

nextTickВ то же время он также поддерживает использование Promise и будет определять, реализован ли Promise.

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  // 将回调函数整合进一个数组中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // 判断是否可以使用 Promise 
  // 可以的话给 _resolve 赋值
  // 这样回调函数就能以 promise 的方式调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Жизненный цикл Vue

Функции жизненного цикла — это функции-ловушки, которые запускаются при инициализации компонента или обновлении данных.

Во время инициализации вызывается следующий код, и жизненный цикл проходит черезcallHookназывается

Vue.prototype._init = function(options) {
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate') // 拿不到 props data
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
}

Можно найти в вышеуказанном коде,beforeCreateПри вызове нельзя получить данные в props или data, так как инициализация этих данных находится вinitStateсередина.

Далее будет выполнена функция монтирования

export function mountComponent {
    callHook(vm, 'beforeMount')
    // ...
    if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
    }
}

beforeMountОн выполняется перед монтированием, затем начинает создавать VDOM и заменяет его реальным DOM и, наконец, выполняетmountedкрюк. Здесь будет логика суждения, если она внешняя.new Vue({})если его не существует$vnodeПоэтому прямое исполнениеmountedмонтировать. Если есть подкомпоненты, то подкомпоненты будут монтироваться рекурсивно, только когда все подкомпоненты будут смонтированы, будет выполнен хук монтирования корневого компонента.

Далее идет функция ловушки, которая будет вызываться при обновлении данных.

function flushSchedulerQueue() {
  // ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 调用 beforeUpdate
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        )
        break
      }
    }
  }
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks(queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

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

Наконец, функция ловушки, которая уничтожает компонент

Vue.prototype.$destroy = function() {
  // ...
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown watchers
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

Вызывается перед выполнением операции уничтоженияbeforeDestroyПерехватите функцию, а затем выполните серию операций уничтожения.Если есть подкомпоненты, подкомпоненты также будут рекурсивно уничтожены.После того, как все подкомпоненты будут уничтожены, корневой компонент будет выполнен.destroyedфункция крючка.

Двусторонняя привязка Vue

  • При инициализации свойств данных рекурсивно связывайте каждое свойство в обоих направлениях.Для массивов вы получите функцию перезаписи прототипа для ручной отправки обновлений. Поскольку функция не может отслеживать изменения данных, сравните ее с прокси.
  • В дополнение к вышеупомянутым функциям массива изменение данных массива по индексу или добавление новых свойств к объекту не может быть инициировано.Вам необходимо использовать встроенную функцию set, которая также вручную отправляет обновления внутри.
  • Когда компонент смонтирован, будет создан экземпляр наблюдателя рендеринга, и будет передан обратный вызов обновления компонента. Во время создания экземпляра объект значения в шаблоне оценивается, запуская сбор зависимостей. Перед запуском зависимости сохраняется текущий наблюдатель отрисовки, который используется для восстановления наблюдателя родительского компонента, когда компонент содержит дочерние компоненты. После запуска сбора зависимостей удаляются ненужные зависимости, оптимизируется производительность и предотвращается повторный рендеринг в ненужных местах.
  • Изменение значения вызовет обновление зависимостей, и все собранные зависимости будут удалены и помещены в nextTick для унифицированного выполнения. В процессе выполнения наблюдатели будут отсортированы первыми, а рендеринг будет выполняться последним. Сначала выполните функцию ловушки beforeupdate, а затем выполните обратный вызов наблюдателя. В процессе выполнения обратного вызова часы могут быть снова задвинуты, потому что в обратном вызове происходит переназначение, судя по бесконечному циклу.

принцип v-модели

  • v:model преобразует код во время компиляции шаблона
  • Суть v-model заключается в :value и v-on, но они немного отличаются. Под контролем ввода есть два прослушивателя событий.При вводе китайского языка назначение данных срабатывает только при выводе китайского языка.
  • v-model и :bind используются одновременно, первый имеет более высокий приоритет, если :value будет конфликт
  • v-модель также может использоваться для связи родитель-потомок из-за синтаксического сахара

Разница между просмотром и вычислением и сценариями применения

  • Первые являются вычисляемыми свойствами, которые полагаются на другие свойства для вычисления значений. И значение компьютера кэшируется, и рендеринг запускается только при изменении вычисленного значения. Последний выполнит обратный вызов при изменении значения.
  • компьютер представляет собой простой расчет, подходящий для рендеринга страниц. watch подходит для некоторой сложной бизнес-логики
  • У первого есть два наблюдателя, наблюдатель за компьютером и наблюдатель за рендерингом. Деконструкция вычисленного значения для рендеринга рендеринга триггера обновления дистрибутива Watcher

Общение родителей и детей Vue

  • использоватьv-modelРеализовать отца к сыну, сына к отцу. Поскольку v-model разрешается в :value и :input по умолчанию
  • отец сыну
    • пройти черезprops
    • пройти через$childrenПолучите доступ к массиву подкомпонентов, обратите внимание, что массив не в порядке
    • Для многоуровневой передачи родитель-потомок вы можете использоватьv-bind={$attrs}, отфильтровать реквизиты, переданные в родительский компонент, но не нужные дочернему компоненту с помощью объекта
    • $listens содержит родительскую область (исключая.nativeмодификатор)v-onпрослушиватель событий.
  • сын к отцу
    • Родительский компонент передает функцию дочернему компоненту, а дочерний компонент передает функцию$emitвызывать
    • Измените родительский компонентprops
    • пройти через$parentдоступ к родительскому компоненту
    • .sync
  • Параллельные компоненты
    • EventBus
  • Vuex решает все

Принцип маршрутизации

Фронтальная маршрутизация на самом деле очень проста для реализации. Сущность состоит в том, чтобы отслеживать изменения URL, а затем сопоставить правила маршрутизации, отображать соответствующую страницу и не нужно обновлять. В настоящее время существует только два способа реализации маршрутизации, используемой одной страницей

  • хэш-режим
  • режим истории

www.test.com/#/URL-адрес хэша, когда#При изменении последующего хеш-значения данные не будут запрашиваться с сервера.hashchangeСобытия для прослушивания изменений в URL, чтобы перейти на страницу.

Режим истории — это новая функция в HTML5, которая красивее, чем Hash URL.

MVVM

MVVM состоит из следующих трех компонентов

  • Вид: интерфейс
  • Модель: модель данных
  • ViewModel: как мост, отвечающий за связь View и Model.

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

В MVVM пользовательский интерфейс управляется данными. После изменения данных соответствующий пользовательский интерфейс будет соответствующим образом обновлен. Если пользовательский интерфейс изменится, соответствующие данные также будут изменены. Таким образом, вы можете заботиться только о потоке данных при бизнес-обработке, не имея возможности напрямую работать со страницей. ViewModel заботится только об обработке данных и бизнесе, а не о том, как View обрабатывает данные.В этом случае View и Model могут быть независимыми, ни одна из сторон не обязательно должна изменять другую сторону, а некоторая повторно используемая логика помещается в ViewModel , и несколько представлений могут повторно использовать эту ViewModel.

В MVVM ядром является двусторонняя привязка данных, такая как обнаружение грязных данных в Angluar и перехват данных в Vue.

обнаружение грязных данных

Когда заданное событие срабатывает, оно войдет в режим обнаружения грязных данных, а затем вызовет$digestПереберите все наблюдатели данных, чтобы определить, отличается ли текущее значение от предыдущего значения. Если обнаружено изменение, он вызовет$watchфункцию, затем вызовите снова$digestЦикл, пока не будет найдено никаких изменений. Цикл не менее двух раз и не более десяти раз.

Хотя обнаружение грязных данных имеет проблему неэффективности, оно может выполнить задачу независимо от того, как данные изменены, но есть проблема с двусторонней привязкой в ​​Vue. А обнаружение грязных данных может обнаруживать обновленные значения пакетами, а затем единообразно обновлять пользовательский интерфейс, что значительно сокращает количество операций DOM. Следовательно, неэффективность также относительна, а это означает, что благожелательный видит благожелательный, а мудрый видит мудрость.

захват данных

Vue используется внутриObject.defineProperty()Для достижения двусторонней привязки через эту функцию можно прослушиватьsetа такжеgetмероприятие.

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

Приведенный выше код просто реализует, как отслеживать данныеsetа такжеgetсобытий, но этого недостаточно, вам также необходимо добавить публикацию и подписку на свойства, когда это необходимо

<div>
    {{name}}
</div>

При разборе приведенного выше кода шаблона столкнитесь{{name}}даст атрибутыnameДобавьте публикацию-подписку.

// 通过 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 

Далее, даdefineReactiveфункция преобразования

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}

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

Прокси против Object.defineProperty

Object.definePropertyНесмотря на то, что удалось добиться двусторонней привязки, он все еще имеет недостатки.

  1. Только перехват данных может быть выполнен для свойств, поэтому вам нужно глубоко пройти весь объект
  2. Изменения в данных не могут быть отслежены для массивов

Хотя это правда, что Vue может обнаруживать изменения в данных массива, на самом деле это хак, и он также имеет недостатки.

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 以下几个函数
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 获得原生函数
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 调用原生函数
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 触发更新
    ob.dep.notify()
    return result
  })
})

Напротив, Proxy не имеет вышеуказанных проблем: он изначально поддерживает мониторинг изменений массива и может напрямую перехватывать весь объект, поэтому Vue также будет использовать Proxy для замены Object.defineProperty в следующей основной версии.

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2

виртуальный DOM

Виртуальный DOM включает в себя много контента, вы можете обратиться к моему предыдущемуписьменная статья

аутентификация маршрута

  • Страница входа отделена от других страниц.После входа в систему создается экземпляр Vue и инициализируются необходимые маршруты.
  • Динамическая маршрутизация, реализованная через addRoute

Разница между Vue и React

  • Форма Vue поддерживает двустороннюю привязку, что делает ее более удобной.
  • Различные способы изменения данных, setState имеет яму
  • props Vue изменяемый, React неизменяемый
  • Чтобы определить, нужно ли обновлять React, об этом можно судить по функции ловушки, Vue использует отслеживание зависимостей для отображения того, что изменено.
  • После React 16 некоторые функции ловушек будут выполняться несколько раз.
  • React должен использовать JSX и должен быть скомпилирован с помощью Babel. Хотя Vue может использовать шаблоны, он также может работать без компиляции, просто написав функцию рендеринга.
  • Экологический React относительно хорош

Интернет

Трехстороннее рукопожатие TCP

В протоколе TCP сторона, которая активно инициирует запрос, является клиентом, а сторона, которая пассивно подключена, называется сервером. Будь то клиент или сервер, после установления TCP-соединения он может отправлять и получать данные, поэтому TCP также является полнодуплексным протоколом.

Первоначально оба конца находятся в ЗАКРЫТОМ состоянии. Перед началом связи обе стороны создают TCB. После того, как сервер создал TCB, он переходит в состояние LISTEN и начинает ждать, пока клиент отправит данные.

первое рукопожатие

Клиент отправляет сегмент запроса на соединение на сервер. Этот сегмент содержит свой собственный начальный порядковый номер для передачи данных. После отправки запроса клиент переходит в состояние SYN-SENT,xУказывает начальный порядковый номер передачи данных клиента.

второе рукопожатие

После того, как сервер получит сегмент запроса на соединение, если он согласится на соединение, он отправит ответ, и ответ также будет содержать свой собственный начальный порядковый номер передачи данных.После завершения передачи он войдет в состояние SYN-RECEIVED.

третье рукопожатие

Когда клиент получает ответ о согласии на подключение, он также отправляет подтверждающее сообщение на сервер. После того, как клиент отправляет этот сегмент, он переходит в состояние ESTABLISHED, в состояние ESTABLISHED входит и сервер после получения ответа, в это время соединение успешно установлено.

PS: третье рукопожатие может содержать данные с помощью технологии TCP Fast Open (TFO). На самом деле, пока задействован протокол рукопожатия, можно использовать метод, аналогичный TFO: клиент и сервер хранят один и тот же файл cookie, и этот файл cookie выдается во время следующего рукопожатия для уменьшения RTT.

У вас есть сомнения, что соединение может быть установлено после двух рукопожатий, зачем вам третий ответ?

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

Можно представить следующий сценарий. Клиент отправляет запрос на соединение A, но тайм-аут вызван сетевыми причинами.В это время TCP запустит механизм повторной передачи тайм-аута, чтобы снова отправить запрос на соединение B. В этот момент запрос успешно достигает сервера, и сервер отвечает и устанавливает запрос. Если запрос на соединение A, наконец, поступает на сервер после того, как два конца закрыты, то сервер будет думать, что клиенту необходимо снова установить TCP-соединение, тем самым отвечая на запрос и переходя в состояние ESTABLISHED. В это время клиент фактически находится в состоянии ЗАКРЫТ, что заставит сервер все время ждать, что приведет к пустой трате ресурсов.

PS: при установлении соединения, если какой-либо конец отключен, TCP повторно передаст пакет SYN, как правило, повторяет попытку пять раз и может столкнуться с атакой SYN FLOOD во время установления соединения. В этом случае вы можете уменьшить количество повторных попыток или просто отклонить запрос, если он не может быть обработан.

Контроль перегрузки TCP

Обработка перегрузки отличается от управления потоком, которое воздействует на получателя, чтобы гарантировать, что у получателя есть время для приема данных. Первый действует в сети, предотвращая перегрузку сети слишком большим объемом данных и избегая перегрузки сети.

Обработка перегрузки включает четыре алгоритма: медленный старт, предотвращение перегрузки, быстрая повторная передача и быстрое восстановление.

алгоритм медленного старта

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

Шаги алгоритма медленного старта следующие:

  1. Соединение изначально устанавливает окно перегрузки (Congestion Window) на 1 MSS (максимальный объем данных для сегмента)
  2. Умножьте размер окна на два после каждого RTT.
  3. Экспоненциальный рост, конечно, не безграничен, поэтому есть пороговое ограничение, и когда размер окна больше порога, активируется алгоритм предотвращения перегрузки.

Алгоритм предотвращения перегрузок

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

В процессе передачи таймер может истечь, в это время TCP подумает, что сеть перегружена и немедленно выполнит следующие действия:

  • Установите порог на половину текущего окна перегрузки
  • Установите окно перегрузки на 1 MSS
  • Запустите алгоритм предотвращения перегрузок

быстрая ретрансляция

Быстрая повторная передача обычно происходит вместе с быстрым восстановлением. Как только пакеты, полученные получателем, выходят из строя, получатель ответит только на последний порядковый номер пакета в правильном порядке (при отсутствии Sack).如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

TCP Taho реализован следующим образом

  • Установите порог на половину текущего окна перегрузки
  • Установите окно перегрузки на 1 MSS
  • Перезапустите алгоритм медленного старта

TCP Рено реализован следующим образом

  • Окно перегрузки уменьшено вдвое
  • установить порог для текущего окна перегрузки
  • Войдите в фазу быстрого восстановления (повторно передайте пакеты, необходимые узлу, и выйдите из этой фазы после получения нового ответа ACK)
  • Используйте алгоритмы предотвращения перегрузок

TCP New Ren улучшил быстрое восстановление

TCP New RenoАлгоритм улучшен доTCP RenoОшибки алгоритма. Ранее, если в быстром восстановлении был получен новый пакет ACK, быстрое восстановление будет завершено.

существуетTCP New Reno, отправитель TCP сначала отмечает максимальный порядковый номер сегмента с тремя дубликатами ACK.

Если у меня есть пакет, данные сегмента которого представляют собой десять порядковых номеров от 1 до 10, а пакеты с порядковыми номерами 3 и 7 потеряны, то максимальный порядковый номер сегмента равен 10. Отправитель получит ответ только с порядковым номером ACK, равным 3. В это время сообщение с порядковым номером 3 отправляется повторно, и получатель успешно принимает его и отправляет ответ с порядковым номером ACK 7. В это время TCP знает, что на противоположном конце есть несколько пакетов, которые не были получены, и будет продолжать отправлять сообщение с порядковым номером 7. Получатель успешно получает его и отправляет ответ с порядковым номером ACK 11. В это время, отправитель думает, что получатель сегмента имеет. После успешного приема фаза быстрого восстановления будет завершена.

рукопожатие HTTPS

HTTPS по-прежнему передает информацию через HTTP, но информация шифруется через протокол TLS.

TLS

Протокол TLS находится выше транспортного уровня и ниже прикладного уровня. Для первой передачи протокола TLS требуется два RTT, которые затем можно сократить до одного RTT посредством возобновления сеанса.

В TLS используются два метода шифрования: симметричное шифрование и асимметричное шифрование.

Симметричное шифрование:

Симметричное шифрование означает, что обе стороны имеют один и тот же секретный ключ, и обе стороны знают, как шифровать и расшифровывать зашифрованный текст.

Асимметричное шифрование:

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

Процесс рукопожатия TLS выглядит следующим образом:

  1. Клиент отправляет случайное значение, требуемый протокол и шифрование
  2. Сервер получает случайное значение клиента, сам генерирует случайное значение и использует соответствующий метод в соответствии с протоколом и методом шифрования, требуемым клиентом для отправки собственного сертификата (если вам нужно проверить сертификат клиента, вы надо объяснить)
  3. Клиент получает сертификат от сервера и проверяет, действителен ли он.После прохождения проверки будет сгенерировано случайное значение, и случайное значение будет зашифровано открытым ключом сертификата сервера и отправлено на сервер.Если серверу необходимо проверить сертификат клиента, к нему будет прикреплен сертификат
  4. Сервер получает зашифрованное случайное значение и расшифровывает его с помощью закрытого ключа, чтобы получить третье случайное значение.В это время на обоих концах есть три случайных значения.Ключ может быть сгенерирован в соответствии с ранее согласованным методом шифрования через эти три случайных значения. Следующее сообщение может быть зашифровано и расшифровано с помощью этого ключа.

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

PS: Все вышеприведенные описания относятся к рукопожатию протокола TLS 1.2.В протоколе 1.3 требуется только один RTT для установления соединения в первый раз, а для последующего восстановления соединения RTT не требуется.

Весь процесс от ввода URL до загрузки страницы

  1. Сначала выполните DNS-запрос, если на этом шаге выполняется интеллектуальное разрешение DNS, IP-адрес с самой высокой скоростью доступа будет предоставлен обратно.
  2. Далее идет рукопожатие TCP, прикладной уровень будет отправлять данные на транспортный уровень, где протокол TCP будет указывать номера портов на обоих концах, а затем отправлять их на сетевой уровень. Протокол IP на сетевом уровне определяет IP-адрес и указывает, как переключаться между маршрутизаторами при передаче данных. Затем пакет будет инкапсулирован в структуру кадра данных канального уровня и, наконец, передача на физическом уровне.
  3. После завершения рукопожатия TCP будет выполнено рукопожатие TLS, после чего начнется официальная передача данных.
  4. Прежде чем данные поступят на сервер, они также могут пройти через сервер, отвечающий за балансировку нагрузки. Его функция заключается в разумном распределении запросов на несколько серверов. В настоящее время предполагается, что сервер будет отвечать на HTML-файл.
  5. Сначала браузер определит, что такое код состояния, если 200, то он продолжит парсинг, если 400 или 500, то сообщит об ошибке, если 300, то будет перенаправлен, будет перенаправление. счетчик здесь, чтобы избежать чрезмерного перенаправления. , если количество раз превышено, будет сообщено об ошибке
  6. Браузер начинает синтаксический анализ файла.Если он в формате gzip, он сначала распаковывает его, а затем знает, как декодировать файл через формат кодирования файла.
  7. После того, как файл будет успешно декодирован, официально начнется процесс рендеринга: сначала будет построено дерево DOM в соответствии с HTML, а при наличии CSS будет построено дерево CSSOM. Если вы столкнетесьscriptЕсли есть метка, он определит, существует ли онаasyncилиdefer, первый будет загружать и выполнять JS параллельно, последний сначала загрузит файл, а затем дождется завершения парсинга HTML и последовательного выполнения, если ничего из вышеперечисленного, он заблокирует процесс рендеринга до тех пор, пока JS не будет выполнен. Если вы столкнетесь с загрузкой файла, вы загрузите файл.Если вы используете здесь протокол HTTP 2.0, это значительно повысит эффективность загрузки нескольких изображений.
  8. Срабатывает, когда исходный HTML полностью загружен и проанализированDOMContentLoadedмероприятие
  9. После того, как дерево CSSOM и дерево DOM будут построены, начнется создание дерева рендеринга.На этом этапе определяется макет, стиль и многие другие аспекты элементов страницы.
  10. В процессе генерации дерева рендеринга браузер начинает вызывать GPU для отрисовки, синтеза слоя и отображения содержимого на экране.

Общие коды возврата HTTP

2ХХ успехов

  • 200 OK, что указывает на корректную обработку запроса от клиента на стороне сервера
  • 204 Нет содержимого, что указывает на то, что запрос выполнен успешно, но ответное сообщение не содержит основной части сущности
  • 205 Reset Content, указывающий на то, что запрос выполнен успешно, но ответное сообщение не содержит основной части сущности, но отличается от ответа 204 тем, что запрашивающему требуется сбросить содержимое
  • 206 Частичный контент, сделайте запрос диапазона

3XX редирект

  • 301 перемещен навсегда, постоянное перенаправление, указывающее, что ресурсу был присвоен новый URL-адрес
  • 302 найдено, временное перенаправление, указывающее на то, что ресурсу временно присвоен новый URL
  • 303 см. другое, что указывает на то, что для ресурса существует другой URL-адрес, и для получения ресурса следует использовать метод GET.
  • 304 не изменено, указывающее на то, что сервер разрешает доступ к ресурсу, но запрос не соответствует условиям из-за возникновения
  • 307 временное перенаправление, временное перенаправление, похожее на 302, но ожидает, что клиент сохранит метод запроса без изменений и сделает запрос на новый адрес

4XX Ошибка клиента

  • 400 неверный запрос, в сообщении запроса есть синтаксическая ошибка
  • 401 неавторизованный, что указывает на то, что для отправленного запроса требуется информация аутентификации через HTTP-аутентификацию.
  • 403 запрещено, что указывает на то, что доступ к запрошенному ресурсу был запрещен сервером
  • 404 not found, указывающий на то, что запрошенный ресурс не найден на сервере

5ХХ ошибка сервера

  • 500 внутренняя ошибка сервера, указывающая на то, что при выполнении запроса произошла ошибка на стороне сервера
  • 501 Not Implemented, указывающий, что сервер не поддерживает функцию, требуемую текущим запросом.
  • Служба 503 недоступна, что указывает на то, что сервер временно перегружен или отключен на техническое обслуживание и не может обрабатывать запросы.

Алгоритмы структуры данных

общий вид

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

function checkArray(array) {
    if (!array || array.length <= 2) return
}
function swap(array, left, right) {
    let rightValue = array[right]
    array[right] = array[left]
    array[left] = rightValue
}

Пузырьковая сортировка

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

Ниже приведен код, реализующий алгоритм

function bubble(array) {
  checkArray(array);
  for (let i = array.length - 1; i > 0; i--) {
    // 从 0 到 `length - 1` 遍历
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) swap(array, j, j + 1)
    }
  }
  return array;
}

Количество операций алгоритма представляет собой арифметическую прогрессиюn + (n - 1) + (n - 2) + 1, после удаления постоянного члена временная сложность составляет O (n * n)

Сортировка вставками

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

Ниже приведен код, реализующий алгоритм

function insertion(array) {
  checkArray(array);
  for (let i = 1; i < array.length; i++) {
    for (let j = i - 1; j >= 0 && array[j] > array[j + 1]; j--)
      swap(array, j, j + 1);
  }
  return array;
}

Количество операций алгоритма представляет собой арифметическую прогрессиюn + (n - 1) + (n - 2) + 1, после удаления постоянного члена временная сложность составляет O (n * n)

сортировка выбором

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

Ниже приведен код, реализующий алгоритм

function selection(array) {
  checkArray(array);
  for (let i = 0; i < array.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      minIndex = array[j] < array[minIndex] ? j : minIndex;
    }
    swap(array, i, minIndex);
  }
  return array;
}

Количество операций алгоритма представляет собой арифметическую прогрессиюn + (n - 1) + (n - 2) + 1, после удаления постоянного члена временная сложность составляет O (n * n)

Сортировка слиянием

Принцип сортировки слиянием заключается в следующем. Рекурсивно разделите массивы на пары, пока они не будут содержать не более двух элементов, затем отсортируйте и объедините массивы, наконец, объединившись в отсортированный массив. Предположим, у меня есть набор массивов[3, 1, 2, 8, 9, 7, 6], средний индекс равен 3, сначала отсортируйте массив[3, 1, 2, 8]. В этом левом массиве продолжайте разбивать до тех пор, пока массив не будет содержать два элемента (если длина массива нечетная, будет разделенный массив, содержащий только один элемент). затем отсортируйте массив[3, 1]а также[2, 8], а затем отсортировать массив[1, 3, 2, 8], так что левый массив отсортирован, затем правый массив отсортирован в соответствии с приведенными выше идеями, и, наконец, отсортирован массив[1, 2, 3, 8]а также[6, 7, 9]Сортировать.

Ниже приведен код, реализующий алгоритм

function sort(array) {
  checkArray(array);
  mergeSort(array, 0, array.length - 1);
  return array;
}

function mergeSort(array, left, right) {
  // 左右索引相同说明已经只有一个数
  if (left === right) return;
  // 等同于 `left + (right - left) / 2`
  // 相比 `(left + right) / 2` 来说更加安全,不会溢出
  // 使用位运算是因为位运算比四则运算快
  let mid = parseInt(left + ((right - left) >> 1));
  mergeSort(array, left, mid);
  mergeSort(array, mid + 1, right);

  let help = [];
  let i = 0;
  let p1 = left;
  let p2 = mid + 1;
  while (p1 <= mid && p2 <= right) {
    help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
  }
  while (p1 <= mid) {
    help[i++] = array[p1++];
  }
  while (p2 <= right) {
    help[i++] = array[p2++];
  }
  for (let i = 0; i < help.length; i++) {
    array[left + i] = help[i];
  }
  return array;
}

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

mergeSort(data, 0, 6) // mid = 3
  mergeSort(data, 0, 3) // mid = 1
    mergeSort(data, 0, 1) // mid = 0
      mergeSort(data, 0, 0) // 遇到终止,回退到上一步
    mergeSort(data, 1, 1) // 遇到终止,回退到上一步
    // 排序 p1 = 0, p2 = mid + 1 = 1
    // 回退到 `mergeSort(data, 0, 3)` 执行下一个递归
  mergeSort(2, 3) // mid = 2
    mergeSort(3, 3) // 遇到终止,回退到上一步
  // 排序 p1 = 2, p2 = mid + 1 = 3
  // 回退到 `mergeSort(data, 0, 3)` 执行合并逻辑
  // 排序 p1 = 0, p2 = mid + 1 = 2
  // 执行完毕回退
  // 左边数组排序完毕,右边也是如上轨迹

Количество операций алгоритма можно рассчитать следующим образом: выполнить рекурсию дважды, каждый раз, когда объем данных составляет половину массива, и, наконец, один раз выполнить итерацию всего массива, поэтому выражение2T(N / 2) + T(N)(T — время, N — объем данных). В соответствии с этим выражением можно применитьформулаВременная сложность получается какO(N * logN)

Быстрый ряд

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

Ниже приведен код, реализующий алгоритм

function sort(array) {
  checkArray(array);
  quickSort(array, 0, array.length - 1);
  return array;
}

function quickSort(array, left, right) {
  if (left < right) {
    swap(array, , right)
    // 随机取值,然后和末尾交换,这样做比固定取一个位置的复杂度略低
    let indexs = part(array, parseInt(Math.random() * (right - left + 1)) + left, right);
    quickSort(array, left, indexs[0]);
    quickSort(array, indexs[1] + 1, right);
  }
}
function part(array, left, right) {
  let less = left - 1;
  let more = right;
  while (left < more) {
    if (array[left] < array[right]) {
      // 当前值比基准值小,`less` 和 `left` 都加一
	   ++less;
       ++left;
    } else if (array[left] > array[right]) {
      // 当前值比基准值大,将当前值和右边的值交换
      // 并且不改变 `left`,因为当前换过来的值还没有判断过大小
      swap(array, --more, left);
    } else {
      // 和基准值相同,只移动下标
      left++;
    }
  }
  // 将基准值和比基准值大的第一个值交换位置
  // 这样数组就变成 `[比基准值小, 基准值, 比基准值大]`
  swap(array, right, more);
  return [less, more];
}

Сложность этого алгоритма такая же, как у сортировки слиянием, но сложность дополнительного пространства меньше, чем у сортировки слиянием, только O(logN), и он занимает меньше постоянного времени, чем сортировка слиянием.

вопросы интервью

Sort Colors: Эта тема взята изLeetCode, предмет требует от нас[2,0,2,1,1,0]сортировать в[0,0,1,1,2,2], в этой задаче можно использовать идею трехфакторной быстрой сортировки.

Ниже приведена реализация кода

var sortColors = function(nums) {
  let left = -1;
  let right = nums.length;
  let i = 0;
  // 下标如果遇到 right,说明已经排序完成
  while (i < right) {
    if (nums[i] == 0) {
      swap(nums, i++, ++left);
    } else if (nums[i] == 1) {
      i++;
    } else {
      swap(nums, i, --right);
    }
  }
};

Kth Largest Element in an Array: Эта тема взята изLeetCode, в задаче нужно найти K-й самый большой элемент в массиве.В этой задаче также можно использовать идею быстрой сортировки. А так как найти K-й наибольший элемент, то в процессе разделения массива можно узнать, какая сторона искомого элемента, и тогда нужно только отсортировать соответствующую сторону массива.

Ниже приведена реализация кода

var findKthLargest = function(nums, k) {
  let l = 0
  let r = nums.length - 1
  // 得出第 K 大元素的索引位置
  k = nums.length - k
  while (l < r) {
    // 分离数组后获得比基准树大的第一个元素索引
    let index = part(nums, l, r)
    // 判断该索引和 k 的大小
    if (index < k) {
      l = index + 1
    } else if (index > k) {
      r = index - 1
    } else {
      break
    }
  }
  return nums[k]
};
function part(array, left, right) {
  let less = left - 1;
  let more = right;
  while (left < more) {
    if (array[left] < array[right]) {
	   ++less;
       ++left;
    } else if (array[left] > array[right]) {
      swap(array, --more, left);
    } else {
      left++;
    }
  }
  swap(array, right, more);
  return more;
}

сортировка кучей

Сортировка кучей использует преимущества характеристик двоичной кучи.Двоичная куча обычно представлена ​​массивом, а двоичная куча представляет собой полное двоичное дерево (все листовые узлы (самый нижний узел) сортируются слева направо, а остальные узлы слоя заполнены). Двоичная куча делится на большую корневую кучу и малую корневую кучу.

  • Большая корневая куча заключается в том, что все дочерние узлы узла имеют значения меньше его
  • Маленькая корневая куча заключается в том, что все дочерние узлы узла имеют значение больше, чем его

Принцип сортировки кучи состоит в том, чтобы сформировать большую корневую кучу или маленькую корневую кучу. Взяв в качестве примера небольшую корневую кучу, индекс левого дочернего узла узла равенi * 2 + 1, право естьi * 2 + 2, родительский узел(i - 1) /2.

  1. Сначала пройдитесь по массиву, чтобы определить, меньше ли родительский узел узла, чем он, если он мал, поменяйте местами и продолжайте судить, пока его родительский узел больше его.
  2. Повторяйте описанную выше операцию 1 до тех пор, пока первая позиция массива не станет максимальным значением.
  3. Затем поменять местами первую позицию и конец и уменьшить длину массива на единицу, указывая, что конец массива уже является максимальным значением, и сравнивать размер не нужно.
  4. Сравните левый и правый узлы, которые больше, затем запомните индекс большего узла и сравните размер с родительским узлом.Если дочерний узел больше, поменяйте местами
  5. Повторяйте описанные выше операции 3–4, пока весь массив не станет большой корневой кучей.

Ниже приведен код, реализующий алгоритм

function heap(array) {
  checkArray(array);
  // 将最大值交换到首位
  for (let i = 0; i < array.length; i++) {
    heapInsert(array, i);
  }
  let size = array.length;
  // 交换首位和末尾
  swap(array, 0, --size);
  while (size > 0) {
    heapify(array, 0, size);
    swap(array, 0, --size);
  }
  return array;
}

function heapInsert(array, index) {
  // 如果当前节点比父节点大,就交换
  while (array[index] > array[parseInt((index - 1) / 2)]) {
    swap(array, index, parseInt((index - 1) / 2));
    // 将索引变成父节点
    index = parseInt((index - 1) / 2);
  }
}
function heapify(array, index, size) {
  let left = index * 2 + 1;
  while (left < size) {
    // 判断左右节点大小
    let largest =
      left + 1 < size && array[left] < array[left + 1] ? left + 1 : left;
    // 判断子节点和父节点大小
    largest = array[index] < array[largest] ? largest : index;
    if (largest === index) break;
    swap(array, index, largest);
    index = largest;
    left = index * 2 + 1;
  }
}

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

Сложность этого алгоритма составляет O(logN)

Система поставляется с реализацией сортировки

Внутренняя реализация сортировки отличается для каждого языка.

Для JS, если длина массива больше 10, будет использоваться быстрая сортировка, в противном случае — сортировка вставками.реализация исходного кода. Сортировка вставками выбрана потому, что хотя временная сложность очень мала, в случае небольшого объема данных иO(N * logN)Это почти то же самое, однако постоянное время, необходимое для сортировки вставками, невелико, поэтому она быстрее, чем другие сортировки.

Для Java также учитывается тип внутреннего элемента. Для массивов, в которых хранятся объекты, используется устойчивый алгоритм. Стабильность означает, что относительный порядок не может быть изменен для одного и того же значения.

бинарное дерево поиска

разное

Внедрение шаблонов проектирования

Заводская выкройка

Есть несколько фабричных паттернов, которые здесь не будут объясняться Ниже приведен пример простого фабричного паттерна.

class Man {
  constructor(name) {
    this.name = name
  }
  alertName() {
    alert(this.name)
  }
}

class Factory {
  static create(name) {
    return new Man(name)
  }
}

Factory.create('yck').alertName()

Конечно, заводская выкройка предназначена не только для новых.Пример.

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

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

В исходном коде Vue вы также можете увидеть использование фабричного шаблона, например, создание асинхронных компонентов.

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
    
    // 逻辑处理...
  
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

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

одноэлементный шаблон

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

Ядром одноэлементного шаблона является обеспечение доступа только к одному объекту в мире. Поскольку JS - это бесклассовый язык, то, как другие языки реализуют синглтоны, не могут быть интегрированы в JS. Нам нужно только использовать переменную, чтобы гарантировать, что экземпляр создается только один раз. Ниже приведен пример того, как реализовать шаблон синглтона. .

class Singleton {
  constructor() {}
}

Singleton.getInstance = (function() {
  let instance
  return function() {
    if (!instance) {
      instance = new Singleton()
    }
    return instance
  }
})()

let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true

В исходном коде Vuex также можно увидеть использование шаблона singleton, хотя его реализация не такая, через внешнюю переменную для управления установкой Vuex только один раз

let Vue // bind on install

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    // ...
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

режим адаптера

Адаптер используется для устранения несовместимости двух интерфейсов, не требует изменения существующего интерфейса и реализует нормальное взаимодействие двух интерфейсов, обертывая один слой.

Вот пример того, как реализовать шаблон адаптера

class Plug {
  getName() {
    return '港版插头'
  }
}

class Target {
  constructor() {
    this.plug = new Plug()
  }
  getName() {
    return this.plug.getName() + ' 适配器转二脚插头'
  }
}

let target = new Target()
target.getName() // 港版插头 适配器转二脚插头

Во Vue мы на самом деле часто используем шаблон адаптера. Например, родительский компонент передает атрибут временной метки дочернему компоненту, и временная метка должна быть преобразована в обычное отображение даты внутри компонента, которое обычно используется.computedДля преобразования этот процесс использует шаблон адаптера.

орнамент

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

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

function readonly(target, key, descriptor) {
  descriptor.writable = false
  return descriptor
}

class Test {
  @readonly
  name = 'yck'
}

let t = new Test()

t.yck = '111' // 不可修改

В React шаблон декоратора буквально везде.

import { connect } from 'react-redux'
class MyComponent extends React.Component {
    // ...
}
export default connect(mapStateToProps)(MyComponent)

прокси-режим

Прокси должен контролировать доступ к объекту и предотвращать прямой доступ к объекту извне. В реальной жизни также существует множество сценариев использования прокси. Например, если вам нужно купить иностранный продукт, вы можете приобрести продукт через покупку.

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

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>

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

модель публикации-подписки

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

В реальном коде на самом деле он на самом деле опубликован - режим подписки также очень распространен. Например, мы нажимаем на кнопку для запуска события щелчка для использования этого режима.

<ul id="ul"></ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>

В Vue, как реализовать отзывчивость, также используется этот шаблон. Для объектов, которым необходимо реализовать отзывчивость, вgetСбор зависимостей выполняется при изменении объекта, а диспетчерские обновления запускаются при изменении свойств объекта.

Если у вас остались вопросы о том, как реализовать отзывчивость, вы можете прочитать мою предыдущую статьюУглубленный анализ принципа отзывчивости Vue

наконец

Если вы думаете, что есть другие хорошие темы для внесения вклад, вы также можете спросить их в комментариях

Вам может быть интересно, как я написал текст размером 25 тыс. На самом деле, на многие вопросы интервью можно найти ответы в моем проекте Wanxing.адрес проекта

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

Узнайте больше об акции по поиску работы Nuggets Autumn Recruitment 👉При приеме на работу осенью есть подарки за написание статей | Nuggets Technology Essay