Vue 2:nextTick 超全解析

在 Vue 官网上,对于 nextTick 的说法是这样的:
image.png

问题入手,灵魂五问

相信大家大部分都会用,那我提几个问题供大家思考一下:

  1. 如果以下代码,打印出的是什么?
<template>
  <div class="page">
    <input ref="iref" type="text" v-if="visible">
    <button @click="updateTitle">Click</button>
  </div>
</template>

<script>
import { nextTick } from 'vue'
export default {
  data() {
      return {
        visible: false,
      }
  },
  methods: {
    updateTitle() {
      this.visible = true;
      nextTick(() => {
        console.log(this.$refs.iref);
      });
    }
  }
}
</script>

显而易见,正确答案是:
image.png

  1. 如果以下代码,打印出的是什么?
<template>
  <div class="page">
    <input ref="iref" type="text" v-if="visible">
    <button @click="updateTitle">Click</button>
  </div>
</template>

<script>
import { nextTick } from 'vue'
export default {
  data() {
      return {
        visible: false,
      }
  },
  methods: {
    updateTitle() {
      nextTick(() => {
        console.log(this.$refs.iref);
      });
      this.visible = true;
    }
  }
}
</script>

答案也显而易见,正确答案是:
image.png

  1. 上点难度了!如果以下代码,打印出的是什么?
<template>
  <div class="page">
    <p>{{ title }}</p>
    <button @click="updateTitle">Click</button>
  </div>
</template>


<script>
import { nextTick } from 'vue'
export default {
  data() {
      return {
        title: 'This is a title.',
      }
  },
  methods: {
    updateTitle() {
      nextTick(() => {
        console.log(this.title);
      });
      this.title = 'change the title.';
    }
  }
}
</script>

答案也许和你想得不一样:
image.png

  1. 地狱难度来了!如果代码如下,打印出的会是什么?
<template>
  <div class="page">
    <input ref="iref" type="text" v-if="visible">
    <p>{{ title }}</p>
    <button @click="updateTitle">Click</button>
  </div>
</template>


<script>
import { nextTick } from 'vue'
export default {
  data() {
      return {
        title: 'This is a title.',
        visible: false,
      }
  },
  methods: {
    updateTitle() {
      this.title = 'change the title.';
      nextTick(() => {
        console.log(this.title, this.$refs.iref);
      });
      this.visible = true;
    }
  }
}
</script>

答案如下,有没有出乎意料?
image.png

  1. Vue 是如何保证,等待 DOM 更新完成以后,再去执行 nextTick 里面的方法呢?

其实,归根结底,很少会有人思考上面的第五个问题。前面的四道题,都与第五个问题有千丝万缕的联系。

从 nextTick 原理出发

先贴一下 nextTick 源码,为了方便看,我把源码注释去掉,加上了自己的说明注释:

// 任务 “队列”
const callbacks: Array<Function> = []
// 是否可执行队列
let pending = false

// 执行任务 “队列”
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// 下面一堆是降级策略,优先使用 Promise
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  // 加入任务 “队列”
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

总之,就 nextTick 来说,是这样执行的:

  1. 调用 nextTick 时,将具体逻辑,加入任务 “队列”;
  2. 利用 Promise 或其它降级方式的异步特性,并不会立刻执行任务 “队列” 的代码;
  3. 等同步代码执行完成以后,再去依次执行任务 “队列” 的逻辑;

nextTick 与状态变更(data 数据变更)的关系

我们直接跳到上面的第四题,这题可以说是很容易答错了,再看下题目:

<template>
  <div class="page">
    <input ref="iref" type="text" v-if="visible">
    <p>{{ title }}</p>
    <button @click="updateTitle">Click</button>
  </div>
</template>


<script>
import { nextTick } from 'vue'
export default {
  data() {
      return {
        title: 'This is a title.',
        visible: false,
      }
  },
  methods: {
    updateTitle() {
      this.title = 'change the title.';
      nextTick(() => {
        console.log(this.title, this.$refs.iref);
      });
      this.visible = true;
    }
  }
}
</script>

我来概括一下执行的路径:

  1. 当执行到 this.title = 'change the title.'时,会先进入到titleset方法中(数据劫持)。set方法先比较了title新老值是否发生了变化,如果没有发生变化,不会进行任何后续操作,如果发生变化,会将**title**的值变更为新值,然后通知 组件的监听器(watcher)
  2. 注意!!!题目中的**template**依赖了两个状态值:**title****visible**,所以这里的监听器的执行的任务中,后续可能会有两个状态的变更但监听器后续的执行逻辑,不会马上执行,而是将后续的逻辑,也通过 nextTick 加到任务 “队列” 中。是不是没想到,监听器后续视图变更逻辑,也是通过 nextTick 异步执行的?
  3. 接着,回到题目中来,继续执行到我们自己写的 nextTick ,会将我们自己的逻辑也加入到任务 “队列” 中。这时候,我们的逻辑就排在监听器逻辑的后面了;
  4. 再次执行到this.visible = true,重复步骤 1;
  5. 注意!!!这里并不会再将一个监听器加到任务 “队列” 中去了,只要判断组件的监听器已经存在,即会 return 出来,不做后续操作了
  6. 同步逻辑走完,开始跑任务 “队列” 了,先进任务 “队列” 的是监听器逻辑,也就先开始执行监听器逻辑了。监听器发现自己监听的组件有两个视图依赖状态发生变更,后续将会更新视图;
  7. 任务 “队列” 继续执行我们自己通过 nextTick 加入的逻辑,这时候,由于状态和视图已经变更完毕,打印出了以下结果:

image.png

总结

  • 由于 data 值的变更(仅谈变更),是同步的(见上面步骤 1),所以,无论 data 的属性的变更在 nextTick 之前或之后,nextTick 都可以获取到 data 属性变更后的值;
  • 由于监听器的后续逻辑,也是通过 nextTick 进行异步执行的,与我们调用 nextTick 执行的逻辑是属于同一级别的任务,这样就可以保证,nextTick 可以拿到最终渲染的 dom(前提是监听器后续的逻辑,进任务 “队列” 的时机要比我们自己写的 nextTick 早);

建议再回过头来看看上面的五道问题,以及 Vue 官方文档对于 nextTick 的描述,是否清晰了许多呢

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值