今天做了一个需求, 场景是这样的:
在页面拉取一个接口, 这个接口返回一些数据, 这些数据是这个页面的一个浮层组件要依赖的, 然后我在接口一返回数据就展示了这个浮层组件, 展示的同时, 上报一些数据给后台 (这些数据就是父组件从接口拿的), 这个时候, 神奇的事情发生了, 虽然我拿到数据了, 但是浮层展现的时候, 这些数据还未更新到组件上去.
父组件:
- <template>
- .....
- <pop ref="pop" :name="name"/>
- </template>
- <script>
- export default {
- .....
- created() {
- ....
- // 请求数据, 并从接口获取数据
- Data.get({
- url: xxxx,
- success: (data) => {
- // 问题出现在这里, 我们赋值以后直接调用 show 方法, 去展现, show 方法调用的同时上报数据, 而上报的数据这个时候还未更新到子组件
- this.name = data.name
- this.$refs.pop.show()
- }
- })
- }
- }
- </script>
子组件
- <template>
- <div v-show="isShow">
- ......
- </div>
- </template>
- <script>
- export default {
- .....
- props: ['name'],
- methods: {
- show() {
- this.isShow = true
- // 上报
- Report('xxx', {name: this.name})
- }
- }
- }
- </script>
问题分析:
原因 vue 官网上有解析
可能你还没有注意到, Vue 异步执行 DOM 更新. 只要观察到数据变化, Vue 将开启一个队列, 并缓冲在同一事件循环中发生的所有数据改变. 如果同一个 watcher 被多次触发, 只会被推入到队列中一次. 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要. 然后, 在下一个的事件循环 "tick" 中, Vue 刷新队列并执行实际 (已去重的) 工作. Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel, 如果执行环境不支持, 会采用 setTimeout(fn, 0) 代替.
这句话就是说, 当我们在父组件设置 this.name=name 的时候, vue 并不会直接更新到子组件中 (dom 的更新也一样未立即执行), 而是把这些更新操作全部放入到一个队列当中, 同个组件的所有这些赋值操作, 都作为一个 watcher 的更新操作放入这个队列当中, 然后等到事件循环结束的时候, 一次性从这个队列当中获取所有的 wathcer 执行更新操作. 在我们这个例子当中, 就是我们在调用 show 的时候, 实际上, 我们的 this.name=name 并未真正执行, 而是被放入队列中. vue 的这种做法是基于优化而做的, 毋庸置疑, 不然我们如果有 n 多个赋值 vue 就执行 n 多个 dom 更新, 那效率将会非常的低效和不可取的.
下文中的更新操作指对 data 的值进行更新的操作, 在 vue 中, 都会被放入队列异步执行.
解决方案:
1, 使用 nextTick 来延迟执行 show 方法 (笼统得说, 执行所有需要在数据真正更新后的操作
通过上面的分析我们知道, 我们的所有的对 vue 实例的更新操作, 都会先被放入一个队列当中, 延迟异步执行, 这些异步操作, 要么是 microtask, 要么是 macrotask(是 microtask 还是 macroktask 取决于环境, nextTick 的源码中有所体现), 根据事件循环机制, 先入队列的先执行, 所以如果我们在 nextTick 当中执行操作就会变成这样.
2, 使用 setTimeout 来延迟执行 show 方法, 原理同上
所以我们的解决方法可以是:
- this.name = data.name
- setTimeout(() => {
- this.$refs.pop.show()
- })
或者
- this.name = data.name
- this.$nextTick(() => {
- this.$refs.pop.show()
- })
前端全栈学习交流圈: 866109386, 面向 1-3 经验年前端开发人员, 帮助突破技术瓶颈, 提升思维能力, 群内有大量 PDF 可供自取, 更有干货实战项目视频进群免费领取.
nextTick 的实现原理
其实 nextTick 的实现原理是挺简单的, 简单点说, 就是实现异步, 通过不同的执行环境, 用不同的方式来实现, 保证 nextTick 里面的回调函数能够异步执行. 为什么要这么做呢? 因为 vue 对 dom 的更新也是异步的呀.
下面贴出源码:
- /**
- * Defer a task to execute it asynchronously.
- */
- export const nextTick = (function () {
- const callbacks = []
- let pending = false
- let timerFunc
- function nextTickHandler () {
- pending = false
- const copies = callbacks.slice(0)
- callbacks.length = 0
- for (let i = 0; i <copies.length; i++) {
- copies[i]()
- }
- }
- // the nextTick behavior leverages the microtask queue, which can be accessed
- // via either native Promise.then or MutationObserver.
- // MutationObserver has wider support, however it is seriously bugged in
- // UIwebView in iOS>= 9.3.3 when triggered in touch event handlers. It
- // completely stops working after triggering a few times... so, if native
- // Promise is available, we will use it:
- /* istanbul ignore if */
- if (typeof Promise !== 'undefined' && isNative(Promise)) {
- var p = Promise.resolve()
- var logError = err => { console.error(err) }
- timerFunc = () => {
- p.then(nextTickHandler).catch(logError)
- // in problematic UIWebViews, Promise.then doesn't completely break, but
- // it can get stuck in a weird state where callbacks are pushed into the
- // microtask queue but the queue isn't being flushed, until the browser
- // needs to do some other work, e.g. handle a timer. Therefore we can
- // "force" the microtask queue to be flushed by adding an empty timer.
- if (isIOS) setTimeout(noop)
- }
- } else if (!isIE && typeof MutationObserver !== 'undefined' && (
- isNative(MutationObserver) ||
- // PhantomJS and iOS 7.x
- MutationObserver.toString() === '[object MutationObserverConstructor]'
- )) {
- // use MutationObserver where native Promise is not available,
- // e.g. PhantomJS, iOS7, Android 4.4
- var counter = 1
- var observer = new MutationObserver(nextTickHandler)
- var textNode = document.createTextNode(String(counter))
- observer.observe(textNode, {
- characterData: true
- })
- timerFunc = () => {
- counter = (counter + 1) % 2
- textNode.data = String(counter)
- }
- } else {
- // fallback to setTimeout
- /* istanbul ignore next */
- timerFunc = () => {
- setTimeout(nextTickHandler, 0)
- }
- }
- return function queueNextTick (cb?: Function, ctx?: Object) {
- let _resolve
- callbacks.push(() => {
- if (cb) {
- try {
- cb.call(ctx)
- } catch (e) {
- handleError(e, ctx, 'nextTick')
- }
- } else if (_resolve) {
- _resolve(ctx)
- }
- })
- if (!pending) {
- pending = true
- timerFunc()
- }
- if (!cb && typeof Promise !== 'undefined') {
- return new Promise((resolve, reject) => {
- _resolve = resolve
- })
- }
- }
- })()
前端全栈学习交流圈: 866109386, 面向 1-3 经验年前端开发人员, 帮助突破技术瓶颈, 提升思维能力, 群内有大量 PDF 可供自取, 更有干货实战项目视频进群免费领取.
首先我们看到这个是利用了闭包的特性, 返回 queueNextTick, 所以我们实际调用的 nextTick 其实就是调用 queueNextTick, 一调用这个方法, 就会把 nextTick 的回调放入队列 callbacks 当中, 等到合适的时机, 会将 callbacks 中的所有回调取出来执行, 以达到延迟执行的目的. 为啥要用闭包呢, 我觉得有两个原因:
1, 共享变量, 比如 callbacks,pending 和 timerFunc.
2, 避免反复判断, 即是避免反复判断 timerFunc 是利用 Promise 还是利用 MutationObserver 或是 setTimeout 来实现异步, 这是函数柯里化的一种运用.
这里有两个最主要的方法需要解释下:
1, nextTickHandler
这个函数, 就是把队列中的回调, 全部取出来执行, 类似于 microtask 的任务队列. 我们通过调用 Vue.$nextTick 就会把回调全部放入这个队列当中, 等到要执行的时候, 调用 nextTickHandler 全部取出来执行.
2, timerFunc
这个变量, 它的作用就是通过 Promise/Mutationobserver/Settimeout 把 nextTickHandler 放入到真正的任务队列当中, 等到事件循环结束, 就从任务队列当中取出 nextTickHandler 来执行, nextTickHandler 一执行, callbacks 里面的所有回调就会被取出来执行来, 这样就达到来延迟执行 nextTick 传的回调的效果.
通过这个简单的源码分析, 我们可以得出两个结论
1,nextTick 会根据不同的执行环境, 异步任务可能为 microtask 或者 macrotask, 而不是固定不变的. 所以, 如果你想让 nextTick 里面的异步任务统统看成是 microtask 的话, 你会遇到坑的.
2,nextTick 的并不能保证一定能获取得到更新后的 dom, 这取决于你是先进行数据赋值还是先调用 nextTick. 比如:
- new Vue({
- el: '#app',
- data() {
- return {
- id: 2
- }
- },
- created() {
- },
- mounted() {
- this.$nextTick(() => {
- console.log(document.getElementById('id').textContent) // 这里打印出来的是 2, 因为先调用了 nextTick
- })
- this.id = 3
- }
- })
来源: http://www.jianshu.com/p/2067c45db1c2