原文地址:https://qq282126990.github.io/2018/02/10/

nextTick
JS的运行机制:JS执行是单线程的,它是基于事件循环的对于事件循环的理解大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 主线程以外,还存在一个“任务队列”。只要异步任务有了运行结果,就在“任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件对应的那些异步任务, 结束等待,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

主线程的执行过程就是一个tick,而所有的异步结果都是通过“任务队列”来调度被调度。消息队列中存放的是一个个的任务(task)。规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

简单通过一段代码演示他们的执行顺序:

for (macroTask of macroTaskQueue) {// 1. Handle current MACRO-TASKhandleMacroTask();// 2. Handle all MICRO-TASKfor (microTask of microTaskQueue) {handleMicroTask(microTask);}
}

在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then。

回到Vue的nextTick,nextTick其实就是下一个tick,Vue内部实现了nextTick,并把它作为一个全局API暴露出来,它支持传入一个回调函数,保证回调函数的执行时机就是下一个tick。

官网文档介绍了Vue.nextTick 的使用场景:

使用:在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即只用这个方法,获取更新后的DOM

在vue里是数据驱动视图变化,由于JS执行是单线程的,在一个tick的过程中,它可能会多次修改数据,但Vue并不会傻到每修改一次数据就去驱动一次视图变化,它会把这个数据的修改全部push到一个队列里,然后内部调用一个nextTick去更新视图,所以数据到DOM视图的变化是需要在下一个tick才能完成。

接下来,我们来看一下Vue的nextTick的实现,在Vue.js 2.5+的版本,抽出来一个单独的next-tick.js 文件去实现它。

/* @flow */
/* globals MessageChannel */import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'const callbacks = []
let pending = falsefunction flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {macroTimerFunc = () => {setImmediate(flushCallbacks)}
} else if (typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) ||// PhantomJSMessageChannel.toString() === '[object MessageChannelConstructor]'
)) {const channel = new MessageChannel()const port = channel.port2channel.port1.onmessage = flushCallbacksmacroTimerFunc = () => {port.postMessage(1)}
} else {/* istanbul ignore next */macroTimerFunc = () => {setTimeout(flushCallbacks, 0)}
}// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()microTimerFunc = () => {p.then(flushCallbacks)// 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 {// fallback to macromicroTimerFunc = macroTimerFunc
}/*** Wrap a function so that if any code inside triggers state change,* the changes are queued using a (macro) task instead of a microtask.*/
export function withMacroTask (fn: Function): Function {return fn._withTask || (fn._withTask = function () {useMacroTask = trueconst res = fn.apply(null, arguments)useMacroTask = falsereturn res})
}export function nextTick (cb?: Function, ctx?: Object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = trueif (useMacroTask) {macroTimerFunc()} else {microTimerFunc()}}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}

在Vue 2.4之前的版本,nextTick几乎都是基于micro task 实现的,但由于micro task的执行优先级非常高,在某些场景下他甚至要比事件冒泡还要快,就会导致一些诡异的问题。如 issue #4521、#6690、#6566;但是如果全部都改成 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。所以最终nextTick采取的策略是默认走micro task,对于一些DOM交互事件,如v-on绑定的事件回调的处理,会强制走macro task。

这个强制是怎么做的呢,原来在 Vue.js 在绑定 DOM 事件的时候,默认会给回调的 handler 函数调用 withMacroTask 方法做一层包装,它保证整个回调函数执行过程中,遇到数据状态的改变,这些改变都会被推到 macro task 中。

对于macro task的执行,Vue优先检测是否支持元素 setImmediate,这是一个高版本IE和Edge才支持的特征,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就降级为 setTimeout。

nextTick对video&audio播放的影响

回到我们问题,移动端APP下和安卓&ios浏览器不能播放。先来看一下播放功能实现与方法。

我们的代码中会有一个播放器组件play.vue这个组件中会有一个HTML5的audio标签。由于可调用的地方有很多,比如歌曲组件、榜单组件、等等,因此我们用vuex对播放器的相关数据进行管理。我们把正在播放的列表playlist和当前播放索引currentIndex用state维护,当前播放的歌曲currentSong通过它们计算得到:

// state.js
const state = {playlist: [],currentIndex:0
}
// getters.js
export const currentSong = (state) => {return state.playlist[state.currentIndex] || {}
}

然后我们在 player.vue 组件里 watch currentSong 的变化去播放歌曲:

// player.vue
watch : {currentSong(newSong,oldSong) {if (!newSong.id || !newSong.url || newSong.id === oldSong.id) {return}this.$refs.audio.src = newSong.urlthis.$refs.audio.play()}
}

这样我们就可以在任何组件中提交对 playlist 和 currentIndex 的修改来达到播放不同歌曲的目的。那么这么写和 nextTick 有什么关系呢?

因为在 Vue.js 中,watcher 的回调函数执行默认是异步的,当我们提交对 playlist 或者 currenIndex 的修改,都会触发 currentSong 的变化,但是由于是异步,并不会立刻执行 watcher 的回调函数,而会在 nextTick 后执行。所以当我们点击歌曲列表中的歌曲后,在 click 的事件回调函数中会提交对 playlist 和 currentIndex 的修改, 经过一系列同步的逻辑执行,最终是在 nextTick 后才会执行 wathcer 的回调,也就是调用 audio 的 play。

所以本质上,就是用户点击到 audio 的 play 并不是在一个 tick 中完成,并且前面提到 Vue.js 中对 v-on 绑定事件执行的 nextTick 过程会强制使用 macro task。那么到底是不是由于 nextTick 影响了 audio 在 iOS 微信浏览器中的播放呢。

我们就来把化繁为简,写一个简单 demo 来验证这个问题,用的 Vue.js 版本是 2.5+ 的。

<template><div id="app"><audio ref="audio"></audio><button @click="changeUrl">click me</button></div>
</template><script>const musicList = ['http://dl.stream.qqmusic.qq.com/C4000041FwTv0Ai3Ku.m4a?vkey=EC7785A072C4BF592AF63FDD72CEDD8CD19AA3EA558A7AAD2D70A5CA8773D36F494F3F4D125AE3610BEE57152F2133E139F7886A00E78ABC&guid=7718257440&uin=282126990&fromtag=66','http://dl.stream.qqmusic.qq.com/C4000035fuK83mjCoC.m4a?vkey=0CF2F6B3A108032BE192BB97935DB72B095BB1FD4B816527008A63E8074314608F3E692E0522352949BA789C2FCCE3D00CD59B72FB50A8EC&guid=7718257440&uin=282126990&fromtag=66','http://dl.stream.qqmusic.qq.com/C400002XFAaU2gfRfV.m4a?vkey=4A79C0FF9883169C2C8B340E2EF6D35CFF471072E1A5D40F84E21D1BC16163B81F84A9C4EDF1960539EE255280C5BF4978E15240770E5C5D&guid=7718257440&uin=282126990&fromtag=66'];export default {data() {return {index: 0,url: ''};},methods: {changeUrl() {this.index = (this.index + 1) % musicList.length;this.url = musicList[this.index];}},watch: {url(newUrl) {this.$refs.audio.src = newUrl;this.$refs.audio.play();}}
};
</script>

这段代码的逻辑非常简单,我们会添加一个 watcher 监听 url 变化,当点击按钮的时候,会调用 changeUrl 方法,修改 url,然后 watcher 的回调函数执行,并调用 audio 的 play 方法。这段代码在 PC 浏览器是可以正常播放歌曲的,但是移动端APP下和安卓&ios浏览器不能播放,这就证实了我们之前的猜想——在用户点击事件的回调函数到 audio 的播放如果经历了 nextTick(v-on里面会自动执行nextTick) 在 iOS 微信浏览器下不能播放。

macro task 的锅?

经过上面我们可能会认为浏览器应该需要在同一个 tick 才能执行,果真需要这样吗?我们把上述代码做一个简单的修改:

changeUrl() {this.index = (this.index + 1) % musicList.lengththis.url = musicList[this.index]setTimeout(()=>{this.$refs.audio.src = this.urlthis.$refs.audio.play()}, 0)
}

我们现在不利用 Vue.js 的 nextTick 了,直接来模拟 nextTick 的过程,发现使用 setTimeout 0 是可以在 iOS 微信浏览器器、包括 iOS safari 下播放的,然而实际上我们只要在 1000ms 内的延时时间播放都是可以的,但是超过 1000ms,比如 setTimeout 1001 又不能播放了。

所以通过上述的实验,我们发现并不一定要在同一个 tick 执行播放,那么为啥 Vue.js 的 nextTick 是不可以的呢?回到 nextTick 的 macro task 的实现,它优先 setImmediate、然后 MessageChannel,最后才是 setTimeout 0。我们知道,除了高版本 IE 和 Edge,setImmediate 是没有原生支持的,除非一些工具对它进行了重新改写。而 MessageChannel 的浏览器支持程度还是非常高的,那么我把这段 demo 的异步过程改成用 MessageChannel 实现。

changeUrl() {this.index = (this.index + 1) % musicList.lengththis.url = musicList[this.index]let channel = new MessageChannel()let port = channel.port2channel.port1.onmessage = () => {this.$refs.audio.src = this.urlthis.$refs.audio.play()}port.postMessage(1)
}

这段代码在 PC 浏览器是可以播放的,而在移动端APP下和安卓&ios浏览器又不能播放,调试后发现 this.$refs.audio.play() 的逻辑也是可以执行到的,但是歌曲并不能播放,应该是浏览器对 audio 播放在使用 MessageChannel 做异步的一种限制。

前面提到实现 macro task 还有一种方法是利用 postMessage,它的浏览器支持程度也很好,我们来把 demo 改成利用它来实现。

postMessage(实际测试这种方式在iOS 13里已经不行了)

changeUrl() {this.index = (this.index + 1) % musicList.lengththis.url = musicList[this.index]addEventListener('message', () => {this.$refs.audio.src = this.urlthis.$refs.audio.play()}, false);postMessage(1, '*')
}

这段代码在 PC 浏览器和移动端APP下和安卓&ios浏览器都可以播放的,说明并不是 macro task 的锅,而是 MessageChannel 的锅

如何解决?

现在我们定位到问题的本质是因为 Vue.js 的 nextTick 中优先使用了 MessageChannel,它会影响移动端APP下和安卓&ios浏览器的播放

Vue.js 的版本降级

如果是真实运行在生产环境中的项目,毫无疑问这肯定是优先解决问题的首选,因为确实也是因为 Vue.js 的升级才造成这个 bug 的。在我们的实际项目中,我们都是锁死某个 Vue.js 的版本的,除非我们想使用某个 Vue.js 新版的 feature 或者是当前版本遇到了一个严重 bug 而新版已经修复的情况,我们才会考虑升级 Vue.js,并且每次升级都需要经过完整的功能测试。

为何把 Vue.js 降级到 2.4+ 就没问题呢,因为 Vue.js 2.5 之前的 nextTick 都是优先使用 microtask 的,那么 audio 播放的时机实际上还是在当前 tick,所以当然不会有问题。

其它方式

其实还有很多方式都能“修复”这个问题,比如我们不通过 watcher,改成每次点击通过 event bus 去通知;比如仍然使用同步 watcher,但 currentSong 不通过计算,直接用 state 保留;比如每次点击事件不通过 v-on 绑定,我们直接在 mounted 的钩子函数里利用原生的 addEventListener 去绑定 click 事件。

但是最终我选择了event bus去通知改变数据的变动,因为event bus本身是同步的所以它避免了异步操作而产生的使tick改变。我觉得event bus 是一种比较折中的做法

// song-list.vue
// 发送选择歌曲的信息总线程
Bus.$emit('selectSong', this.getCurrentSong);
// play.vue
// 监听选择歌曲事件
Bus.$on('selectSong', (data) => {if (!this.getCurrentSong.id) {return;}if (data.id) {// 初始化一些操作this._initSome();// 设置播放器播放地址this._getSinglePlayingUrl(null, data.mid);// 请求歌词this.getLyric(data.mid);}else {savePlayUrl(data);this.playUrl = getPlayUrl();// 设置歌曲播放setTimeout(() => {this.$refs.audio.play();}, 500);}
});

详细可以参考我的项目的代码

启发

笔者实际遇到的情况是如果改变了影响视图的变量,再播放音频,就移动端就播放不出来。所以改成了先播放音频(在timeout里做延迟播放),再去修改影响视图的变量。

本文参考的的文章:
Vue.js 升级踩坑小记
JavaScript 运行机制详解:再谈Event Loop

关于Vue中nextTick异步调用videoaudio的方法失效解决方案相关推荐

  1. Vue中$nextTick的理解

    Vue中$nextTick的理解 Vue中$nextTick方法将回调延迟到下次DOM更新循环之后执行,也就是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,能够获取更新后的 ...

  2. vue中nextTick的实际应用

    vue中nextTick的实际应用 简单来说就是:页面数据渲染完成之后再调用 在使用Vue框架的时候,有时候需要在Vue在页面数据渲染完成之后调用方法,不然获取不到准确的数据,特别是在获取列表的高度的 ...

  3. vue中组件之间调用方法——子组件调用父组件的方法 父组件调用子组件的方法

    vue中组件之间调用方法--子组件调用父组件的方法 & 父组件调用子组件的方法 1.vue中子组件调用父组件的方法 1.1.第一种方法是直接在子组件中通过this.$parent.event来 ...

  4. 在vue中methods互相调用的方法

    在vue中methods互相调用的方法 转载于:https://www.cnblogs.com/macT/p/10212878.html

  5. C#中的异步调用剖析

    我上次写创建线程的时候就想写一篇深入异步调用的笔记,但是由于当时对windows的进程与线程的概念不太清楚,没敢写,今天我仔细的分析并调试了一下C#中的异步调用的四种方法.把学习笔记分享出来. 假如要 ...

  6. Spring Boot 中启用异步调用

    在Java中一般开发程序都会同步调用的,程序中代码是一行一行执行下去的,每一行代码需要等待上一行代码执行完成才能开始执行. 在异步编程中,代码执行不是阻塞的,在方法调用中不需要等待所有代码执行完毕就可 ...

  7. Spring Boot 中的异步调用

    Spring Boot 中的异步调用 通常我们开发的程序都是同步调用的,即程序按照代码的顺序一行一行的逐步往下执行,每一行代码都必须等待上一行代码执行完毕才能开始执行.而异步编程则没有这个限制,代码的 ...

  8. 如何理解vue中 同步异步

    如何理解vue中 同步异步 同步异步 , 举个例子来说,一家餐厅吧来了5个客人,同步的意思就是说,来第一个点菜,点了个鱼,好, 厨师去捉鱼杀鱼,过了半小时鱼好了给第一位客人,开始下位一位客人,就这样一 ...

  9. 异步调用Web服务方法

    基于Ajax技术构建的门户是web 2.0这一代中最为成功的Web应用程序.而这块市场上iGoogle和Pageflakes这两大站点已经走在了时代的前列. 当你打开Pageflakes,将会看到如下 ...

最新文章

  1. c语言汽水瓶换汽水的编程题,c语言:2种方法编程及优化;喝汽水问题
  2. 洛谷 - P4721 【模板】分治 FFT(分治NTT)
  3. Hive安装及常用交互命令
  4. redis-cli 命令详解
  5. myeclipse中自带的tomcat在安装文件中的具体位置
  6. 使用多线程拷贝文件夹
  7. oracle19c监听服务启动失败,Oracle19c安装(有失败成功记录)
  8. Java核心(二)深入理解线程池ThreadPool
  9. MySQLdb查询有中文关键字查不到数据
  10. MVC Area领域处理以及T4MVC的使用
  11. Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
  12. Essay写作具体内容怎么了解?
  13. cmd怎么查看当前静态路由_计算机cmd命令之route,查看路由表,或配置一个更有效的路由...
  14. 鸿蒙曰蜉蝣不知所求,《庄子》释解(五七):浮游不知所求,猖狂不知所往
  15. 少有人知的 Python “重试机制“:tenacity
  16. c语言输入r1 r2垫片的面积,C语言上机实验答案Word版
  17. 如何计算IT投资回报(ROI)
  18. Gradle sync failed: No variants found for ‘:app‘. Check build files to ensure at least one varian
  19. 算法入门1:基本概念
  20. 一维信号卷积与图像卷积的区别

热门文章

  1. GJB 5000B二级-MPM测量与绩效管理
  2. 利用python编写气泡提醒小程序
  3. 北大教授健康讲座-笔记
  4. JavaScript数组基础练习题
  5. 中标麒麟系统安装teamviewer流程
  6. 乐鑫Esp32-S2学习之旅② ESP32-S2 控制 ws2812b 实现五彩斑斓的效果,代码开源!
  7. 使用可画怎么进行抠图,可画抠图步骤
  8. TouchDesigner 学习 Chop Noise
  9. windows 和 ubuntu 下 git + svn 客户端
  10. 计算机怎么连音乐,电脑如何连接蓝牙耳机听音乐_教你给电脑连接蓝牙耳机的方法-系统城...