通过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任务的操作包括:

           setTimeoutsetIntervalsetImmediateI/OUI renderingmacro 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)MutationObserverprocess.nextTick

      备注:事件冒泡甚至会在microtask中的任务执行之后,microtask优先级非常高

      Macrotasks、Microtasks执行机制:

      1.主线程执行完后会先到micro-task队列中读取可执行任务

      2.主线程执行micro-task任务

      3.主线程到macro-task任务队列中读取可执行任务

      4.主线程执行macro-task任务

      5....转到Step 1

二丶vue.nextTick实现
      在 Vue.js 里是数据驱动视图变化,由于 JS 执行是单线程的,在一个 tick (task)的过程中,它可能会多次修改数据,但 Vue.js 并不会傻到每修改一次数据就去驱动一次视图变化,它会把这些数据的修改全部 push 到一个队列里,然后内部调用 一次 nextTick 去更新视图,所以数据到 DOM 视图的变化是需要在下一个 tick 才能完成。这便是我们为什么需要vue.nextTick.
     在每个 task 运行完以后,UI 都会重渲染,那么很容易想到在 microtask 中就完成数据更新(数据更新并不代表这ui更新),当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。
vue.nextTick:
     定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。(修改数据之后Ui不会立即触发更新)

     理解: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");
 

 

posted on 2020-04-03 17:58  晓风零乱  阅读(550)  评论(0编辑  收藏  举报

导航