通过microtask和macrotask理解Vue.nextTick()的实现
一丶JavaScript 运行机制详解:再谈Event Loop
1.JavaScript 运行机制详解:
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
注:只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制
任务队列:
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。
2.Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
3. 任务队列中的microtask和macrotask
任务队列(task queue)
中的异步任务分为两种:微任务(microtask)
和宏任务(macrotask)
。
宏任务(macrotask): 在浏览器端,其可以理解为该任务执行完后,在下一个macrotask执行开始前,浏览器可以进行页面渲染。触发macrotask任务的操作包括:
setTimeout
、setInterval
、setImmediate
、I/O
、UI rendering
macro task事件:
备注:宏任务一般是当前事件循环的最后一个任务,ui的渲染在这里最后执行,浏览器的ui绘制会插在每个macrotask之间,这就是为什么angularjs中settimeout会触发视图更新的 原因。阻塞macrotask会导致ui数据不能更新
这里注意:script(整体代码)
即一开始在主执行栈中的同步代码本质上也属于macrotask,属于第一个执行的task
微任务(microtask)可以理解为页面渲染前立即执行的任务,值得注意的是,UI Rendering是在micro-task之后执行。给micro-task队列添加过多回调阻塞macro-task队列的任务 执行是小事,重点是这有可能会阻塞UI Render,导致页面不能更新。浏览器也会基于性能方面的考虑,对micro-task中的任务个数进行限制。触发microtask任务的操作包括:
Promises(浏览器实现的原生Promise)
、MutationObserver
、process.nextTick
备注:事件冒泡甚至会在microtask中的任务执行之后,microtask优先级非常高
Macrotasks、Microtasks执行机制:
1.主线程执行完后会先到micro-task队列中读取可执行任务
2.主线程执行micro-task任务
3.主线程到macro-task任务队列中读取可执行任务
4.主线程执行macro-task任务
5....转到Step 1
二丶vue.nextTick实现
vue.nextTick
.理解:nextTick(),是将回调函数延迟在下一次dom更新数据后调用,简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数.
使用场景:
1.1、Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
1.2、当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中;通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它
1.3、在使用某个第三方插件时 ,希望在vue生成的某些dom动态发生变化时重新应用该插件,也会用到该方法,这时候就需要在 $nextTick 的回调函数中执行重新应用插件的方法。
注:vue实现响应式并不是数据发生变化后dom立即变化,而是按照一定的策略(如上)来进行dom更新。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <div id="example"> {{message}} </div> </div> <script> var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message'; // 更改数据 此时数据发生了变化,但是dom并没有更新,ui没有渲染 console.log(vm.message); //new message console.log(vm.$el.textContent); // 123 Vue.nextTick(function() { //nextTick相当于主线程正在进行macroTask,此时dom,ui已经更新 console.log(vm.$el.textContent) //DOM更新完成,ui已经渲染完毕 }) //备注:vue实现响应式并不是数据发生变化后dom立即变化,而是将每次数据的变化放入一个队列里,当这个task任务完成后 // 才会更新DOM,渲染数据。 </script> </body> </html>
改变数据DOM不更新的解决方案
2.1数组
由于 JavaScript 的限制,Vue 不能检测以下变动的数组:
当你利用索引直接设置一个项时,例如:this.items[indexOfItem] = newValue
当你修改数组的长度时,例如:this.items.length = newLength
解决方案:
- Vue.set(this.items, indexOfItem, newValue)
- this.items.splice(indexOfItem, 1, newValue)
- this.$set(this.items, indexOfItem, newValue) (this.$set 实例方法是全局方法 Vue.set 的一个别名)
- this.items.splice(newLength)
2.2对象
由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除:
var vm = new Vue({ data: { userProfile: { name: 'Anika' } } }) //你可以添加一个新的 age 属性到嵌套的 userProfile 对象: Vue.set(vm.userProfile, 'age', 27) //你还可以使用 vm.$set 实例方法,它只是全局 Vue.set 的别名: this.$set(this.userProfile, 'age', 27) //有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign() 或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。 this.userProfile = Object.assign({}, this.userProfile, { age: 27, favoriteColor: 'Vue Green' })
通俗来说:
//删除对象属性的方法(前面对象名称,后面具体属性名):
this.$delete(this.userProfile, "name");