面试当中肯定会被问到有关
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
上层能够带来一定的性能优化。