写在前面
本篇是「源码级答复」大厂高频Vue面试题
系列的第二篇,本篇也是选择了面试中经常会问到的一些经典面试题,从源码视点去分析。
想从第一篇开端看的,地址在这儿
话不多说,干就完了!
简述 Vue 中 diff 算法原理
diff 简介
diff
算法是一种经过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时刻复杂度只有 O(n)
。diff
算法的在许多场景下都有运用,例, 2 3 5 H * u 0 R如在 Vue
虚拟 dom
烘托成实在 dom
的新旧 VNode
节点比较更新时,就用到了该算法。diff
算法有两个比较显著的6 % 6 n C D特色:
-
比较只会3 x ; r在同层级进行, 不会跨层级比较。 -
在 diff 比较的过程中,循环从两头向中心收拢。
upG $ # : bdateChildren
咱们知道,在对 model
进行操作时,会触发对应 Dep
中的 Watcher
目标。Watcher
目标会调用对应的 update
来修改视图。最终是将新产生/ | m E *的 VNode
节P ( 5 |点与老 VNode
进行一个 patch
的过程,比对得出「差异」
,最终将这些0 C E I「差异」更新到视图上。
而 diff
算法又是patch
的中心内容,咱们用 diff
算法能够比对出两颗树的「差异」,假设咱们现在有如下两颗树,它们分别是新老 VNode
节点,这时分到了 patch
的过程,咱们需求将他们进行比对:
diff
算法是经过同层的树节点进行比较而非对树进行逐层搜索遍历的方法,所以时刻复杂度只有 O(n O k a I Sn)
,是一种适当高效的4 N z G y N _ B算法,如下图。
❝
图中的相同色彩的方块中的节点会进行比对,比对得到「差异」后将这些「差异」更新到视图上。因为只进行同层级的比对,所以非常高效。
❞a ` B 0 Q X _ `
patch
的过 m N 3 G ?程比较复杂,咱们这儿主要说一下「oldCh
与 ch
都存在且不相一起,运用 updateChildren
函数来更新子节点」这种状况。
来看下updateChildren
函数
❝
为了便利了解,我在对应代码中添加了注释
❞
funcu # 0 Htion updateChildreO { o on(
parentElm,
oldCh,
newChJ g ~ } X o y,
insertedVnodeQueue,
removek S w : QOnly
) {
let oldStartIdx = 0; // oldVnode开端下标
let newStartIdx = 0; // newVnode开端下标
let oldEndIdx = oldCh.length - 1; // oldVnode完毕下标
let newEndIdx = newCh.length - 1s C { T ~; // nee U | } Q ; h HwVnode完毕下标
let o] : L k XldStartVnode = oldCh[0]; // oldVno# @ 0 { 4 $de开端节点
let newStartVnode = newCh[0]; // newVnode开端节点
let oldEndVnode = oldChA . Q - = z 3 G h[oldEndIdx]; // o; E } C }ldVnode完5 r _ ] z I ) e毕节点
let newEndVnode = newCh[newEndIdx]; // newVnode完毕节点
let oldKeyToIdx, idxInOld, vnv D 2 t 9odeToMove, refElm;
// ...
}
首先界说了 oldStartIdx
、newStartIdx
、oldEndIdx
以及 newEndIdx
分别是新老两个 VNode
的开端/完毕的下标,一起 oldStartVnode
、new, $ = a [ W s 5StartVnode
、oldEndVnode
以及 newEndVnode
分别指向这几个u } _ , :索引对应的 VNode
节点。
接下来是7 # r m j &一个 while
循环,在这过程中,oldStartIdx
、newStaA x * 3rtIdx
、oldEndIdx
以及 newEndIdx
会逐步向中心挨近[ , @ @ X x Y m 5。
while (oldStartIdx &A d ] 7 b D }lt;= oldEndIdx && newStartIdx3 7 _ T e r B <= newEndIdx)Q ~ W H L b {
// ...
}
首先当 oldStartVnode
或者 oldEndVnod6 0 %e
不存在的时分,oldStartIdx
与 oldEndIdx
继续向中心挨– ) 4 _近,并更新对应的 oldStartVnode
与 oldEndVnode
的指向。
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (isUndef(oldEn7 u adVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
}
接下来这一块,是将 oldStartIdx
、newStaU N ^rtIdx
、ol, r z v u } | ddEndIdx
以及 newEndIdx
两两比对的过程,一共会呈( l 3 e b现 2*2=4 种状况。
首先是 oldStartVnode
与 newStartVnode
契合 sameVnode
时,阐明老 VNode
节点的头部与新 VNodg m l } # ` W z xe
节点的头部N 4 – F是相同的 VNode
节点,直接进行 patchVnode
,一起 oldStartIdx
与 newStartIdx
向后移动一位。
if (sameVnode(oldStartVnode, newSh X * . WtaB y y E G YrtVnode)) {
// 首先是 oldStaO C 6 : 7 %rtVnode 与 newStartVnode 契合 sameVnode 时,
// 阐明老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patch1 Z ,Vno1 } ; , jde,一起 oldStartIdx 与 newStartIdx 向后移动一位
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldStartVnod_ o H 9 ] ( $ d We = oldCh[++olz E 4 [ k $ H } pdStartIdx];
newA 4 z } f 6StartVnode = newt _ N 5Ch[++newStartIdx];
}
其次是Q m } s R D K Z o d % } s 6ldEndVnode
与 newEndVnoX | U = W - 7de
契合 sameVnode
,也便是两A F B个 VNode
的完毕是相同的 VNode` T S }
,相同进行 patchVnode
操作并将 oldEnH * c @ @dVnode
与 newEndVnode
向前移动一位。
if (sameVnode(oldEndVnode,m ^ r | ? d 3 newEndVnode)) {
// 其次是 oldEndVnode 与 newEndVnode 契合 sameVnode,
// 也便是两个 VNode 的完毕是相同 z N } Z 8 @ z的 VNode,相同进行 pA y a X ^atchVnode 操作并将 oldEndVnode 与 newEnh w [ Q pdVnode_ f U 向前移动一位。
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue= m r W X , i n [,
newCh,
newEndIdx
);
oldEndVnode = oldCh[--oldEe ( K x O mndIdx];
newEndVnode = newCh[--newEnd r T KIdx];
}
接下来是oldStartVnode
与 newEndVnodx O % l ] 1 R X ue
契合 sameVnode
的时分,也便是老 VNode
节点的头部与新 VNode
节点的尾部是同R d 5一节点的时分,将 oldStartVnode.elm
这个节点直接移动到 oldEndVnode.elm
这个节点的后 S N边即可。然后 oldStartIdx
向后移动一位,newEndIdx
向前移动一位。
if (sameVnode(oldStartVnode, newEndVnode)) {
// oldStartVnode 与 newEndVnode 契合 sameVnode 的时分,
// 也便是老 VNode 节点的头部与新 VNz 9 V 8 Yo* ; p + ^ !de 节点的尾部是同一节点的时分,
// 将 oldStartVnode.elm 这个节点直接移动到 oldEndVnodeZ ^ a ?.elm 这个节点的后边即} y h n |可。然后 oldStartIdx 向后移动一位,nE K FewEnH ! =dIdx 向前移动一位。
patchVnode(
oldSd f { [ ? vtartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVA n ] I f o tnode.elm)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdxz # J [ V];
}
最后是oll 1 a D f f / i )dEndVnode
与 newStartVnode
契合 sameVnode
时,也便是老 VNode
节点的尾部与新 VNode
节点的头部是同一节点的时分m p ! v @ M,将 oldEndVnode.elm
刺进g d i ; $ / X 到 ol@ [ ydStartVnode.k L [ q e # ~ [elm
前面。相同的,6 j t c = ^ t q 7oldEndIdx
向前移动一位,newStartIdx
向后移动一位。
if (sameVnode(oldEndVnode, newStartVnode)) {
// old, D P Q 3 L ,EndVnode 与 newStartVw 1 : Cnode 契合 sameVnode 时,
// 也便是老 VNode 节点的尾部与新 VNode 节点的头部是同一节点的时分,
// 将 oldEndVnode.elm 刺进到 oldStartVnode.elm 前面。相同的,oldEndIdx 向前移动一位,newStartIdx 向后移动一位。
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newSta) j [ o S :rtIdx
);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, o7 J TldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];q x q
nZ : s C % [ ? tewStartVnode = newCh[? E i n V++newStartIdx];
}
假如都不满意以上四种景象,那阐明没有相同的节点能够复用。
所以则经过查找事先建立好的以T B S ( t b .旧的 VNode
为 ke= Y 1 My
值,对应 index
为 value
值的哈希表。
从这个哈希表中找到与 newStartVnode
一致 key
的旧的 VNode
节点,假如两者满意 sameVnode
的条件,在进行 patchVnode
的一起会将这个实在 dom
移动到 oldStartVnode
对应的实在 dom
的前面;假如没有找到,则阐F H E Q %明当时索引下的新的 VNode5 % Z
节点在旧的 VNode
队列; E Z ^ s Y y C P中不存在,无法进行节点的复用,那么就只能调用 createElm
创立一个新的 dom
节点放到当时 newStartIdx
的位置。
最后还有一段代码:
// while 循环完毕
if (oldStartIdx > oldEndIdx) {
// 假如 oldStartIdx > oldEndIdx,阐明老节点比对完了,可是新节点还有多的,需求将新节点刺进到实在 DO$ | } P e R M 中去a h t M a Y } g Y,调用 addVnodes 将这些节点刺进即( N * c s 3可。
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
adde z W 3 s & 1Vnodes(
parentElm,
refElm,
newCh,
n@ B 8 o Q ) x m UewStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else if (newStartIdx > newEndIdx) {
// 假如满意 nX = W % iewStartIdx >x / | newEndIdx 条件,阐明新节点比对完了,老节点还有多,将这些无用的老节点经过 removeVnodesq 1 t 批量删除即可。
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
当 while
循环完毕今后,假如 oldStartIdx > oldEndIdx
,阐明老节点I % B F 5 N比对完了,可是新节点还有多的,需求将9 C I k { *新节p C J $ R 点刺进到实在 DOM
中去,调用 addVnodes
将这些节点刺进即可。
假如满5 / f S 7 }意 newSS ] ) q 4 c u *tartIdx > newEndIdx
条件J t ~ n ` ` 4 j,阐明新节点E & k ~比对# ~ 8 m R 2 完了,老节点还有多,6 p I将这些无用的老节点经过 removeVnodes
批量删除即可。
Vue 组件中的 data 为什么是8 ] I N 9 e个函数?
❝
其实这个问题还有下半句:而
new Vue
实例里,data
能够直接是一个目标?❞
先来看下平时在组件和new Vue
时运用data
的场景:
// 组件
data() {
return {
msg: "hello 森林",
}
}
// new Vue
new Vue({
data: {
msg: 'hello jack-cool'
},
el: '#app',
router,
template: '<App/>',
cp r ) j W p ]omponents: {
App
}
})
咱们知道,Vue
组件其实便是一个Vue
实例。
JS
中X . 8 (的实例是经过结构函数
来创立的,每个结构函数能够new
出许多个实例,那么每个实例都会承继原型上的办法或特点。
Vue
的data
数据其实是Vue
原型上的特点,数据存在于内存傍边
Vue
为了确保每个实例上的data
数据的独立性,规定了有必要运用函数,而不是目标。
因为运用目标的话,每个实例(组件)上运用a P w的data
数据是相互影响的,这当然就不是咱们想要的了。目标是对于内存地址的引证,直接界说个目标的话组件之间都会运用这个目标,这样会造成组件之间数据相互影响。
咱们来看个示例:
// 创立一个简Z T L O B [略的构建函数
var MyComponentE I t ? = function() {
// ...
}
// 原型链目标上设置data数据,data设为Object
MyH 7 0Component.protot= 0 T W U .ype.data = {
name: '森林x { X Y x J Y',
age: 20,
}
// 创立两个实例:春娇,志明
var chunjiao = new MyComponent/ E I y B()
var zhiming = new MyComponent()
// 默认状态下春娇和志明的年纪相同
console.log(chunjiao.data.age === zhiming.data.age) // true
// 改动春娇的年+ } T x C 4 ! J A纪
chunjiao.data.age = 25;
// 打印志明的年纪,发现因为改动了春娇的年纪,成果造成志明的年纪也变了
console.log(chunjiao.data.age)// 25
console.log(zhimi^ / W ` t 2 ] cng.data.age) // 25
运用函数后,运用( ) : S的是data()
函数,data()1 j b J L
函数中的this
指向的是当时实例自身,就不会相互影响了。
总结一下,G R i l V {便是:
组件中的data
是一个函数的原因在于:同一个组件被复用多次,会创立多个实例。这些实例用的是同一个结构函数,假如 data
是一个W 6 j * h i W H目标的话。那么一切组件都同享了同一个目标。为了确保组件的数据独立性要@ H + 5 m Z求每个组件有必要z M * w d x t经过 data
函数返回一个目标作为组件的状态。
而 new Vue
的实例,是不会被复用的,{ Q ) W因而不存在引证目标的问题。
谈谈你对 Vue 生命周期的了解?
答复这个问题,咱们先要概括的答复一{ 2 ?下Vue生命周期
是什么:
Vue
实例有一个完好的生命周期,也– E C q @ u q ` N便是从开端 n C 创立、初S S B ! s始化数据、编译模版、挂载 Dom
-> 烘托、更新 -> 烘? 8 M i u托、卸载等一系列过程– P f c,咱们称这是 Vue
的生命周期。
下面的表格展@ x K K J 3现了每个生命周期分别在什么时分被调用:
生命周期 | 描述 |
---|---|
beforeCreate |
在实例初始化之后,数据观测(data observer ) 之前被调用。 |
created5 i ` E | ` { & |
实例现已创立完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer ),特点和办法的运算, watch/event 事情回调。L M & c #但t 9 } # T C W实在 dom 还没有生成,$el 还不可用 |
beforeMount |
在挂载开端之前被调用,相关的 r] + ? ` cender# 6 F Z 函数初次被调用。 |
mounted |
el 被新创立的 vm.$el 替换,并挂载到实例上去之后调/ z T 0 V用该钩子q } G 5。 |
beforeUpdate |
数据更新时调用,发生在虚拟 DOM 从头烘托和打补丁之前。 |
updatep z % P E M Y yd |
因为数据$ e 8 N 5 A更改导B f : U R致的虚拟 DOM 从头烘托和d R X打补丁,在这之后5 9 f G 9会调用该钩子~ A o x。 |
activited |
keep-alive 专属,组件被激活时调用 |
deactivated |
keep-alive 专属,组件被销毁时调用 |
beforeDestory |
实例销毁之前调用。在这一# | f 2步,实例仍然彻底可用。 |
destoryed |
Vue 实例销毁后调用。 |
-
Vue
本质上是一个结构函数,界说在src/core/instance/index.js
中:
// src/core/instance/index.js
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this insM ! y ) w G Itanceof V) L P ; Cue2 K B H)) {
warn(Q V s G X [ _ f"Vue is a constructor and should be called with the `nu ) e y = 8 f @ei ? # ^ d w q ? fw` keyword");
}
this._init(options);
}
-
结构函数的中Q S [ G l ( P ` t心是调用了 _ p J !init
办法,_init
界说在src/core/instance/init.js
中:
// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
conJ F F ist vm: Component = this;
// a uid
vm._uidu S R $ c = uid++;
[1];
let startTag, endTag;
/* istanbul ignore if ** F L ) I &/
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
startTag = `v} ; f xue-perf-start:${vm._uid}`;
endTag = `! W 9 *vue-perf-e[ C ;nd:${vm._uid}`;
mark(stau c I / O J ] ( ?rtTag);
}
// a fli 8 ; JagF ; / ! L x I d to avoid this being observed
vm._ik 5 V 7 & r 2 usVue = true;
// merge options
i] f a n m U rf (options && options._isComponent) {
// optimize internal component instantiation
// since dynaj * * S { 2 e X ,mic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$optioR 1 O + z ; L hns = mergeOptions(
resolveConstructorOptionsu l O 2(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")[2Z e T g _ p X];
/* istanbul ignore if */
if (procesa ? r , , Z Qs.env.NODE_ENV !== "productiW ~ b N mon" && config.performance && mark) {
vm._name = formatT E N AComponentName(vm, false);
mark(endTag);
measure(`vue ${vm._name} init`, startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
_init
内调用了许多初始化函数,从函数名称能够e A K f A &看出分别是履行初始化生命周期(initLifecycle
)、初始化事情中心(initEvents
)、初始化烘托(initRender
)、履行beforeCreate
钩子(callHook(vm, 'beforeCreate')
)、解析 inject(initInjections
)、初j J ] n a | ^ t始化k s 7状态(i2 l c p Y v E P 2nitState
)、解析 prh W m $ J jovide(initProvS Q ^ x E s .ide
)、履行created
钩子(callHook(vm, 'created')
)。
-
在 _init
函数的最后有判断假如有el
就履行$mount
办法。界说在# F nsrc/platforms/` [ R y r j ? Eweb/entry-runtime-wit2 U 3 }h-compiler.js
中:
/4 S r ; 9 + C/ src+ n X - S 9/platforms/web/entry-runtime-with-ce T a }ompiler.js
// ...
const mount1 8 = & J n K = Vue.prototype.$mount;
Vue.prototype.$+ G j 7 4 H x Ymount = function(
el?: string | Element,
hydrating?: boolean
): ComponW 1 &ent {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documen3 / 8 6 ! = ptElement) {
process.env.NODE_ENV !== "pr} e o ! ! Q w _oduction" &&
warn(
`Do not mount Vue to <html> or <body> - mount to norma` j v 0 l hl elements instead.`
);
return this;
}
const options = this.$options;
// resolve template/el and convert to res j F 7 Tnder functn ^ @ Q Xion
if (!options.render) {
let template = options.template;
if (template: & ^ j S .) {
if (typeofw + . Y ? h template === "st. Z } : cring") {
// ...
} else if (template.nodeType) {
template = template.innerHTML;
} else {
// ...
return this;a m Q ( D h 9 1
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
// ...
}
}
return mount.call(this, el, hydrating);
};
// ...
export defau# $ L N U t L * rlt Vue;
这儿面主要做了两件事:
1、f $ r ] 6 s . Z l 重写了Vue
函数的原0 D J = A型上的$mount
函数
2、 判断是否有模板,而且将模板转化成render
函数
最后调用了runtime
的z N T hmount
办法,用来挂载组件,也便是mountComponent
办法。
-
mountComponent
内首先调用了beforeMount
办法,然后在初次烘托和更新后会履行vm._update(vm._render(), hydrating)
办法e | – n e ,。最后烘托完成后调用* s : /mounted
钩子。 -
be) 9 yforeUpdate
和up$ u R E 5dated
钩子是在页面发生变化,触发更新后,被调用的,# P . h ] % e对应是在src/core/obser^ d lver/scheduler.js@ V S Q V
的flushSchedulerQueue
函数中。 -
beforeDestroy
和destroyed
都在履行$destroy
函数时被调用。$destroy
函数是界说在Vue.prototype
上的一个办法,对应在src/core/insts F M - .ance/lifecycle.js
文件中:
// src/core/instance/lifecycle.js
Vz R T r ~ ^ k Wue.prototype.$destroy = function() {
const vm: Component = this;
if (vm._isBeingDestroyed) {
return;
}
cal5 3 ]lHook(vm, "beforeDestroy")B O b;
vm._isBeingDestroyed = true;
// remove selC P [ 2f from parent
consti D c l r W U @ parent = vm.$parent;
if (paro I # [ 7 . n 3 Bent && !parent._isBeinl 7 R _ )gDestroyed && !vm.$options.abstract) {
rm L C j q 7 O ]emove(parent.$children, vm);
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown();` 2 ( v | M t z L
}
let i = vm._watchers.length;
while (i--) {
vm._watcM r , B r f m jhers[i].teardown();
}
// remove reZ b _ 0 t A S ] rference from data ob
// frozen object mb g xay not have observer.
if (v! p : ;m._data.__ob__) {
vm._dae * % / Pta.__ob__.vmCount--;
}
// call the last hook.{ G !..
vm._is: x } D ADestroyed = true;
// invoke des % c x L / 5 1 dtroy hooks on current rend` 6 :ered tree
vm.__patch__(vm._vnode, null);
// fire destroyed hook
callHook(vm, "destroyedA _ s 2 D o");
//3 V 1 H K tur| / 6 n Rn off all instance listeners* Z E : /.
vm.$off();
// remove __vue__ reference
if (vmo p / %.$el) {
vm.$el.__vue__ = null;
}
// release circular refery 4 7 qence (#6759)
if (vm.$vnode) {
vV N 0 P H I $ :m.$vnode.parent = nu~ v ) X ull;
}
};
Vue 中常见的Q 6 U 7功能优化方法
编码优化
-
尽量不要将一切的数据都放在 data
中,Z 2 T qdata
中的数据都会增加getter
和setter
,c b a – +会搜集对应的watcher
-
vue
在v-for
时给每项元素绑定事情尽量用事情署理 -
拆分组件( 进步复用H | D P m S i 6性、增加代码的可维护性,减少不必要的烘托 ) -
v-if
当值为false
时内部指令不会履行,具有阻断功能,许多状况下运用v-if
替代v-show
-
合理运用路由懒加z . ! – L 7 ! I T载、异步组件 -
Objecl U L . ?t.freeze
冻住数据
用户体验
-
app-skeleton
骨架屏 -
pwa
serviceworker
加载功能优化
-
第三方模块按需导入 ( babel-plugin-component
) -
滚动到可视区域动态加载 ( https://tangbc.github.io/vue-virtual-scrollz S Q U q-list
) -
图片懒c o z ; $ N ]加载 ( https://github.com/hilongjw/vue-lazyload.git
)
SEO 优化
-
预烘托插件 prerender-spa-z : .plugX h ~in
-
服务端烘托 ssr
打包优化
-
运用 cdn
的方法加载第三方模块 -
多线程} 3 j P打包 happypack
、parallel-webpack
-
控制包文件大小( tree shaking
/spF q N q litQ ; ~ W jChunksPlugin
)N w k -
运用x U U % & DllPlugin
进步打包速度
缓存/压缩
-
客户端缓存/服务端缓存 -
服务端 gzip
压缩