面试题:created
生命周期中两次修改数据,会触发几次页面更新?
一、同步的
先举个简单的同步的
例子:
new Vue({el: "#app",template: `<div><div>{{count}}</div></div>`,data() {return {count: 1,}},created() {this.count = 2;this.count = 3;},
});
在created
生命周期中,通过this.count = 2
和this.count = 3
的方式将this.count
重新赋值。
这里直接抛出答案:渲染一次。
为什么?
这个与数据的响应式处理有关,先看响应式处理的逻辑:
export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {// 重点:创建一个发布者实例const dep = new Dep()const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setif ((!getter || setter) && arguments.length === 2) {val = obj[key]}let childOb = !shallow && observe(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (Dep.target) {// 重点:进行当前正在计算的渲染Watcher的收集dep.depend()if (childOb) {childOb.dep.depend()if (Array.isArray(value)) {dependArray(value)}}}return value},set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val/* eslint-disable no-self-compare */if (newVal === value || (newVal !== newVal && value !== value)) {return}/* eslint-enable no-self-compare */if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()}// #7981: for accessor properties without setterif (getter && !setter) returnif (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = !shallow && observe(newVal)// 重点:当数据发生变化时,发布者实例dep会通知收集到的watcher进行更新dep.notify()}})
}
在数据响应式处理阶段,会实例化一个发布者dep
,并且通过Object.defineProperty
的方式为当前数据定义get
和set
函数。在生成虚拟vNode
的阶段,会触发get
函数中会进行当前正在计算的渲染Watcher
的收集,此时,发布者dep
的subs
中会多一个渲染Watcher
实例。在数据发生变化的时候,会触发set
函数,通知发布者dep
中subs
中的watcher
进行更新。
至于数据修改会触发几次更新,就与当前发布者dep
的subs
中收集了几次渲染watcher
有关了,再看watcher
收集和created
执行之间的顺序:
Vue.prototype._init = function (options) {// ...initState(vm);// ...callHook(vm, 'created');// ...if (vm.$options.el) {vm.$mount(vm.$options.el);}
}
我们知道在initState(vm)
阶段对数据进行响应式处理,但是此时发布者dep
的subs
还是空数组。当执行callHook(vm, 'created')
的时候,会执行this.count = 2
和this.count = 3
的逻辑,也的确会触发set
函数中的dep.notify
通知收集到的watcher
进行更新。但是,此时dep
的subs
是空数组,相当于啥也没做。
只有在vm.$mount(vm.$options.el)
执行过程中,生成虚拟vNode
的时候才会进行渲染Watcher
收集,此时,dep
的subs
才不为空。最终,通过vm.$mount(vm.$options.el)
进行了页面的一次渲染,并未因为this.count=2
或者this.count=3
而触发多余的页面更新。
简言之,就是created
钩子函数内的逻辑的执行是在渲染watcher
收集之前执行的,所以未引起因为数据变化而导致的页面更新。
二、异步的
同步的场景说完了,我们再举个异步的
例子:
new Vue({el: "#app",template: `<div><div>{{count}}</div></div>`,data() {return {count: 1,}},created() {setTimeout(() => {this.count = 2;}, 0)setTimeout(() => {this.count = 3;}, 0)},
});
在created
生命周期中,通过异步的方式执行this.count = 2
和this.count = 3
的方式将this.count
重新赋值。
这里直接抛出答案:首次渲染一次,因为数据变化导致的页面更新两次。
为什么?
这个就与eventLoop
事件循环机制有关了,我们知道javascript
是一个单线程执行的语言,当我们通过new Vue
实例化的过程中,会执行初始化方法this._init
方法,开始了Vue
底层的处理逻辑。当遇到setTimeout
异步操作时,会将其推入到异步队列
中去,等待当前同步任务执行完以后再去异步队列中取出队首元素进行执行。
当前例子中,在initState(vm)
阶段对数据进行响应式处理。当执行callHook(vm, 'created')
的时候,会将this.count = 2
和this.count = 3
的逻辑推入到异步队列等待执行。继续执行vm.$mount(vm.$options.el)
的过程中会去生成虚拟vNode
,进而触发get
函数的渲染Watcher
收集,此时,dep
的subs
中就有了一个渲染watcher
。
等首次页面渲染完成以后,会去执行this.count=2
的逻辑,数据的修改会触发set
函数中的dep.notify
,此时发布者dep
的subs
不为空,会引起页面的更新。同理,this.count=3
会再次引起页面数据的更新。也就是说,首次渲染一次,因为this.count=2
和this.count=3
还会导致页面更新两次。
三、附加
如果我改变的值和data
中定义的值一致呢?
new Vue({el: "#app",template: `<div><div>{{count}}</div></div>`,data() {return {count: 1,}},created() {setTimeout(() => {this.count = 1;}, 0)},
});
这个时候,在触发set
的逻辑中,会当执行到if (newVal === value || (newVal !== newVal && value !== value)) { return }
的逻辑,不会再执行到dep.notify
,这种场景下数据的数据也不会引起页面的再次更新。
总结
从生命周期
created
和页面渲染的先后顺序,Object.defineProperty
触发get
和set
函数的机理,以及eventLoop
事件循环机制入手,去分析created
中两次数据修改会触发几次页面更新的问题就会清晰很多。
最后
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享