vue中的任务队列和异步更新策略(任务队列,微任务,宏任务)

目录
  • 事件循环
  • 任务队列
  • 如何理解微任务和宏任务?
  • 深究Vue异步更新策略原理

事件循环

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。

为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。

Event Loop 包含两类:

  • 一类是基于 Browsing Context
  • 一种是基于 Worker

二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

任务队列

vue 数据驱动视图是数据改变,视图异步等待所有数据变化完成,统一进行视图更新。

既然是异步就有顺序和优先级, 异步任务队列是那种顺序执行 ?

tip: 微任务优先级高于宏任务 MDN 介绍

任务队列主要分为两种:

1、microtasks(微任务):

  • Promise :ES6的异步处理方案
  • process.nextTick(vue.nextTick) :下轮tick更新机制,
  • Mutation Observer API: DOM改变 监听 API

2、macrotasks(宏任务也称任务):

  • setTimeout() : 延时器
  • setInterval(): 计时器
  • setImmediate:node.js 回调函数延迟执行,process.nextTicl() 方法十分类似

process.nextTick()中的回调函数执行的优先级要高于setImmediate().这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一个轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将该数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数

  • I/O :系统IO(input/output)
  • UI render :页面渲染
  • requestAnimationFrame():异步动画渲染 ,浏览器调用指定的函数以在下次重绘之前更新动画

如何理解微任务和宏任务?

创造代码的人也是人,他们的灵感多数来自于生活。我们这里打个比方(朴灵老师也这样比喻),javascript处理异步就像去餐馆吃饭,服务员会先为顾客下单,下完单把顾客点的单子给后厨让其准备,然后就去服务下一位顾客,,而不是一直等待在出餐口。

javascript将顾客下单的行为进行了细分。无外乎两种酒水类和非酒水类。对应着我们javascript中的macroTask和microTask。

但是在不同场景下的步骤是不一样的,就像西餐和中餐。西餐划分的非常详细:头盘->汤->副菜->主菜->蔬菜类菜肴->甜品->咖啡,中餐就相对简单许多:凉菜->热菜->汤。

任务队列,下面看几个示例, 输出内容便是执行顺序:

任务队列,下面看几个示例, 输出内容便是执行顺序:

setTimeout(()=>{
    console.log('1')
    Promise.resolve().then(function() {
        console.log('2')
    })
}, 0)
setTimeout(()=>{
    console.log('3')
    Promise.resolve().then(function() {
        console.log('4')
    })
}, 0)
setTimeout(function() {console.log('6')}, 0)
requestAnimationFrame(function(){
    console.log('5')
})
setTimeout(function() {console.log('7')}, 0)
new Promise(function executor(resolve) {
    console.log('1')
    resolve()
    console.log('2')
}).then(function() {
    console.log('4')
})
console.log('3')
console.log('1');
setTimeout(() => {
    console.log('5');
    process.nextTick(() => console.log('6'));
}, 0);
process.nextTick(() => {
    console.log('3');
    process.nextTick(() => console.log('4'));
});
console.log('2');

这样就可以理解Vue的异步更新策略运行机制

  created() {
    this.textDemo()
  },
  methods: {
    textDemo() {
      console.log(1)
      setTimeout(() => { // macroTask
        console.log(4)
        setTimeout(() => { // macroTask
          console.log(8)
        }, 0)
        this.$nextTick(() => { // microTask
          console.log(5)
        })
        Promise.resolve().then(function() { // microTask
          console.log(7)
        })
        this.$nextTick(() => { // microTask
          console.log(6)
        })
      }, 0)
      this.$nextTick(() => { // microTask
        console.log(3)
      })
      console.log(2)
    },
}

到此我们已经知道代码是如何运行的了, 如果你想要更深入理解, 请继续向下阅读。

深究Vue异步更新策略原理

我们先看一个示例:

<template>
  <div>
    <div ref="test">{{test}}</div>
    <button @click="handleClick">tet</button>
  </div>
</template>
<script>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText);// 结果输出 begin
        }
    }
}
</script>

通过上面示例可以看出 Vue是异步执行DOM更新, 更新会缓冲到队列中, 在nextTick 集中刷新队列并执行实际 (已去重的) 工作

当然新版 this.$nextTick 有一些变化 。从Vue 2.5+开始,抽出来了一个单独的文件next-tick.js来执行它。

其大概的意思就是:在Vue 2.4之前的版本中,nextTick 几乎都是基于 microTask 实现的(具体可以看文章nextTick一节),但是由于 microTask 的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成 macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终 nextTick 采取的策略是默认走 microTask,对于一些DOM的交互事件,如 v-on绑定的事件回调处理函数的处理,会强制走 macroTask。

具体做法就是,在Vue执行绑定的DOM事件时,默认会给回调的handler函数调用withMacroTask方法做一层包装,它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM更新)的任务都会被推到macroTask。

源码:

function add$1 (event, handler, once$$1, capture, passive) {
    handler = withMacroTask(handler);
    if (once$$1) { handler = createOnceHandler(handler, event, capture); }
    target$1.addEventListener(
        event,
        handler,
        supportsPassive
        ? { capture: capture, passive: passive }
        : capture
    );
}
/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a Task instead of a MicroTask.
 */
function withMacroTask (fn) {
    return fn._withTask || (fn._withTask = function () {
        useMacroTask = true;
        var res = fn.apply(null, arguments);
        useMacroTask = false;
        return res
    })
}

最后,写一段DEMO验证一下 :

<template>
    <div>
        <button @click="handleClick">change</button>
    </div>
</template>
<script>
export default {
   created() {
      this.handleClick() //  得出结果 : 2 3 1 4
  },
   methods: {
       handleClick() {
         setTimeout(() => { // macroTask
           console.log(4)
         }, 0)
    
         this.$nextTick(() => { // microTask
           console.log(2)
         })
    
         Promise.resolve().then(function() { // microTask
           console.log(1)
         })
    
         this.$nextTick(() => { // microTask
           console.log(3)
         })
       }
   }
}
</script>

在Vue 2.5+中,这段代码的输出顺序是1 - 2 - 3 - 4, 而 Vue 2.4 和 不通过DOM 输出 2 - 3 - 1 - 4。nextTick执行顺序的差异正好说明了上面的改变。

tips: 所以这里需要留意遇到DOM操作, 同步执行受阻或者节点内容未及时更新可以使用 this.$nextTick 等待一下在执行下面的操作。

<div id="example">
    <audio ref="audio"
           :src="url"></audio>
    <span ref="url"></span>
    <button @click="changeUrl">click me</button>
</div>
<script>
const musicList = [
    'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112003137.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
    'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112002493.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
    'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112004168.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3'
];
var vm = new Vue({
    el: '#example',
    data: {
        index: 0,
        url: ''
    },
    methods: {
        changeUrl() {
            this.index = (this.index + 1) % musicList.length
            this.url = musicList[this.index];
            this.$refs.audio.play();
        }
    }
});
</script>

毫无悬念,这样肯定是会报错的:

Uncaught (in promise) DOMException: The element has no supported sources.

原因就在于audio.play()是同步的,而这个时候DOM更新是异步的,src属性还没有被更新,结果播放的时候src属性为空,就报错了。

解决办法就是在play的操作加上this.$nextTick()。

this.$nextTick(function() {
    this.$refs.audio.play();
});

异步更新有什么好处?

<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
<script>
export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}
</script>

上面的例子非常直观,可以这么理解当数据更新同步操作DOM会出现频繁渲染视图造成页面卡顿,极端的消耗资源。所以异步更新大大提升了性能, 并且数据更新很高效体验并没有降低。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 详解Vue的异步更新实现原理

    最近面试总是会被问到这么一个问题:在使用vue的时候,将for循环中声明的变量i从1增加到100,然后将i展示到页面上,页面上的i是从1跳到100,还是会怎样?答案当然是只会显示100,并不会有跳转的过程. 怎么可以让页面上有从1到100显示的过程呢,就是用setTimeout或者Promise.then等方法去模拟. 讲道理,如果不在vue里,单独运行这段程序的话,输出一定是从1到100,但是为什么在vue中就不一样了呢? for(let i=1; i<=100; i++){ console.

  • vue源码之批量异步更新策略的深入解析

    vue异步更新源码中会有涉及事件循环.宏任务.微任务的概念,所以先了解一下这几个概念. 一.事件循环.宏任务.微任务 1.事件循环Event Loop:浏览器为了协调事件处理.脚本执行.网络请求和渲染等任务而定制的工作机制. 2.宏任务Task: 代表一个个离散的.独立的工作单位.浏览器完成一个宏任务,在下一个宏任务开始执行之前,会对页面重新渲染.主要包括创建文档对象.解析HTML.执行主线JS代码以及各种事件如页面加载.输入.网络事件和定时器等. 3.微任务:微任务是更小的任务,是在当前宏任务

  • vue的状态更新方式(异步更新解决)

    目录 状态更新(异步更新解决) 解决方案 异步更新及nexttick 为什么需要异步更新 nextTick 原理 状态更新(异步更新解决) 在vue中状态更新是异步的,这一点和react中的setstate类似. 解决方案 非组件解决方案: <div id="example">{{message}}</div> var vm = new Vue({   el: '#example',   data: {     message: '123'   } }) vm.

  • vue中的任务队列和异步更新策略(任务队列,微任务,宏任务)

    目录 事件循环 任务队列 如何理解微任务和宏任务? 深究Vue异步更新策略原理 事件循环 JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事. 为了协调事件.用户交互.脚本.UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生. Event Loop 包含两类: 一类是基于 Browsing Context 一种是基于 Worker 二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的

  • 解决vue中虚拟dom,无法实时更新的问题

    碰到的问题:使用jq获取元素节点的个数时一直为0 解决方法:使用vue的nextTick()函数即可解决 原理:nextTick可以在下一次更新dom之后进行回调,我的问题在于,在页面加载完成时无法获取虚拟dom,而使用回调函数后就可以获取到正确的dom数量,所以只需要在nextTick函数中执行jq函数就可以正确获取了. self.$nextTick(function () { // DOM 更新了 $("#myCarousel").carousel(0); }) 以上这篇解决vue

  • 详解从Vue.js源码看异步更新DOM策略及nextTick

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出,

  • c#中Winform实现多线程异步更新UI(进度及状态信息)

    引言 在进行Winform程序开发需要进行大量的数据的读写操作的时候,往往会需要一定的时间,然在这个时间段里面,界面ui得不到更新,导致在用户看来界面处于假死的状态,造成了不好的用户体验.所以在大量数据操作的应用上,需要使用多线程来处理这种情况.在c#中使用多线程很方便只需要使用System.Threading.Thread的一个实例的Start方法就行了,但是如何实现多线程之间的交互就不是那么简单.本文实现了用子线程去处理数据,并实时更新主线程的ui状态了.下面就开始一步步的去实现异步线程更新

  • Vue中的同步和异步调用顺序详解

    目录 Vue的同步和异步调用顺序 例如 Vue两个异步方法顺序执行 第一个异步方法 第二个异步方法 Vue的同步和异步调用顺序 Vue中的方法调用顺序是依次进行的,方法体内部也是依次执行的,但是,两个方法体的执行顺序并不能严格控制. 以下方法中都带有promise函数或异步调用.     initUserData() {       this.getPsCountryList() // 1 获取国家列表stateOptions,方法内同步       this.getTimeZone() //

  • Vue异步更新机制及$nextTick原理的深入讲解

    目录 前言 Vue的异步更新 DOM更新是异步的 DOM更新还是批量的 事件循环 执行过程 源码深入 异步更新队列 nextTick $nextTick 总结 一般更新DOM是同步的 既然更新DOM是个同步的过程,那为什么Vue却需要借用$nextTick来处理呢? 为什么优先使用微任务? 总结 前言 相信很多人会好奇Vue内部的更新机制,或者平时工作中遇到的一些奇怪的问题需要使用$nextTick来解决,今天我们就来聊一聊Vue中的异步更新机制以及$nextTick原理 Vue的异步更新 可能

  • 简单理解Vue中的nextTick方法

    Vue中的nextTick涉及到Vue中DOM的异步更新,感觉很有意思,特意了解了一下.其中关于nextTick的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介绍下nextTick. 一.示例 先来一个示例了解下关于Vue中的DOM更新以及nextTick的作用. 模板 <div class="app"> <div ref="msgDiv">{{msg}}</div> <div v-if="msg1&q

  • 详解Vue 数据更新了但页面没有更新的 7 种情况汇总及延伸总结

    如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事. 1. Vue 无法检测实例被创建时不存在于 data 中的 property 原因:由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的. 场景: var vm = new Vue({ data:{}, // 页面不会变化 template: '<div>{{message}

随机推荐