3

请你挑战一下这几道nextTick面试题

 3 years ago
source link: https://www.kai666666.top/2021/07/30/%E8%AF%B7%E4%BD%A0%E6%8C%91%E6%88%98%E4%B8%80%E4%B8%8B%E8%BF%99%E5%87%A0%E9%81%93nextTick%E9%9D%A2%E8%AF%95%E9%A2%98/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

请你挑战一下这几道nextTick面试题

发表于 2021-07-30

| 分类于 JavaScript

| 0

| 阅读次数: 41

Vue大家再熟悉不过了,Vue的this.$nextTick大家也再熟悉不过了,今天我们就来看看自创的nextTick相关的几道面试题,看看你是否真正理解Vue的nextTick


<template>
<div>
<div ref="text">{{ text }}</div>
</div>
</template>

<script>
export default {
data() {
return {
text: 0
}
},
mounted() {
this.$nextTick(() => {
// 下面语句输出什么?
console.log(this.$refs.text.textContent)
})
this.text = 1
this.text = 2
this.text = 3
}
}
</script>

给你3秒,请你仔细考虑一下这道题输入什么?3、2、1…OK,你的答案是3吗?如果是的话,那么恭喜你,答错了!正确答案是0。什么?this.$nextTick不是等DOM处理完后才执行吗,这里怎么不适用了?等等我们再来一题,至于为什么最后再讨论。

<template>
<div>
<div ref="text">{{ text }}</div>
</div>
</template>

<script>
export default {
data() {
return {
text: 0
}
},
mounted() {
this.text = 4
this.$nextTick(() => {
// 下面语句输出什么?
console.log(this.$refs.text.textContent)
})
this.text = 1
this.text = 2
this.text = 3
}
}
</script>

题目2和题目1就多了一行this.text = 4,这个打印的是多少呢?再给你3秒,3、2、1…OK,正确答案是3,跟你的答案一样吗?如果一样那么恭喜你了,我们再来一道过过瘾。

<template>
<div>
<div ref="text">{{ text }}</div>
</div>
</template>

<script>
export default {
data() {
return {
text: 0
}
},
mounted() {
this.text = 0
this.$nextTick(() => {
// 下面语句输出什么?
console.log(this.$refs.text.textContent)
})
this.text = 1
this.text = 2
this.text = 3
}
}
</script>

题目3和题目2唯一的区别是把this.text = 4替换成了this.text = 0,你的答案是多少呢?3、2、1…OK,正确答案是0!此刻你的内心:“啊…这!!!”。别急,还有呢!!

<template>
<div>
<div ref="text">{{ text }}</div>
<div>{{ text1 }}</div>
</div>
</template>

<script>
export default {
data() {
return {
text: 0,
text1: 0
}
},
mounted() {
this.text1 = 1
this.$nextTick(() => {
// 下面语句输出什么?
console.log(this.$refs.text.textContent)
})
this.text = 1
this.text = 2
this.text = 3
}
}
</script>

本题增加了一个text1变量,并且修改了text1的值,这次打印的是多少呢?3、2、1…OK,正确答案是3

<template>
<div>
<div ref="text">{{ text }}</div>
</div>
</template>

<script>
export default {
data() {
return {
text: 0,
text1: 0
}
},
mounted() {
this.text1 = 1
this.$nextTick(() => {
// 下面语句输出什么?
console.log(this.$refs.text.textContent)
})
this.text = 1
this.text = 2
this.text = 3
}
}
</script>

题目5比题目4模板中少了一行,你再想想这次打印的是多少?3、2、1…OK,正确答案是0

好了,我们这里总共5道题,答对3道算及格,你及格了吗?接下来我们分析一下。

这节我们会粘贴大量Vue源码,大家只要看关键的代码就可以了,觉得看源码枯燥难懂的同学可以直接看本节最后的总结。

像如this.text = 1来设置值的时候,Vue会帮助我们异步的去更新视图,这里涉及Vue响应式原理,最终会调用nextTick来更新视图,本题中主要考察的是nextTick先后的顺序。哪怕你没看过Vue的源码也肯定知道Vue响应式原理是通过Object.defineProperty这个API来实现的吧,他是怎么做的呢?源码如下:

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/setters
const getter = property && property.get
const setter = property && property.set
if ((!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) : val
if (Dep.target) {
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 setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

这里的代码有点长,但是你不要怕,你只要看29行和57行就行了,在第29行说明当调用get的时候会调用dep.depend(),在第57行的说明当调用set的时候会调用dep.notify()

那么dep的depend()notify()又做了什么呢?

export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

addSub (sub: Watcher) {
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null

这里的Dep.targetsub都是Watcher对象的实例。这里我们先捋一捋,当我们调用属性的set的时候,会调用dep.notify()而该函数又调用了subs[i].update()也就是Watcher对象的update()方法,那么Watcherupdate和上面给到的addDep方法又做了什么呢?

addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

addDep方法中可以看到当Watcher对象调用addDep的时候,实际上是传入的Dep对象把自己当做sub添加进去,这样在Dep对象调用notify才能通知到对应的Watcher,也就是说组件的data在调用set前一定要调用get才会通知对应的Watcher来更新视图,实际上只要模板中用到了变量就会调用变量的get

update方法中我们看到有一个queueWatcher(this)的方法,这个又是搞什么呢?

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true

if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}

queueWatcher我们可以看到如果has[id] == null就把has[id]设置为true,然后把watcher插入到队列中。由于一个组件对应一个Watcher(当然计算属性也会对应Watcher,这里说的是组件级别的Watcher),当一个属性改变后就会调用has[id] = true,这样当再把当前组件的属性改了以后,由于has[id]已经是true了,就没必要再加入到队列中了,毕竟更新视图,一次性直接全改了。最后你看到最重要的一行代码,就是nextTick(flushSchedulerQueue),我们终于看到nextTick的影子了。调用nextTick的时候会把传入的函数push进回调队列里面,也就是这里把flushSchedulerQueue放在队列的尾部了,这个函数又做了什么呢?

function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id

queue.sort((a, b) => a.id - b.id)

// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// 省略部分代码...
}

// 省略部分代码...
}

可以看到flushSchedulerQueue先给queue进行了排序,queue中存放的就是watcher,如果有watcher.before的话则调用一下,处理完后把has[id]置为null,最关键的一行是调用了watcher.run(),我们再看看watcher.run()做了什么。

run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

run方法貌似就设置了一下value的值,另外执行了一个this.cb.call(this.vm, value, oldValue)方法,这个cb是Watcher构造函数的第三个参数,通常情况下是一个空函数。这里最重要的是const value = this.get()这行代码,这里调用了一下Watcher的get方法,这个get方法是什么呢?

get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

这里需要注意的是get方法开始的时候调用pushTarget,结束的时候调用popTarget那么在这两段中间的代码调用Dep.target时指向的就是当前的Watcher对象。另外get方法中有一个this.getter,这个的值如下根据Watcher第二个参数expOrFn来定的,我们可以在Watcher的构造方法中看到getter的取值逻辑:

class Watcher {
// 部分代码省略
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
this.value = this.lazy
? undefined
: this.get()
}

// 部分代码省略
}

如果第二个参数expOrFn是函数的话this.getter就是它,如果是a.b.c这种字符串的话则进行解析,否则给个空函数。那么这第二个参数expOrFn又是什么呢,请看下面:

function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
// 省略部分代码...
callHook(vm, 'beforeMount');

var updateComponent;

// 省略部分代码...

updateComponent = function () {
vm._update(vm._render(), hydrating);
};

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}

mountComponent代码写的是非常漂亮!首先调用了beforeMount生命周期方法,然后初始化Watcher,最后调用mounted生命周期方法。我们注意看Watcher的第二个参数updateComponent,该函数的实现是vm._update(vm._render(), hydrating);也就是说Watcher调用get方法的时候实际上是去更新视图了。这里你需要注意一点,Watcher的constructor中最后会调用this.get()而这时最终也会调用updateComponent方法,这也就是在beforeMountmounted之间会把视图更新在DOM上的代码。

  1. Vue会在beforeMount和mounted生命周期之间创建Watcher,并更新视图,当组件的Watcher对象调用run方法的时候,最终会调用vm._update(vm._render(), hydrating);来更新视图;
  2. 当数据改变的时候,会调用Object.defineProperty中的set,这时除了赋值以外,还会调用dep.notify()来通知已收集依赖的Watcher调用update方法进行更新;
  3. Watcher调用update方法进行更新时,会调用queueWatcher(this)把当前的Watcher对象加入到队列中,同时执行nextTick(flushSchedulerQueue)
  4. 当下一个tick执行的时候会调用flushSchedulerQueue方法,该方法会调用watcher.run()方法,进而调用watcher.get()用来更新视图;
  5. 只有先调用get收集了依赖的data,在set时才可能会引起视图的更新。

通过源码分析我们对Vue修改视图的逻辑有了更深的认识,现在我们再回过头来看看前面的题。

题目1:由于先调用的this.$nextTick后修改的数据,这样数据后引起视图更改的nextTick会在this.$nextTick之后,所以打印未修改前的值,所以是0
题目2:由于先修改的数据后调用的this.$nextTick,这样数据后引起视图更改的nextTick会在this.$nextTick之前,由于nextTick是异步的,当nextTick执行的时候,值已经是最后一次修改的值了,所以是3
题目3:虽然首先调用的赋值,但是值并没有改变,在Object.definePropertyset方法中可以看到,如果值相同直接return了,所以本题和题目1其实是一样的,也是0
题目4:修改text1的时候也会使用nextTick来更新视图,而this.$nextTick中的函数排在更新完视图后执行,所以结果是更新后的值,也就是3
题目5:在模板中把text1部分去掉了,那么text1相当于就没有调用get了,这样就不会再调用dep.depend()来收集依赖了,所以text1修改并不会引起nextTick去更新视图,所以此题的情况跟题目1也是一样的了结果是0

通过本章的学习,估计你已经收获满满,现在来一道最难的题:

<template>
<div>
<div ref="text">{{ text }}</div>
</div>
</template>

<script>
export default {
data() {
return {
text: 0,
text1: 0
}
},
mounted() {
console.info(this.text1)

this.text1 = 1
this.$nextTick(() => {
// 下面语句输出什么?
console.log(this.$refs.text.textContent)
})
this.text = 1
this.text = 2
this.text = 3
}
}
</script>

这题是题目5的变种,在设置前通过console.info(this.text1)打印了一下text1的值,那么就会调用get方法,那么问题来了,此时结果是什么?3、2、1…OK,什么!你说3?哈哈,当然不对,这里还是0,为什么呢?这里虽然调用get方法了,但是Dep.targetundefined,所以也没有收集到依赖,毕竟在get方法中只有Dep.target不为空才去调用dep.depend()。那么为什么Dep.targetundefined的呢?之前说过Watcherget方法开始的时候调用pushTarget,结束的时候调用popTarget,而这个时候打印的时早就popTarget了,所以Dep.targetundefined。那么为什么写在模板里面就有了呢?实际上在mountComponent方法中创建Watcher时,构造方法最下面会调用Watcherget方法,get方法不是先会调用一下pushTarget吗?此时的Dep.target指向的是当前的Watcher对象,这个时候this.getter.call(vm, vm)实际调用的是vm._update(vm._render(), hydrating);,而vm._render就会处理模板中的变量,那么模板中变量的get也就会被调用了,所以放在模板中的变量在会被收集依赖


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK