前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >请你挑战一下这几道nextTick面试题

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

作者头像
kai666666
发布2024-07-11 19:07:00
160
发布2024-07-11 19:07:00
举报
文章被收录于专栏:橙光笔记橙光笔记

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

题目1

代码语言:javascript
复制
<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处理完后才执行吗,这里怎么不适用了?等等我们再来一题,至于为什么最后再讨论。

题目2

代码语言:javascript
复制
<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,跟你的答案一样吗?如果一样那么恭喜你了,我们再来一道过过瘾。

题目3

代码语言:javascript
复制
<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!此刻你的内心:**”啊…这!!!”**。别急,还有呢!!

题目4

代码语言:javascript
复制
<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

题目5

代码语言:javascript
复制
<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来实现的吧,他是怎么做的呢?源码如下:

代码语言:javascript
复制
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()又做了什么呢?

代码语言:javascript
复制
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方法又做了什么呢?

代码语言:javascript
复制
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)的方法,这个又是搞什么呢?

代码语言:javascript
复制
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放在队列的尾部了,这个函数又做了什么呢?

代码语言:javascript
复制
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()做了什么。

代码语言:javascript
复制
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方法是什么呢?

代码语言:javascript
复制
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的取值逻辑:

代码语言:javascript
复制
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又是什么呢,请看下面:

代码语言:javascript
复制
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.defineProperty的set方法中可以看到,如果值相同直接return了,所以本题和题目1其实是一样的,也是0。题目4:修改text1的时候也会使用nextTick来更新视图,而this.

题目6

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

代码语言:javascript
复制
<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也就会被调用了,所以放在模板中的变量在会被收集依赖

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 题目1
  • 题目2
  • 题目3
  • 题目4
  • 题目5
  • 源码分析
  • 回到本题
  • 题目6
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档