написать впереди
Эта статья「源码级回答」大厂高频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Скелетонный экран -
pwaserviceworker
Оптимизация производительности загрузки
- Сторонние модули импортируются по запросу (
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компрессия