美文网首页
nextTick实现原理

nextTick实现原理

作者: 泪滴在琴上 | 来源:发表于2022-04-29 10:41 被阅读0次

    为什么会有nextTick这个东西的存在?

    因为 vue 采用的异步更新策略,当监听到数据发生变化的时候不会立即去更新DOM,
    而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更;
    这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作DOM的次数,
    如果不采用这种方法,假设数据改变100次就要去更新100次DOM,而频繁的DOM更新是很耗性能的;

    nexTick 的作用?

    nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行;
    使用场景:想要操作 基于最新数据的生成DOM 时,就将这个操作放在 nextTick 的回调中;

    nextTick 实现原理

    将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;
    nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeOut(fn,0)

    源码解读

    源码位置 core/util/next-tick
    源码并不复杂,三个函数,60几行代码,沉下心去看!
    Tips:为了便于理解我调整了源码中 nextTick、timerFunc、flushCallbacks 三个函数的书写顺序

    import { noop } from 'shared/util'
    import { handleError } from './error'
    import { isIE, isIOS, isNative } from './env'
    
    //  上面三行与核心代码关系不大,了解即可
    //  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
    //  handleError 错误处理函数
    //  isIE, isIOS, isNative 环境判断函数,
    //  isNative 判断是否原生支持,如果通过第三方实现支持也会返回 false
    
    
    export let isUsingMicroTask = false     // nextTick 最终是否以微任务执行
    
    const callbacks = []     // 存放调用 nextTick 时传入的回调函数
    let pending = false     // 标识当前是否有 nextTick 在执行,同一时间只能有一个执行
    
    
    // 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
    export function nextTick(cb?: Function, ctx?: Object) {
        let _resolve
        // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
        callbacks.push(() => {
            if (cb) {   // 对传入的回调进行 try catch 错误捕获
                try {
                    cb.call(ctx)
                } catch (e) {
                    handleError(e, ctx, 'nextTick')
                }
            } else if (_resolve) {
                _resolve(ctx)
            }
        })
        
        // 如果当前没有在 pending 的回调,就执行 timeFunc 函数选择当前环境优先支持的异步方法
        if (!pending) {
            pending = true
            timerFunc()
        }
        
        // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
        if (!cb && typeof Promise !== 'undefined') {
            return new Promise(resolve => {
                _resolve = resolve
            })
        }
    }
    
    
    // 判断当前环境优先支持的异步方法,优先选择微任务
    // 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
    // setTimeOut 最小延迟也要4ms,而 setImmediate 会在主线程执行完后立刻执行
    // setImmediate 在 IE10 和 node 中支持
    
    // 多次调用 nextTick 时 ,timerFunc 只会执行一次
    
    let timerFunc   
    // 判断当前环境是否支持 promise
    if (typeof Promise !== 'undefined' && isNative(Promise)) {  // 支持 promise
        const p = Promise.resolve()
        timerFunc = () => {
        // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
            p.then(flushCallbacks)
            if (isIOS) setTimeout(noop)
        }
        // 标记当前 nextTick 使用的微任务
        isUsingMicroTask = true
        
        
        // 如果不支持 promise,就判断是否支持 MutationObserver
        // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
        let counter = 1
        // new 一个 MutationObserver 类
        const observer = new MutationObserver(flushCallbacks) 
        // 创建一个文本节点
        const textNode = document.createTextNode(String(counter))   
        // 监听这个文本节点,当数据发生变化就执行 flushCallbacks 
        observer.observe(textNode, { characterData: true })
        timerFunc = () => {
            counter = (counter + 1) % 2
            textNode.data = String(counter)  // 数据更新
        }
        isUsingMicroTask = true    // 标记当前 nextTick 使用的微任务
        
        
        // 判断当前环境是否原生支持 setImmediate
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
        timerFunc = () => { setImmediate(flushCallbacks)  }
    } else {
    
        // 以上三种都不支持就选择 setTimeout
        timerFunc = () => { setTimeout(flushCallbacks, 0) }
    }
    
    
    // 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
    // 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
    function flushCallbacks() {
        pending = false
        const copies = callbacks.slice(0)    // 拷贝一份
        callbacks.length = 0    // 清空 callbacks
        for (let i = 0; i < copies.length; i++) {    // 遍历执行传入的回调
            copies[i]()
        }
    }
    
    // 为什么要拷贝一份 callbacks
    
    // callbacks.slice(0) 将 callbacks 拷贝出来一份,
    // 是因为考虑到 nextTick 回调中可能还会调用 nextTick 的情况,
    // 如果 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
    // nextTick 回调中的 nextTick 应该放在下一轮执行,
    // 如果不将 callbacks 复制一份就可能一直循环
    
    

    作者:大古Zio
    链接:https://juejin.cn/post/7087866362785169416
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

          本文标题:nextTick实现原理

          本文链接:https://www.haomeiwen.com/subject/keceertx.html