视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001 知道1 知道21 知道41 知道61 知道81 知道101 知道121 知道141 知道161 知道181 知道201 知道221 知道241 知道261 知道281
问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501
浏览器事件循环与vue nextTicket的实现
2020-11-27 21:58:46 责编:小采
文档
  • 同步:就是在执行栈中(主线程)执行的代码
  • 异步:就是在异步队列(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,如果有,则对其进行处理。
  • (上述过程循环往复,直到两个队列都清空)

    注意:处理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的实现,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

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

    下载本文
    显示全文
    专题