「小记」JS运行机制之Event Loop

153 阅读1分钟

一、vm.$nextTick

1. 官方用法

  • 将回调延迟到下次DOM更新循环后执行。
  • 在修改数据之后立即使用它,然后等待DOM更新。
  • 它跟全局方法Vue.nextTick一样,不同的是回调的this自动绑定到调用它的实例上。

2. 示例

methods: {
  example: function () {
      this.message = 'changed' // 修改数据
      // DOM 还没有更新
      this.$nextTick( function() {
        // DOM 现在更新了
        this.doSomethingElse()// `this` 绑定到当前实例
      })
    }
}

3. nextTick源码

  • isNative():判断所传参数是否在当前环境原生支持;
  • 四个判断对当前环境进行降级处理,尝试使用原生的Promise.thenMutationObserversetImmediate,都不支持的情况下最后使用setTimeout。降级处理的目的是将flushCallbacks函数放入任务队列(微任务、宏任务),等待下一次事件循环时来执行。
  • 参考链接:Vue中$nextTick源码解析

二、异步更新队列

1. Vue 在更新 DOM 时是异步执行的

  • 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
  • 如果同一个 watcher 被多次触发,只会被推入到队列中一次。(这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的)然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
  • Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

2. 示例

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

Vue.component('example', {
    template: '<span>{{ message }}</span>',
    data() {
        return {
            message: '未更新'
        }
    },
    methods: {
        updateMessage () {
            this.message = '已更新'
            console.log(this.$el.textContent) // => '未更新'
            this.$nextTick(function () {
                console.log(this.$el.textContent) // => '已更新'
            })
        }
    }
})

三、JS运行机制

1. Event Loop

  • JS分为同步任务异步任务。同步任务进入主线程;异步任务进入事件表(EventTable)
  • 同步任务在主线程按顺序执行;异步任务在事件表中注册函数,满足条件后进入任务队列
  • 主线程的同步任务执行完毕后,查看任务队列中是否有可执行的异步任务。
  • 有,异步任务进入主线程执行;
  • 没有,开始下一个循环。

2. 任务队列

  • 任务队列是一个事件的队列,除IO设备的事件以外,包括用户产生的事件(鼠标点击、页面滚动等);
  • 任务队列是一个先进先出的数据结构,排在前面的事件优先被主线程读取;
  • 任务队列除了放置异步任务的事件,还放置定时事件,即指定某些代码在多少时间后执行。

3. 定时器:setTimeout

  • setTimeout接受两个参数:回调函数、推迟执行的毫秒数;
  • 如果第二个参数为0,表示当前代码执行完以后立即执行指定的回调函数;
  • setTimeout(fn,0),指定某个任务在主线程最早可得的空闲时间执行,尽可能早的执行(在任务队列的尾部添加一个事件,等到同步任务和任务队列现有的事件处理完,才会得到执行);
  • 若前面的代码耗时很长,没办法保证,回调函数一定在setTimeout()指定的时间执行。

4. 宏任务、微任务

  • 宏任务(macro-task):包括整段script代码、setImmediatesetTimeoutsetInterval、I/O等;

    #浏览器Node
    setImmediate
    setTimeout
    setInterval
    I/O
    requestAnimationFrame
  • 微任务(micro-task):Promise系列:.then.catch.finallyMutaionObserverprocess.nextTick

    #浏览器Node
    Promise.then catch finally
    MutationObserver
    process.nextTick

5. Async await

  • 函数带上async,函数返回值必定是Promise对象,会自动用Promise.resolve()包装起来成为一个Promise对象(因此await后面的代码相当于放到了Promise.then的回调函数中去);
  • await通过阻塞后面的代码来实现,但是await表达式执行顺序是从右往左,即先执行await右侧的代码,遇到await后再阻塞后面的代码;
  • await等的是右侧表达式的结果(右侧是一个函数,则结果是这个函数的返回值,右侧是一个值则结果就为此值);

四、举个🌰

1. 例一

1.1 打印顺序?

console.log(1)
setTimeout(() => {
  console.log(2)
}, 2000)

setTimeout(() => {
  console.log(3)
  Promise.resolve().then(() => {
    console.log(4)
  })
  setTimeout(() => {
    console.log(5)
  }, 3000)
}, 1000)

new Promise((resolve, reject) => {
  console.log(6)
  resolve()
}).then(() => {
  console.log(7)
})
console.log(8)

1.2 思路&结果

console.log(1)	//同步代码,立即执行,打印1
setTimeout(() => {	//宏任务,等待2000后执行
    console.log(2)	//进入任务队列【3,2】,
}, 2000)

setTimeout(() => {//宏任务,等待1000后执行
    console.log(3)	//,进入任务队列【3】
    Promise.resolve().then(() => {//微任务,进入当前宏任务下的微任务队列
        console.log(4)
    })
    setTimeout(() => {//宏任务,等待3000后执行
        console.log(5)	//进入任务队列【3,2,5】,
    }, 3000)
}, 1000)

new Promise((resolve, reject) => {
    console.log(6)//同步代码,立即执行,打印6
    resolve()
}).then(() => {
    console.log(7)//微任务,进入当前宏任务下的微任务队列
})
console.log(8)//同步代码,立即执行,打印8

①主线程执行同步代码,也就是开始一个宏任务的执行:即打印1、6、8;然后依次执行当次循环宏任务产生的所有微任务:即打印7

②主线程读取任务列表中的下一个宏任务:即打印3;并依次执行当前宏任务下的微任务:即打印4

③主线程读取任务列表中的下一个宏任务:即打印2;并依次执行当前宏任务下的微任务:没有微任务;

④主线程读取任务列表中的下一个宏任务:即打印5;并依次执行当前宏任务下的微任务:没有微任务;

结果:1、6、8、7、3、4、2、5

2. 例二

2.1 打印顺序?

console.log('1');
setTimeout(function() {
    console.log('2');
    Promise.resolve().then(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
}, 100)

Promise.resolve().then(function() {
    console.log('6');
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    Promise.resolve().then(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
async function async1() {
    console.log('13')
    await async2()
    console.log('14')
}
async function async2() {
    console.log('15')
}
async1()
console.log('16')

2.2 思路&结果

console.log('1');//同步任务,立即执行,打印1
setTimeout(function() {//宏任务①,进入任务队列【②,①】
    console.log('2');	//等待100后执行
    Promise.resolve().then(function() {
        console.log('3'); //微任务,入①的微任务队列【3】
    })
    new Promise(function(resolve) {
        console.log('4');//等待100后执行
        resolve();
    }).then(function() {
        console.log('5');//微任务,入①的微任务队列【3,5】
    })
}, 100)

Promise.resolve().then(function() {
    console.log('6');	//微任务,入微任务列表【6】
})

new Promise(function(resolve) {
    console.log('7');	//同步代码
    resolve();
}).then(function() {
    console.log('8')	//微任务,入微任务列表【6,8】
})

setTimeout(function() {	//宏任务②,进入任务队列【②】
    console.log('9');	//尽早的执行
    Promise.resolve().then(function() {
        console.log('10');	//微任务,入②的微任务列表【10】
    })
    new Promise(function(resolve) {
        console.log('11');	//尽早的执行
        resolve();
    }).then(function() {
        console.log('12');	//微任务,入②的微任务列表【10,12】
    })
})

async function async1() {
    console.log('13')	//同步代码,立即执行,打印13
    await async2()		//从右往左执行,先运行async2():打印15,后阻塞代码
    console.log('14')	//放到Promise的then回调函数中执行,即微任务,入微任务列表【6,8,14】
}
async function async2() {
    console.log('15')
}
async1()
console.log('16')	//同步任务,立即执行,打印16

①主线程执行同步代码,也就是开始一个宏任务的执行:即打印1、7、13、15、16;然后依次执行当次循环宏任务产生的所有微任务:即打印6、8、14

②主线程读取任务列表中的下一个宏任务:即打印9、11;并依次执行当前宏任务下的微任务:即打印10、12

③主线程读取任务列表中的下一个宏任务:即打印2、4;并依次执行当前宏任务下的微任务:即打印3、5

结果:1、7、13、15、16、6、8、14、9、11、10、12、2、4、3、5


五、参考链接