面试当中肯定会被问到有关
Vue的一些问题,在这里我收集了一些题目,写下自己的回答,进行一次自我面试。【如果涉及到Vue3.0会进行标注,否则默认2.X】
- Vue中为什么采用异步渲染?
-
Vue中的响应式数据发生变化后,会触发dep.notify()方法通知渲染watcher进行更新操作;watcher中的update方法会将当前的渲染watcher放入到一个队列中;这里Vue会给渲染watcher标记一个uid,如果是相同的则只要添加一次即可; -
设想一下,如果
Vue并没有采取异步渲染,那么多次更新数据都要进行了一次渲染,这样性能就会很差。出于性能考虑,Vue采取了异步渲染方式;顺便说下,因为是异步渲染,所以我们更新数据后不能马上获取到dom上最新的值,如果有需求可用nextTick;
- 说一下nextTick的原理?
- 涉及的知识点有
宏任务、微任务;浏览器事件循环中遇到同步任务直接执行,异步任务分为宏任务、微任务,执行顺序是首先执行同步任务然后将宏任务添加到宏任务队列,在执行所有的微任务,完毕后又会执行新的宏任务,依次类推下去。
// 这是一个同步任务
console.log('1')
// 这是一个宏任务
setTimeout(function () {
console.log('2')
});
new Promise(function (resolve) {
// 这里是同步任务
console.log('3');
resolve();
// then是一个微任务
}).then(function () {
console.log('4')
setTimeout(function () {
console.log('5')
});
});
nextTick就是把我们的视图更新的操作塞到一个微任务或者宏任务中,来进行异步执行;Vue中首先检测是否支持promise,支持则进行promise.then(flushCallbacks)执行回调,否则看是否支持MutationObserver,支持则进行new MutationObserver(flushCallbacks),否则看是否支持setImmediate支持则进行setImmediate(flushCallbacks),否则进行setTimeout(flushCallbacks, 0);
- 谈一谈Vue中的响应式原理
-
Vue中的响应式是其一大特色,何为响应式?就是我们由数据来驱动视图的更新。一旦发现我们所依赖的数据发生了变化,我们的视图就会执行更新;这带来的好处就是我们只要关心数据这一层就好,不用过多的关心视图层的变化。
-
Vue中的响应式核心方法就是使用ES5中
Object.defineProperty对数据操作进行‘劫持’,在读取数据的时候会执行get方法,然后进行依赖收集操作,每个依赖收集当前的渲染watcher或者计算watcher;收集的过程会形成一个相互映射关系,每个watcher会收集对应的依赖,每个依赖会收集当前的watcher;当我们进行数据修改操作的时候会执行set方法,这个方法将会通知该依赖的所有watcher触发update方法进行视图更新;当然Vue2.X中的响应式还是存在一些不可为的东西,比如数据的索引直接赋值,就不会进行响应式;当然Vue2.X中也提供了如Vue.$set、Vue.$delete方法进行间接的进行响应式;在Vue3.X中使用了Proxy则解决了这个问题;
- 你知道Vue中是如何检测数组响应式变化?
-
Vue中初始化的时候会对数据进行了递归遍历,让每个属性值进行响应式,如果是数组则遍历每一项进行响应式处理;
-
对于数据的新增、删除等原生方法操作,我觉得Vue比较很巧妙的进行了处理;利用改写数组的原型链的方法,从而拦截了
push、delete、splice原生方法;这是怎么实现的呢?伪代码如下:
// array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
...
methodsToPatch.forEach(function (method) {
// cache original 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)
// notify change
ob.dep.notify()
return result
})
})
// observe.js
...
if (hasProto) { // 判断浏览器是否兼容 __prop__
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src // 完成数组的原型链修改, 从而使得数组变成响应式的 ( pop, push, shift, unshift, ... )
/* eslint-enable no-proto */
}
...
通过进行原型链的拦截,我们给每次添加的值进行响应式然后手动触发更新;Vue中的Vue.$set、Vue.$delete实现的原理也就是通过数组的splice进行实现;
- 说一说Vue中的watch
-
首先说下什么是
watch,就是一个响应式数据的监听器,一旦监听的数据发生变化,就会触发回调函数进而做一些复杂的逻辑处理; -
具体的用法如下:
...
let app = new Vue({
el: '#root',
data() {
return {
msg: 'hello world'
}
},
created() {
},
methods: {
changeMsg() {
this.msg = 'hello world!'
}
},
watch: {
msg(value) {
console.log('value change', value)
}
}
})
...
还有一些高级点的用法:比如是否立即执行immediate:true,是否进行深度监听deep:true; 这里就不赘述,可以去官网查看;
- 实现原理
首先Vue在初始化initState方法的时候进行检测是否有用户写的watch,如果有则进入initWatch方法;
function initState(vm){
...
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
...
}
在initWatch方法中,对opts.watch进行遍历创建watcher;调用了createWatcher(vm, key, handler)
function createWatcher(vm, key, handler){
...
return vm.$watch(expOrFn, handler, options)
}
好了最终我们通过vm.$watch揭开了watch的神秘面纱;其实就是一个watcher.不过这里的watcher被标记为用户watcher,user:true;这里还有个逻辑就是options.immediate=true,这就是上述高级用法中立即执行的实现逻辑;
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
...
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
...
};
提到watcher我们很容易想到的就是进行依赖收集,形成依赖跟watcher相互绑定关系;那么如何进行依赖收集呢?让我们再次回到watcher.js中看看Vue对watch的处理.
在watcher初始化中,有一段处理watch表达式的逻辑:
if (typeof expOrFn === 'function') { // 就是 render 函数
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
有前面可知watch传过来的expOrFn是一个字符串key值,所以会走parsePath(expOrFn)方法,这里利用了函数柯里化的技巧返回了一个函数;接着初始化做重要的一件事调用this.get()方法;这个方法就是会进行this.getter方法触发,如果是渲染watcher则进行视图更新,如果是用户watcher则进行取值依赖收集;然后一旦依赖的值发生了变化就会触发各个watcher的update方法;接下来的流程就跟上述响应式流程一样了。
好了,以上便是对watch的介绍
- 说一说Vue中的计算属性computed
-
什么是
computed计算属性,计算属性是基于响应式数据进行缓存的,只有响应式数据发生了变化,才会进行重新计算。这样的好处是只要依赖的响应式数据不发生变化,我们就不会多次的触发所依赖的响应式数据的get方法; -
具体的用法如下:
...
let app = new Vue({
el: '#root',
data() {
return {
msg: 'hello world'
}
},
computed:{
computedMsg(){
return this.msg+'!'
}
},
created() {
},
methods: {
changeMsg() {
this.msg = 'hello world!'
}
}
})
...
- 实现原理
首先
Vue初始化initState方法的时候进行检测时候有computed属性;
function initState(vm){
...
if (opts.computed) { initComputed(vm, opts.computed); }
...
}
在initComputed中做了以下几件事;1:在Vue实例上挂了一个_computedWatchers对象。2:通过遍历computed属性分别往_computedWatchers对象上添加添加计算watcher;并进行了计算属性的计算defineComputed;这里面有个小逻辑,因为我们用户手写计算属性的时候,是可以写get的,所以Vue中进行了处理;还有就是检测我们的computed属性是否已经存在vm中,然后抛出错误信息。这也是我们经常会出现的一些错误;最终得出一个小结论computed也是一个watcher;
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
既然也是一个watcher,那么我们还是进入watcher.js中看看对computed具体做了哪些处理;注意下在创建一个计算watcher的时候传入了一个computedWatcherOptions参数,这也是计算属性的一个关键。下面来看一下:
this.value = this.lazy // computedWatcherOptions 传入的
? undefined
: this.get()
在计算watcher初始化中,并没有进行this.get()调用;然后我们进入defineComputed这个逻辑中;
// 删除掉服务端渲染逻辑
function defineComputed (
target,
key,
userDef
) {
if (typeof userDef === 'function') {
createComputedGetter(key)
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
这里的主要做了一件事就是要对每个要计算的属性进行响应式处理;然后手写了get和set方法,那么我们看下getcreateComputedGetter:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
这里的几个东西需要我们回忆下,_computedWatchers、dirty以及watcher.evaluate();首先是_computedWatchers就是我们的计算属性watcher,我们这里根据key拿到之前存储的watcher,然后因为dirty=true,所以会进行一次计算,这个时候就会触发watcher中的get方法:
evaluate () {
this.value = this.get()
this.dirty = false
}
这就会读取依赖this.msg,这里就会触发msg对计算watcher的收集;仅仅是收集计算watcher是不够的,我们肯定还要收集渲染watcher,于是Vue又进行了一次watcher收集:
if (Dep.target) {
watcher.depend();
}
为什么这样可以收集到渲染watcher呢?
watcher的get方法会对watcher进行压栈出栈的操作,如何理解呢?首先在执行render函数时候会对计算属性computedMsg(上述例子)进行读取,调用了get方法,执行了pushTarget;此时的targetStack为[渲染watcher],然后再进行msg(上述例子)收集计算watcher又调用了get方法,再次进行pushTarget,这时候的targetStack应该是[渲染watcher,计算watcher],执行完get方法后就会调用popTarget;此时的targetStack就变成了[渲染watcher];这样就能很巧妙的完成了msg(上述例子)对渲染watcher收集了。
以上就是对computed计算属性的初始化分析,那又是如何根据依赖的变化而进行重新渲染的呢?
我们已经完成了计算属性的相关依赖的收集,所以只要依赖发生了变化就会触发计算wacther、渲染watcher的更新;那问题来了,我们如何进行重新计算呢?对的,就是改掉这个dirty标识;我们在计算watcher进行update时候设置这个dirty=true:然后执行渲染watcher的时候进行值的更新以及视图的更新;
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
我们都知道computed具有缓存数据作用,主要是靠什么进行缓存的呢?
就是靠我们的dirty标识,dirty的改变是依赖于响应数据的改变的,只要响应式数据不改变我们就不会重新计算,而是返回之前的计算值;
- watch跟computed的区别
首先两者都是依赖于响应式数据的,computed具有缓存作用,只有依赖的数据发生改变才会进行重新计算,watch则只要监听数据发生了变化就会触发回调函数;watch更加偏向于一些异步以及复杂的逻辑处理;正如官网给出的比较demo,在计算fullName=firstName+lastname时候,如果使用watch进行侦听则比较繁琐,建议还是使用computed;
v-for和v-if的优先级以及如何进行性能提升? 有过Vue开发经验的小伙伴都会知道,如果你的编辑器里有这样一段代码:
<ul>
<li v-for="(item,index) in 5" v-if="isExpand">
{{item}}
</li>
</ul>
这样控制台会有一个警告,官网也有给过一个说明永远不要把 v-if 和 v-for 同时用在同一个元素上。;那具体的原因,官网是没有给出的,我们来看一看两种编译后的代码块,对比一下就能一目了然。
- 优先级比较
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
...
}
从这段逻辑看for的优先级是在if前面的; 2. 同时使用后的代码块:
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('div', {
staticClass: "lists"
}, [_c('ul', _l((5), function (item, index) {
return (isExpand) ? _c('li', [_v("\n " + _s(item) + "\n")]) :
_e()
}), 0)])])
}
我们先忽略里面的_c、_l,_l逻辑就是渲染了一个列表,注意这里的逻辑是先遍历一遍然后处理v-if的isExpand三元表达式逻辑。在看一下我们另一段写发代码块;
<ul v-if="isExpand">
<li v-for="(item,index) in 5">
{{item}}
</li>
</ul>
编译后的代码如下:
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('div', {
staticClass: "lists"
}, [(isExpand) ? _c('ul', _l((5), function (item, index) {
return _c('li', [_v("\n " + _s(item) + "\n ")])
}), 0) : _e()])])
}
从这段逻辑可以看出,我们是先进行三元表达式的计算。当结果是true的时候就进行了列表的渲染;所以把v-if写在v-for上层能够带来一定的性能优化。