浏览器事件循环与vue nextTicket的实现


Posted in Javascript onApril 16, 2019
  • 同步:就是在执行栈中(主线程)执行的代码
  • 异步:就是在异步队列(macroTask、microTask)中的代码

简单理解区别就是:异步是需要延迟执行的代码

线程和进程

  • 进程:进程是应用程序的执行实例,每一个进程都是由私有的虚拟地址空间、代码、数据和其它系统资源所组成;进程在运行过程中能够申请创建和使用系统资源(如独立的内存区域等),这些资源也会随着进程的终止而被销毁
  • 线程:线程则是进程内的一个独立执行单元,在不同的线程之间是可以共享进程资源的,是进程内可以调度的实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

简单讲,一个进程可由多个线程构成,线程是进程的组成部分。

js是单线程的,但浏览器并不是,它是一般是多进程的。

以chrome为例: 一个页签就是一个独立的进程。而javascript的执行是其中的一个线程,里面还包含了很多其他线程,如:

  • GUI渲染线程
  • http请求线程
  • 定时器触发线程
  • 事件触发线程
  • 图片等资源的加载线程。

事件循环

ok,常识性内容回顾完,我们开始切入正题。

microTask 和 macroTask

常见的macroTask有:setTimeout、setInterval、setImmediate、i/o操作、ui渲染、MessageChannel、postMessage

常见的microTask有:process.nextTick、Promise、Object.observe(已废弃)、MutationObserver(html5新特性)

用线程的理论理解队列:

macroTask由事件触发线程维护
microTask通常由js引擎自己维护

一个完整的事件循环(Event loop)过程解析

  • 初始状态:调用栈(主线程)、microTask队列、macroTask队列,macroTask里只有一个待执行的script脚本(如:入口文件)
  • 将这个script推入调用栈,同步执行代码。在这过程中,会调用一些接口或者触发一些事件,可产生新的marcoTask与microTask。它们分别会被推入各自的任务队列。同时该script脚本会被从macroTask中移除,在调用栈执行的过程就称之为一个tick。
  • 调用栈代码执行完成后,需要处理的是microTask中的任务。将里面的任务依次推入调用栈执行。
  • 待microTask 所有 的任务都执行完成后,再去macroTask中获取优先级最高的任务推入调用栈。
  • 执行渲染操作,更新界面
  • 查看是否有web worker,如果有,则对其进行处理。

(上述过程循环往复,直到两个队列都清空)

浏览器事件循环与vue nextTicket的实现

注意:处理microTask中的任务时,是执行完所有的任务。而处理macroTask的任务时是一个一个执行。

渲染时机

经过上面的学习我们把异步拿到的数据放在macroTask中还是microTask中呢?

比如先放在macroTask中:

setTimeout(myTask, 0)

那么按照Event loop,myTask会被推入macroTask中,本次调用栈内容执行完,会执行microTask中的内容,然后进行render。而此次render是不包含myTask中的内容的。需要等到 下一次事件循环 (将myTask推入执行栈后)才能执行。

如果放在microTask中:

Promise.resolve().then(myTask)

那么按照Event loop,myTask会被推入microTask中,本次调用栈内容执行完,会执行microTask中的myTask内容,然后进行render,也就是在 本次的事件循环 中就可以进行渲染。

总结:我们在异步任务中修改dom是尽量在microTask完成。

Vue next-tick实现

Vue2.5以后,采用单独的next-tick.js来维护它。

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

// 所有的callback缓存在数组中
const callbacks = []
// 状态
let pending = false

// 调用数组中所有的callback,并清空数组
function flushCallbacks () {
 // 重置标志位
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0
 // 调用每一个callback
 for (let i = 0; i < copies.length; i++) {
  copies[i]()
 }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).

// 微任务function
let microTimerFunc
// 宏任务fuction
let macroTimerFunc
// 是否使用宏任务标志位
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */

// 优先检查是否支持setImmediate,这是一个高版本 IE 和 Edge 才支持的特性(和setTimeout差不多,但优先级最高)
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 macroTimerFunc = () => {
  setImmediate(flushCallbacks)
 }
// 检查MessageChannel兼容性(优先级次高)
} else if (typeof MessageChannel !== 'undefined' && (
 isNative(MessageChannel) ||
 // PhantomJS
 MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
 const channel = new MessageChannel()
 const port = channel.port2
 channel.port1.onmessage = flushCallbacks
 macroTimerFunc = () => {
  port.postMessage(1)
 }
// 兼容性最好(优先级最低)
} else {
 /* istanbul ignore next */
 macroTimerFunc = () => {
  setTimeout(flushCallbacks, 0)
 }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */

// 微任务用promise来处理
if (typeof Promise !== 'undefined' && isNative(Promise)) {
 const p = Promise.resolve()
 microTimerFunc = () => {
  p.then(flushCallbacks)
  // in problematic UIWebViews, Promise.then doesn't completely break, but
  // it can get stuck in a weird state where callbacks are pushed into the
  // microtask queue but the queue isn't being flushed, until the browser
  // needs to do some other work, e.g. handle a timer. Therefore we can
  // "force" the microtask queue to be flushed by adding an empty timer.
  if (isIOS) setTimeout(noop)
 }
// promise不支持直接用宏任务
} else {
 // fallback to macro
 microTimerFunc = macroTimerFunc
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
// 强制走宏任务,比如dom交互事件,v-on (这种情况就需要强制走macroTask)
export function withMacroTask (fn: Function): Function {
 return fn._withTask || (fn._withTask = function () {
  useMacroTask = true
  const res = fn.apply(null, arguments)
  useMacroTask = false
  return res
 })
}

export function nextTick (cb?: Function, ctx?: Object) {
 let _resolve
 // 缓存传入的callback
 callbacks.push(() => {
  if (cb) {
   try {
    cb.call(ctx)
   } catch (e) {
    handleError(e, ctx, 'nextTick')
   }
  } else if (_resolve) {
   _resolve(ctx)
  }
 })
 // 如果pending为false,则开始执行
 if (!pending) {
  // 变更标志位
  pending = true
  if (useMacroTask) {
   macroTimerFunc()
  } else {
   microTimerFunc()
  }
 }
 // $flow-disable-line
 // 当为传入callback,提供一个promise化的调用
 if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
   _resolve = resolve
  })
 }
}

这段代码主要定义了Vue.nextTick的实现。 核心逻辑:

  • 定义当前环境支持的microTimerFunc和macroTimerFunc(调用时会执行flushCallbacks方法)
  • 调用nextTick时,缓存传入的callback
  • pending设置为false,执行microTimerFunc或macroTimerFunc(也就是执行flushCallbacks方法)
  • pending设置为true,执行完数组中的callbakc,清空数组

vue在this.xxx=xxx进行节点更新时,实际上是触发了Watcher的queueWatcher

export function queueWatcher (watcher: Watcher) {
 const id = watcher.id
 if (has[id] == null) {
  has[id] = true
  if (!flushing) {
   queue.push(watcher)
  } else {
   // if already flushing, splice the watcher based on its id
   // if already past its id, it will be run next immediately.
   let i = queue.length - 1
   while (i > index && queue[i].id > watcher.id) {
    i--
   }
   queue.splice(i + 1, 0, watcher)
  }
  // queue the flush
  if (!waiting) {
   waiting = true
   nextTick(flushSchedulerQueue)
  }
 }
}

queueWatcher做了在一个tick内的多个更新收集。

具体逻辑我们在这就不专门讨论了(有兴趣的可以去查阅vue的观察者模式),逻辑上就是调用了nextTick方法

所以vue的数据更新是一个异步的过程。

那么我们在vue逻辑中,当想获取刚刚渲染的dom节点时我们应该这么写

你肯定会说应该这么写

getData(res).then(()=>{
 this.xxx = res.data
 this.$nextTick(() => {
  // 这里我们可以获取变化后的 DOM
 })
})

没错,确实应该这么写。

那么问题来了~

前面不是说UI Render是在microTask都执行完之后才进行么。

而通过对vue的$nextTick分析,它实际是用promise包装的,属于microTask。

在getData.then中,执行了this.xxx= res.data,它实际也是通过wather调用$nextTick

随后,又执行了一个$nextTick

按理说目前还处在同一个事件循环,而且还没有进行UI Render,怎么在$nextTick就能拿到刚渲染的dom呢?

我之前被这个问题困扰了很久,最终通过写test用例发现,原来UI Render这块我理解错了

UI render理解

之前一直以为新的dom节点必须等UI Render之后渲染才能获取到,然而并不是这样的。

在主线程及microTask执行过程中,每一次dom或css更新,浏览器都会进行计算,而计算的结果并不会被立刻渲染,而是在当所有的microTask队列中任务都执行完毕后,统一进行渲染(这也是浏览器为了提高渲染性能和体验做的优化)所以,这个时候通过js访问更新后的dom节点或者css是可以访问到的,因为浏览器已经完成计算,仅仅是它们还没被渲染而已。

总结

以上所述是小编给大家介绍的浏览器事件循环与vue nextTicket的实现,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
javascript new fun的执行过程
Aug 05 Javascript
jQuery 遍历- 关于closest() 的方法介绍以及与parents()的方法区别分析
Apr 26 Javascript
JS在IE下缺少标识符的错误
Jul 23 Javascript
js实现上一页下一页的效果【附代码】
Mar 10 Javascript
用AngularJS来实现监察表单按钮的禁用效果
Nov 02 Javascript
用最简单的方法判断JavaScript中this的指向(推荐)
Sep 04 Javascript
mescroll.js上拉加载下拉刷新组件使用详解
Nov 13 Javascript
原生JS实现DOM加载完成马上执行JS代码的方法
Sep 07 Javascript
vue中选项卡点击切换且能滑动切换功能的实现代码
Nov 25 Javascript
JavaScript创建对象的四种常用模式实例分析
Jan 11 Javascript
微信小程序定义和调用全局变量globalData的实现
Nov 01 Javascript
JS前端广告拦截实现原理解析
Feb 17 Javascript
理理Vue细节(推荐)
Apr 16 #Javascript
ES6知识点整理之Proxy的应用实例详解
Apr 16 #Javascript
js实现删除li标签一行内容
Apr 16 #Javascript
js实现弹出框的拖拽效果实例代码详解
Apr 16 #Javascript
重学 JS:为啥 await 不能用在 forEach 中详解
Apr 15 #Javascript
你不知道的Vue技巧之--开发一个可以通过方法调用的组件(推荐)
Apr 15 #Javascript
详解JavaScript中的强制类型转换
Apr 15 #Javascript
You might like
php 生成WML页面方法详解
2009/08/09 PHP
php中有关合并某一字段键值相同的数组合并的改进
2015/03/10 PHP
PHP面向对象程序设计之多态性的应用示例
2018/12/19 PHP
Domino中运用jQuery读取视图内容的方法
2009/10/21 Javascript
javascript sudoku 数独智力游戏生成代码
2010/03/27 Javascript
基于mootools插件实现遮罩层新手引导
2012/05/24 Javascript
JQuery 常用方法和事件详细介绍
2013/04/18 Javascript
Javascript让DEDECMS告别手写Tag
2014/09/01 Javascript
javascript实现炫酷的拖动分页
2015/05/11 Javascript
jQuery+PHP实现可编辑表格字段内容并实时保存
2015/10/09 Javascript
jQuery实现下拉加载功能实例代码
2016/04/01 Javascript
jquery删除table当前行的实例代码
2016/10/07 Javascript
js 颜色选择插件
2017/01/23 Javascript
JS实现图片放大缩小的方法
2017/02/15 Javascript
jQuery Ajax前后端使用JSON进行交互示例
2017/03/17 Javascript
JS扩展String.prototype.format字符串拼接的功能
2018/03/09 Javascript
JS简单判断是否在微信浏览器打开的方法示例
2019/01/08 Javascript
vscode下vue项目中eslint的使用方法
2019/01/13 Javascript
微信实现自动跳转到用其他浏览器打开指定APP下载
2019/02/15 Javascript
[04:20]DOTA2-DPC中国联赛 正赛 VG vs LBZS 选手采访 1月19日
2021/03/11 DOTA
利用Python找出序列中出现最多的元素示例代码
2017/12/08 Python
Python中装饰器高级用法详解
2017/12/25 Python
Python上下文管理器全实例详解
2019/11/12 Python
Pycharm插件(Grep Console)自定义规则输出颜色日志的方法
2020/05/27 Python
Python实现图片查找轮廓、多边形拟合、最小外接矩形代码
2020/07/14 Python
切尔西足球俱乐部官方网上商店:Chelsea FC
2019/06/17 全球购物
毕业生怎样写好自荐信
2013/11/11 职场文书
网站设计师的岗位职责
2013/11/21 职场文书
结婚典礼证婚词
2014/01/11 职场文书
企业安全生产承诺书
2014/05/22 职场文书
车辆委托书范本
2014/10/05 职场文书
篮球友谊赛通讯稿
2014/10/10 职场文书
2016年会开场白台词
2015/06/01 职场文书
通讯稿范文
2015/07/22 职场文书
用Java实现简单计算器功能
2021/07/21 Java/Android
基于Python实现nc批量转tif格式
2022/08/14 Python