-
Notifications
You must be signed in to change notification settings - Fork 99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心! #6
Comments
@Thinking80s 正文中有这个链接 |
@dcy0701 👍 |
我是不是可以这样理解:
|
感觉可以把 requestAnimationFrame 放进来一起讨论,很有趣啊,我去研究研究 |
第二点有问题,用户修改 reactive 数据不会立即触发 DOM 更新,而只是被所有订阅这个 reactive 数据的 watcher 监听到,然后稍后进行批处理,把这些监听到数据变动的 watcher 的真实数据写进 DOM。 也就是你改数据后,先不改 DOM ,过一会,等待当前的 microtask 完成之后再去批处理执行所有改 DOM 的操作。 Vue 提供这个操作并不是为了你说的去『观测 DOM 更新』,DOM modification 是实时的,DOM modification 是实时的,DOM modification 是实时的,重要的话说三遍。虽然我其实在文章里多次强调这句话。DOM modification 是实时的、同步的,是你在上一行代码修改 DOM, DOM tree 就立即、实时、直接、同步的修改完成了的。不需要任何回调去进行观测。 Vue 采用先不修改 DOM,累计一段时间后再去修改 DOM 的原因我在文章中其实详细说了:
另外,我在博客上写过一篇长文深入说明过浏览器渲染 和 eventloop 的关系,也说过 rAF的东西,可是目前我的博客 chuckliu.me 正在备案无法访问,等可以访问了之后我@你吧。 |
@Ma63d 唔,我说的观测指的是用户的行为(其实就是试图访问更新后的 DOM):
|
@jin5354 哦哦,这样理解是对的。我以为你说的是 vue 去观测。 |
@Ma63d 学长你可以先用 let's encrypt 开个 https 服务,备案只查 80 端口的。你的每篇 issue 我都读过了,真是受益匪浅 |
好的,非常感谢,我周末试试。 @jin5354 |
你好,请问input 绑定v-model会实现双向数据绑定实时刷新页面。 是不是input每输入一个字符,就会造成一轮事件循环吗? |
你好,请问下你所说的“也就是你改数据后,先不改 DOM ,过一会,等待当前的 microtask 完成之后再去批处理执行所有改 DOM 的操作。”,这句话里面的“等当前的microtask完成后再去批处理执行所有修改Dom的操作”是什么意思啊?难道修改Dom的操作不就在当前的microtask里面吗?还是说其实是想说等microtask执行完之后才UIrender? |
还有个问题,我把文中的jsFiddle里面的代码copy下来试了一下,发现还是有上下晃动的现象发生?链接: |
|
我提供的 fiddle 不会抖动,你用的版本为 vue@2.5.17, 这一版本内部机制我并不了解。你可以探索一下。 |
这篇文章太好了,每次看,都有新的体会。 |
太棒了,第一遍看只理解了一小部分,要看很多很多遍 |
分析得相当好,可见作者写得十分认真,幸运读到这篇文章解我惑,谢谢,好评。 |
楼主,既然等待当前的microtask完成之后才去批量执行所有改dom的操作,那么如果使用Promise或者MO 把 nextTick的回调放到微任务队列中,这样的话在回调中怎么能获取到更新后的DOM呢,求回复 |
赞同作者这句话:
官方的说明
我之前也是一直以为这句话中的“DOM更新”是真实DOM,现在才知道,官方说明中提到的“Vue更新 DOM ”,这个DOM都不是真实DOM。
|
您好,我是薛文毅,已收到您的邮件,一会查收,谢谢!!
|
您好,我是薛文毅,已收到您的邮件,一会查收,谢谢!!
|
前面的文章已经详细记述了Vue的核心执行过程。相当于已经搞定了主线剧情。后续的文章都会对其中没有介绍的细节进行展开。
现在我们就来讲讲其他支线任务:nextTick和microtask。
Vue的nextTick api的实现部分是Vue里比较好理解的一部分,与其他部分的代码也非常的解耦,因此这一块的相关源码解析文章很多。我本来也不准备单独写博客细说这部分,但是最近偶然在别人的文章中了解到:
每轮次的event loop中,每次执行一个task,并执行完microtask队列中的所有microtask之后,就会进行UI的渲染。但是作者似乎对于这个结论也不是很肯定。而我第一反应就是Vue的$nextTick既然用到了MutationObserver(MO的回调放进的是microtask的任务队列中的),那么是不是也是出于这个考虑呢?于是我想研究了一遍Vue的$nextTick,就可以了解是不是出于这个目的,也同时看能不能佐证UI Render真的是在microtask队列清空后执行的。
研究之后的结论:我之前对于$nextTick源码的理解完全是错的,以及每轮事件循环执行完所有的microtask,是会执行UI Render的。
task/macrotask和microtask的概念自从去年知乎上有人提出这个问题之后,task和microtask已经被很多同学了解了,我也是当时看到了microtask的内容,现在已经有非常多的中文介绍博客在介绍这部分的知识,最近这篇火遍掘金、SF和知乎的文章,最后也是考了microtask的概念。如果你没有看过task/microtask的内容的话,我还是推荐这篇英文博客,是绝大多数国内博客的内容来源。
先说nextTick的具体实现
先用120秒介绍MutationObserver: MO是HTML5中的新API,是个用来监视DOM变动的接口。他能监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等等。
调用过程很简单,但是有点不太寻常:你需要先给他绑回调:
var mo = new MutationObserver(callback)
通过给MO的构造函数传入一个回调,能得到一个MO实例,这个回调就会在MO实例监听到变动时触发。
这个时候你只是给MO实例绑定好了回调,他具体监听哪个DOM、监听节点删除还是监听属性修改,你都还没有设置。而调用他的observer方法就可以完成这一步:
一个需要先说的细节是,MutationObserver的回调是放在microtask中执行的。
ok了,现在这个domTarget上发生的文本内容修改就会被mo监听到,mo就会触发你在
new MutationObserver(callback)
中传入的callback。现在我们来看Vue.nextTick的源码:
上面这个函数执行过程后生成的那个函数才是nextTick。而这个函数的执行过程就是先初始化pending变量和cb变量,cb用来存放需要执行的回调,pending表示是否把清空回调的nextTickHandler函数加入到异步队列中。
然后就是创建了一个MO,这个MO监听了一个新创建的文本节点的文本内容变化,同时监听到变化时的回调就是nextTickHandler。nextTickHandler遍历cb数组,把需要执行的cb给拿出来一个个执行了。
而最后返回出去作为nextTick的那个函数就比较简单了:
也就是把传入的回调放入cb数组当中,然后执行
timerFunc(nextTickHandler, 0)
,其实是执行timerFunc()
,后面传入的两参数没用,在浏览器不支持MO的情况timerFunc才回退到setTimeout,那俩参数才有效果。timerFunc就是把那个被MO监听的文本节点改一下它的内容,这样我改了文本内容,MO就会在当前的所有同步代码完成之后执行回调,从而执行数据更新到DOM上之后的任务。我一开始在看这一段代码时忘记了MutationObserver的回调是在microtask里执行的。而且当时也还没有看过Vue的其他源码,当时的我大体看懂nextTick代码流程之后,形成了如下的理解,而且觉得似乎完美的解释了代码逻辑:
watcher监听到数据变化之后,会立马去修改dom,接着用户书写的代码里的nextTick被执行,而nextTick内部也是去修改DOM(textNode),当这个最后修改的textNode修改完成了,触发了MutationObserver的回调,那就意味着,前面的DOM修改也已经完成了,所以nextTick向用户保证的
DOM更新之后再执行用户的回调
就得以实现了。Damn,现在看了Batcher的代码和认真反思了以后,立马醒悟,上面的想法完完全全就是一坨狗屎,totally shit!
首先,一个普遍的常识是DOM Tree的修改是实时的,而修改的Render到DOM上才是异步的。根本不存在什么所谓的等待DOM修改完成,任何时候我在上一行代码里往DOM中添加了一个元素、修改了一个DOM的textContent,你在下一行代码里一定能立马就读取到新的DOM,我知道这个理。但是我还是搞不懂我怎么会产生用
nextTick来保证DOM修改的完成
这样的怪念头。可能那天屎吃得有点多了。其次,我们来看看使用nextTick的真正原因:
Vue在两个地方用到了上述nextTick:
nextTick(flushBatcherQueue)
,flushBatcherQueue
则负责执行完成所有的dom更新操作。Batcher的源码,我在上一篇文章当中已经详细的分析了,在这里我用一张图来说明它和nextTick的详细处理过程吧。
假设此时Vue实例的模板为:
<div id="a">{{a}}</div>
仔细跟踪了代码执行过程我们会发现,真正的去遍历watcher,批处理更新是在microtask中执行的,而且用户在修改数据后自己执行的
nextTick(cb)
也会在此时执行cb,他们都是在同一个microtask中执行。根本就不是我最开始想的那样,把回调放在以后的事件循环中去执行。同时,上面这个过程也深切的揭露出Vue nextTick的本质,我不是想要MO来帮我真正监听DOM更改,我只是想要一个异步API,用来在当前的同步代码执行完毕后,执行我想执行的异步回调。
之所以要这样,是因为用户的代码当中是可能多次修改数据的,而每次修改都会同步通知到所有订阅该数据的watcher,而立马执行将数据写到DOM上是肯定不行的,那就只是把watcher加入数组。等到当前task执行完毕,所有的同步代码已经完成,那么这一轮次的数据修改就已经结束了,这个时候我可以安安心心的去将对监听到依赖变动的watcher完成数据真正写入到DOM上的操作,这样即使你在之前的task里改了一个watcher的依赖100次,我最终只会计算一次value、改DOM一次。一方面省去了不必要的DOM修改,另一方面将DOM操作聚集,可以提升DOM Render效率。
那为什么一定要用MutationObserver呢?不,并没有一定要用MO,只要是microtask都可以。在最新版的Vue源码里,优先使用的就是
Promise.resolve().then(nextTickHandler)
来将异步回调放入到microtask中(MO在IOS9.3以上的WebView中有bug),没有原生Promise才用MO。这充分说明了microtask才是nextTick的本质,MO什么的只是个备胎,要是有比MO优先级更高、浏览器兼容性更好的microtask,那可能就分分钟把MO拿下了。任何一个microtask类型的异步回调都是可以完成现有代码里MO的作用的。甚至脑洞开得大一点,要是现在所有的浏览器都支持yield和generator,那我可以把watcher yield出去,然后在协程里把这个watcher存放队列里,然后协程执行一个会触发microtask回调的函数,再然后协程把函数控制权交回generator,这样也是可以的。不扯了,意图就是告诉你,Vue用的只是microtask而已。
那问题又来了,为什么一定要microtask?task可以吗?(macrotask和task是一回事哈,HTML5标准里甚至都没有macrotask这个词)。
哈,现在刚好有个例子,Vue一开始曾经改过nextTick的实现。我们来看看这两个jsFiddle:jsfiddle1和jsfiddle2。
两个fiddle的实现一模一样,就是让那个绝对定位的黄色元素起到一个fixed定位的效果:绑定scroll事件,每次滚动的时候,计算当前滚动的位置并更改到那个绝对定位元素的top属性上去。大家自己试试滚动几下,对比下效果,你就会发现第一个fiddle中的黄元素是稳定不动的,fixed很好。而后一个fiddle中就有问题了,黄色元素上下晃动,似乎跟不上我们scroll的节奏,总要慢一点,虽然最后停下滚动时位置是对的。
上述两个例子其实是在这个issue中找到的,第一个jsfiddle使用的版本是Vue 2.0.0-rc.6,这个版本的nextTick实现是采用了MO,而后因为IOS9.3的WebView里的MO有bug,于是尤雨溪更改了实现,换成了
window.postMessage
,也就是后一个fiddle所使用的Vue 2.0.0-rc.7。后来尤雨溪了解到window.postMessage
是将回调放入的macrotask 队列。这就是问题的根源了。HTML中的UI事件、网络事件、HTML Parsing等都是使用的task来完成,因此每次scroll事件触发后,在当前的task里只是完成了把watcher加入队列和把清空watcher的flushBatcherQueue作为异步回调传入nextTick。
如果nextTick使用的是microtask,那么在task执行完毕之后就会立即执行所有microtask,那么flushBatcherQueue(真正修改DOM)便得以在此时立即完成,而后,当前轮次的microtask全部清理完成时,执行UI rendering,把重排重绘等操作真正更新到DOM上(后文会细说)。(注意,页面的滚动效果并不需要重绘哈。重绘是当你修改了UI样式、DOM结构等等,页面将样式呈现出来,别晕了。)
如果nextTick使用的是task,那么会在当前的task和所有microtask执行完毕之后才在以后的某一次task执行过程中处理flushBatcherQueue,那个时候才真正执行各个指令的修改DOM操作,但那时为时已晚,错过了多次触发重绘、渲染UI的时机。而且浏览器内部为了更快的响应用户UI,内部可能是有多个task queue的:
而UI的task queue的优先级可能更高,因此对于尤雨溪采用的
window.postMessage
,甚至可能已经多次执行了UI的task,都没有执行window.postMessage
的task,也就导致了我们更新DOM操作的延迟。在重CPU计算、UI渲染任务情况下,这一延迟达到issue观测到的100毫秒到1秒的级别是完全课可能的。因此,使用task来实现nextTick是不可行的,而尤雨溪也撤回了这一次的修改,后续的nextTick实现中,依然是使用的Promise.then和MO。task microtask和每轮event loop之后的UI Render
我最近认真阅读了一下HTML5规范,还是来说一说task和microtask处理完成之后的UI渲染过程,讲一下每次task执行和所有microtask执行完毕后使如何完成UI Render的。
先上HTML标准原文:
比较典型的task有如下这些
此外,还包括setTimeout, setInterval, window.postMessage等等。
上述Reacting to DOM manipulation并不是说你执行DOM操作时就会把这个DOM操作的执行当成一个task。是那些异步的reacting会被当做task。
HTML5标准:task、microtask和UI render的具体执行过程如下:
解释一下:第一步,从多个task queue中的一个queue里,挑出一个最老的task。(因为有多个task queue的存在,使得浏览器可以完成我们前面说的,优先、高频率的执行某些task queue中的任务,比如UI的task queue)。
然后2到5步,执行这个task。
第六步, Perform a microtask checkpoint. ,这里会执行完microtask queue中的所有的microtask,如果microtask执行过程中又添加了microtask,那么仍然会执行新添加的microtask,当然,这个机制好像有限制,一轮microtask的执行总量似乎有限制(1000?),数量太多就执行一部分留下的以后再执行?这里我不太确定。
第七步,Update the rendering:
7.2到7.4,当前轮次的event loop中关联到的document对象会保持某些特定顺序,这些document对象都会执行需要执行UI render的,但是并不是所有关联到的document都需要更新UI,浏览器会判断这个document是否会从UI Render中获益,因为浏览器只需要保持60Hz的刷新率即可,而每轮event loop都是非常快的,所以没必要每个document都Render UI。
7.5和7.6 run the resize steps/run the scroll steps不是说去执行resize和scroll。每次我们scoll的时候视口或者dom就已经立即scroll了,并把document或者dom加入到 pending scroll event targets中,而run the scroll steps具体做的则是遍历这些target,在target上触发scroll事件。run the resize steps也是相似的,这个步骤是触发resize事件。
7.8和7.9 后续的media query, run CSS animations and send events等等也是相似的,都是触发事件,第10步和第11步则是执行我们熟悉的requestAnimationFrame回调和IntersectionObserver回调(第十步还是挺关键的,raf就是在这执行的!)。
7.12 渲染UI,关键就在这了。
第九步 继续执行event loop,又去执行task,microtasks和UI render。
更新:找到一张图,不过着重说明的是整个event loop,没有细说UI render。
The text was updated successfully, but these errors were encountered: