«Ответ исходного уровня» Дачанские высокочастотные вопросы интервью Vue (в центре)

опрос
«Ответ исходного уровня» Дачанские высокочастотные вопросы интервью Vue (в центре)

написать впереди

Эта статья「源码级回答」大厂高频Vue面试题Вторая часть серии, эта статья также выбирает некоторые классические вопросы интервью, которые часто задают в интервью, и анализирует их с точки зрения исходного кода.

Хочу начать читать с первой статьи, адрес находится по адресуздесь

Нечего сказать, дело сделано!

Кратко опишите принцип работы алгоритма diff в Vue.

Введение в дифф

diffАлгоритм является эффективным алгоритмом, который сравнивает узлы дерева одного и того же слоя, избегая послойного поиска и обхода дерева, поэтому временная сложность составляет всегоO(n).diffАлгоритмы используются во многих сценариях, например, вVueвиртуальныйdomсделать как реальныйdomСтарый и новыйVNodeЭтот алгоритм используется при сравнении и обновлении узлов.diffАлгоритм имеет две примечательные особенности:

  • Сравнения будут проводиться только на одном уровне, а не между уровнями.
  • Во время сравнения diff петля рисуется с обеих сторон к середине.

updateChildren

мы знаем этоmodelПри выполнении операции соответствующийDepсерединаWatcherобъект.WatcherОбъект вызовет соответствующийupdateчтобы изменить вид. в конечном итоге будет вновь созданVNodeузел и старыйVNodeпровестиpatchпроцесс, сравнение「差异」И, наконец, обновите эти «различия» на вид.

а такжеdiffАлгоритм сноваpatchОсновное содержание , мы используемdiffАлгоритм может сравнивать «разницу» двух деревьев Предположим, что теперь у нас есть следующие два дерева, новое и старое соответственно.VNodeНод, пораpatchпроцесс, нам нужно сравнить их:

diffАлгоритм представляет собой узел дерева путем сравнения одного и того же слоя дерева послойно, а не в режиме поиска, поэтому только временная сложностьO(n), является довольно эффективным алгоритмом, как показано на рисунке ниже.

Узлы в квадратах одного цвета на рисунке будут сравниваться, и после того, как будут получены «различия», эти «различия» будут обновлены в представлении. Поскольку выполняется только один и тот же уровень сравнения, это очень эффективно.

patchПроцесс более сложный, в основном речь идет о «oldChа такжеchКогда оба существуют и не совпадают, используйтеupdateChildrenфункция для обновления дочерних узлов".

посмотриupdateChildrenфункция

Для простоты понимания я добавил комментарии к соответствующему коду

function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  let oldStartIdx = 0; // oldVnode开始下标
  let newStartIdx = 0; // newVnode开始下标
  let oldEndIdx = oldCh.length - 1; // oldVnode结束下标
  let newEndIdx = newCh.length - 1; // newVnode结束下标
  let oldStartVnode = oldCh[0]; // oldVnode开始节点
  let newStartVnode = newCh[0]; // newVnode开始节点
  let oldEndVnode = oldCh[oldEndIdx]; // oldVnode结束节点
  let newEndVnode = newCh[newEndIdx]; // newVnode结束节点

  let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

  // ...
}

Сначала определитеoldStartIdx,newStartIdx,oldEndIdxтак же какnewEndIdxНовый и старый соответственноVNodeначальные/конечные индексыoldStartVnode,newStartVnode,oldEndVnodeтак же какnewEndVnodeУкажите на соответствующие индексы этихVNodeузел.Далее идетwhileцикл, в процессе,oldStartIdx,newStartIdx,oldEndIdxтак же какnewEndIdxбудет постепенно двигаться к середине.

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...
}

сначала, когдаoldStartVnodeилиoldEndVnodeкогда его не существует,oldStartIdxа такжеoldEndIdxПродолжайте двигаться ближе к середине и обновляйте соответствующиеoldStartVnodeа такжеoldEndVnodeуказывает на.

if (isUndef(oldStartVnode)) {
  oldStartVnode = oldCh[++oldStartIdx];
} else if (isUndef(oldEndVnode)) {
  oldEndVnode = oldCh[--oldEndIdx];
}

Следующая часть должнаoldStartIdx,newStartIdx,oldEndIdxтак же какnewEndIdxВ процессе попарного сравнения всего будет 2*2=4 ситуации.

прежде всегоoldStartVnodeа такжеnewStartVnodeсоответствоватьsameVnodeвремя с указанием старогоVNodeЗаголовок узла с новымVNodeЗаголовки узлов одинаковыеVNodeузел, напрямуюpatchVnode,в то же времяoldStartIdxа такжеnewStartIdxПереместиться назад на один бит.

if (sameVnode(oldStartVnode, newStartVnode)) {
  // 首先是 oldStartVnode 与 newStartVnode 符合 sameVnode 时,
  // 说明老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode,同时 oldStartIdx 与 newStartIdx 向后移动一位
  patchVnode(
    oldStartVnode,
    newStartVnode,
    insertedVnodeQueue,
    newCh,
    newStartIdx
  );
  oldStartVnode = oldCh[++oldStartIdx];
  newStartVnode = newCh[++newStartIdx];
}

С последующимoldEndVnodeа такжеnewEndVnodeсоответствоватьsameVnode, то есть дваVNodeконцы такие жеVNode, делать то же самоеpatchVnodeработать иoldEndVnodeа такжеnewEndVnodeСдвинуться вперед на одно место.

if (sameVnode(oldEndVnode, newEndVnode)) {
  // 其次是 oldEndVnode 与 newEndVnode 符合 sameVnode,
  // 也就是两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode 操作并将 oldEndVnode 与 newEndVnode 向前移动一位。
  patchVnode(
    oldEndVnode,
    newEndVnode,
    insertedVnodeQueue,
    newCh,
    newEndIdx
  );
  oldEndVnode = oldCh[--oldEndIdx];
  newEndVnode = newCh[--newEndIdx];
}

ДалееoldStartVnodeа такжеnewEndVnodeсоответствоватьsameVnodeкогда старыйVNodeЗаголовок узла с новымVNodeКогда хвост узла является одним и тем же узлом,oldStartVnode.elmЭтот узел перемещается непосредственно вoldEndVnode.elmза этим узлом. потомoldStartIdxсдвинуться на один бит назад,newEndIdxСдвинуться вперед на одно место.

if (sameVnode(oldStartVnode, newEndVnode)) {
  // oldStartVnode 与 newEndVnode 符合 sameVnode 的时候,
  // 也就是老 VNode 节点的头部与新 VNode 节点的尾部是同一节点的时候,
  // 将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。
  patchVnode(
    oldStartVnode,
    newEndVnode,
    insertedVnodeQueue,
    newCh,
    newEndIdx
  );
  canMove &&
    nodeOps.insertBefore(
      parentElm,
      oldStartVnode.elm,
      nodeOps.nextSibling(oldEndVnode.elm)
    );
  oldStartVnode = oldCh[++oldStartIdx];
  newEndVnode = newCh[--newEndIdx];
}

Ну наконец тоoldEndVnodeа такжеnewStartVnodeсоответствоватьsameVnodeвремя, то есть старыйVNodeхвост узла с новымVNodeКогда глава узла является тем же узлом,oldEndVnode.elmвставить вoldStartVnode.elmПередний. такой же,oldEndIdxдвигаться вперед на одно место,newStartIdxПереместиться назад на один бит.

if (sameVnode(oldEndVnode, newStartVnode)) {
  // oldEndVnode 与 newStartVnode 符合 sameVnode 时,
  // 也就是老 VNode 节点的尾部与新 VNode 节点的头部是同一节点的时候,
  // 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。同样的,oldEndIdx 向前移动一位,newStartIdx 向后移动一位。
  patchVnode(
    oldEndVnode,
    newStartVnode,
    insertedVnodeQueue,
    newCh,
    newStartIdx
  );
  canMove &&
    nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
  oldEndVnode = oldCh[--oldEndIdx];
  newStartVnode = newCh[++newStartIdx];
}

如果都不满足以上四种情形,那说明没有相同的节点可以复用。Поэтому путем поиска заранее установленныхVNodeдляkeyзначение, соответствующееindexдляvalueХэш-таблица значений.

Найдите из этой хеш-таблицы с помощьюnewStartVnodeпоследовательныйkeyСтарыйVNodeУзел, если они встречаютсяsameVnodeусловиях, вpatchVnodeв то же время будет эта правдаdomперейти кoldStartVnodeсоответствующая реальностьdomперед индексом; если не найден, новый индекс под текущим индексомVNodeузел в старомVNodeЕсли очереди не существует, и узел нельзя использовать повторно, его можно только вызватьcreateElmсоздать новыйdomузел к текущемуnewStartIdxпозиция.

И, наконец, кусок кода:

// while 循环结束
if (oldStartIdx > oldEndIdx) {
  // 如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的,需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
  addVnodes(
    parentElm,
    refElm,
    newCh,
    newStartIdx,
    newEndIdx,
    insertedVnodeQueue
  );
} else if (newStartIdx > newEndIdx) {
  // 如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多,将这些无用的老节点通过 removeVnodes 批量删除即可。
  removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}

когдаwhileПосле завершения цикла, еслиoldStartIdx > oldEndIdx, что указывает на то, что старые узлы сравнивались, но новых узлов еще много, и новые узлы нужно вставить в реальныйDOMИди, звониaddVnodesПросто вставьте эти узлы.

если удовлетворенnewStartIdx > newEndIdxУсловие, указывающее, что новый узел сравнивается, старых узлов еще много, пропустите эти бесполезные старые узлы черезremoveVnodesМожно удалять массово.

Почему данные в компонентах Vue — это функция?

На самом деле есть вторая половина вопроса:new VueВ данном случаеdataМожет ли он быть непосредственно объектом?

Давайте сначала посмотрим на обычные компоненты иnew Vueиспользовать, когдаdataСценарий:

// 组件
data() {
  return {
   msg: "hello 森林",
  }
}

// new Vue
new Vue({
  data: {
    msg: 'hello jack-cool'
  },
  el: '#app',
  router,
  template: '<App/>',
  components: {
    App
  }
})

мы знаем,VueКомпонент на самом деле являетсяVueпример.

JSЭкземпляр находится через构造函数создавать, каждый конструктор можетnewЕсли экземпляров много, то каждый экземпляр наследует метод или свойство прототипа.

VueизdataДанные на самом делеVueСвойства прототипа, данные существуют в памяти

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

Из-за использования объектов каждый экземпляр (компонент) используетdataДанные взаимодействуют друг с другом, что, конечно, не то, что нам нужно. Объект — это ссылка на адрес памяти.Если объект определен напрямую, объект будет использоваться между компонентами, что приведет к взаимодействию данных между компонентами.

Давайте посмотрим на пример:

// 创建一个简单的构建函数
var MyComponent = function() {
    // ...
}
// 原型链对象上设置data数据,data设为Object
MyComponent.prototype.data = {
  name: '森林',
  age: 20,
}
// 创建两个实例:春娇,志明
var chunjiao = new MyComponent()
var zhiming = new MyComponent()
// 默认状态下春娇和志明的年龄一样
console.log(chunjiao.data.age === zhiming.data.age) // true
// 改变春娇的年龄
chunjiao.data.age = 25;
// 打印志明的年龄,发现因为改变了春娇的年龄,结果造成志明的年龄也变了
console.log(chunjiao.data.age)// 25
console.log(zhiming.data.age) // 25

После использования функции с помощьюdata()функция,data()в функцииthisУказав на сам текущий экземпляр, они не будут влиять друг на друга.

Подводя итог, это:

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

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

Расскажите о своем понимании жизненного цикла Vue?

Чтобы ответить на этот вопрос, мы должны сначала дать общий ответVue生命周期что:

VueЭкземпляр имеет полный жизненный цикл, то есть от начала до создания, инициализации данных, компиляции шаблонов, монтированияDom-> Рендеринг, обновление -> Рендеринг, выгрузка и ряд процессов, которые мы называемVueжизненный цикл.

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

Жизненный цикл описывать
beforeCreate После инициализации экземпляра наблюдения за данными (data observer) звонили раньше.
created Вызывается после создания экземпляра. На этом шаге экземпляр выполнил следующую конфигурацию: наблюдение за данными (data observer), операции над свойствами и методами,watch/eventОбратный вызов события. но правдаdomеще не создан,$elпока недоступно
beforeMount Вызывается до начала монтирования, соответствующийrenderФункция вызывается впервые.
mounted elВновь созданныйvm.$elЗамените и вызовите хук после установки на экземпляр.
beforeUpdate Вызывается при обновлении данных, происходит в виртуальномDOMПеред повторным рендерингом и патчем.
updated Виртуальный из-за изменения данныхDOMПеререндерить и пропатчить, после чего будет вызываться этот хук.
activited keep-aliveЭксклюзивный, вызывается при активации компонента
deactivated keep-aliveExclusive, компонент вызывается при его уничтожении
beforeDestory Вызов до уничтожения экземпляра. На этом этапе экземпляр все еще доступен.
destoryed VueВызывается после уничтожения экземпляра.

Вот блок-схема жизненного цикла официального сайта:

Здесь я использую картинку, чтобы разобраться во всем процессе цикла в исходном коде (предупреждение о длинной картинке):

  • VueПо существу конструктор, определенный вsrc/core/instance/index.jsсередина:
// src/core/instance/index.js
function Vue(options) {
  if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
    warn("Vue is a constructor and should be called with the `new` keyword");
  }
  this._init(options);
}
  • Ядром конструктора является вызов_initметод,_initопределено вsrc/core/instance/init.jsсередина:
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
  const vm: Component = this;
  // a uid
  vm._uid = uid++;
  [1];
  let startTag, endTag;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== "production" && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`;
    endTag = `vue-perf-end:${vm._uid}`;
    mark(startTag);
  }

  // a flag to avoid this being observed
  vm._isVue = true;
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
  }
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== "production") {
    initProxy(vm);
  } else {
    vm._renderProxy = vm;
  }
  // expose real self
  vm._self = vm;
  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  callHook(vm, "beforeCreate");
  initInjections(vm); // resolve injections before data/props
  initState(vm);
  initProvide(vm); // resolve provide after data/props
  callHook(vm, "created")[2];
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== "production" && config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    mark(endTag);
    measure(`vue ${vm._name} init`, startTag, endTag);
  }

  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

_initМногие функции инициализации вызываются внутри, и по именам функций видно, что они выполняют жизненный цикл инициализации (initLifecycle), инициализировать центр событий (initEvents), инициализировать рендеринг (initRender),воплощать в жизньbeforeCreateкрюк(callHook(vm, 'beforeCreate')), разбор инжекта(initInjections), состояние инициализации (initState), парсинг предоставить (initProvide),воплощать в жизньcreatedкрюк(callHook(vm, 'created')).

  • существует_initВ конце функции существует суждение, если естьelпросто выполнить$mountметод. определено вsrc/platforms/web/entry-runtime-with-compiler.jsсередина:
// src/platforms/web/entry-runtime-with-compiler.js

// ...

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el);

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== "production" &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      );
    return this;
  }

  const options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template;
    if (template) {
      if (typeof template === "string") {
        // ...
      } else if (template.nodeType) {
        template = template.innerHTML;
      } else {
        // ...
        return this;
      }
    } else if (el) {
      template = getOuterHTML(el);
    }
    if (template) {
       // ...
    }
  }
  return mount.call(this, el, hydrating);
};
// ...

export default Vue;

Здесь в основном делается две вещи:

1. переписанVueна прототипе функции$mountфункция

2, определяется, является ли шаблон, и шаблон преобразуется вrenderфункция

наконец позвонилruntimeизmountметод, используемый для монтажа компонентов, т.е.mountComponentметод.

  • mountComponentвнутри первого звонкаbeforeMountметод, который затем выполняется после первоначального рендеринга и обновленияvm._update(vm._render(), hydrating)метод. Называется после окончательного визуализацииmountedкрюк.
  • beforeUpdateа такжеupdatedХук вызывается после изменения страницы и запуска обновления, соответствующегоsrc/core/observer/scheduler.jsизflushSchedulerQueueв функции.
  • beforeDestroyа такжеdestroyedисполняют$destroyвызывается функция.$destroyфункция определена вVue.prototypeМетод выше, соответствующийsrc/core/instance/lifecycle.jsВ файле:
// src/core/instance/lifecycle.js

Vue.prototype.$destroy = function() {
  const vm: Component = this;
  if (vm._isBeingDestroyed) {
    return;
  }
  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;
  }
};

Общие методы оптимизации производительности в Vue

Оптимизация кодирования

  • Старайтесь не вводить все данныеdataсередина,dataДанные будут увеличиватьсяgetterа такжеsetter, соберет соответствующиеwatcher
  • vueсуществуетv-forПри привязке событий к каждому элементу попробуйте использовать делегаты событий
  • Разделение компонентов (улучшение возможности повторного использования, повышение удобства обслуживания кода, сокращение ненужного рендеринга)
  • v-ifКогда значениеfalseКогда внутренняя команда не будет выполняться, она имеет функцию блокировки и используется во многих случаях.v-ifзаменятьv-show
  • Разумное использование ленивой загрузки маршрутизации, асинхронных компонентов
  • Object.freezeЗаморозить данные

Пользовательский опыт

  • app-skeletonСкелетонный экран
  • pwa serviceworker

Оптимизация производительности загрузки

  • Сторонние модули импортируются по запросу (babel-plugin-component )
  • Прокрутите до видимой области для динамической загрузки (https://tangbc.github.io/vue-virtual-scroll-list )
  • Ленивая загрузка изображения (https://github.com/hilongjw/vue-lazyload.git)

SEO-оптимизация

  • плагин для предварительного рендерингаprerender-spa-plugin
  • рендеринг на стороне сервераssr

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

  • использоватьcdnспособ загрузки сторонних модулей
  • Многопоточная упаковкаhappypack,parallel-webpack
  • Управлять размером файла пакета (tree shaking / splitChunksPlugin)
  • использоватьDllPluginУлучшить скорость упаковки

кэш/сжатие

  • Кэш клиента/кэш сервера
  • Серверgzipкомпрессия