一分钟带你读懂Vue中nextTick源码和原理

113 阅读3分钟

简述

  1. vue实现响应式并不是数据发生变化之后DOM立即发生变化,而是按照一定的策略进行更新。
  2. Vue在修改数据之后,视图不会立即更新,而是等同一事件循环体制内的所有数据变化完成之后,再同意进行DOM更新。
  3. nextTick可以在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

Vue.nextTic的机制

  1. 为什么用 Vue.nextTick()
    JS的运行机制:JS执行是单线程的,它是基于事件循环的
    由于Vue DOM更新是异步执行,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保能够得到更新后的DOM,所以设置了Vue.nextTick()方法。
  2. 怎么用 Vue.nextTick()
    语法 : Vue.nextTick( [callback, context] )
    参数
    {Function} [callback]:回调函数,不传时提供promise调用
    {Object} [context]:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上。
    官方示例用法
            // 修改数据
            vm.msg = 'Hello'
            // DOM 还没有更新
            Vue.nextTick(function () {
            // DOM 更新了
            }
            // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
            Vue.nextTick()
            .then(function () {
            // DOM 更新了
            })
    
  3. 总结
    使用Vue.nextTick()是为了可以获取更新后的DOM 。
    触发时机:在同一事件循环中的数据变化后,DOM完成更新,立即执行Vue.nextTick()的回调

Vue.nextTick源码

import { noop } from 'shared/util'  //公共方法
import { handleError } from './error' // 报错
import { isIE, isIOS, isNative } from './env' // 当前代码的执行环境
//是否使用微任务
export let isUsingMicroTask = false
//回调数组
//首先定义一个 callback 数组来存储 nextTick 在下一个tick处理回调函数之前
// 所有的cb 都会在这个callback数组中
const callbacks = []
//pending 是一个标记位,代表一个等待的状态
let pending = false
//最后执行 flushCallbacks() 方法后,遍历callback数组,依次执行里边的每个函数
//[cb,cb,cb,cb,cb]
function flushCallbacks() {
    //重置等待的状态
    pending = false
    //防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前赋值备份,清空回调函数队列
    const copies = callbacks.slice(0)
    callbacks.length = 0
    //执行回调函数队列
    for (let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}
let timerFunc

/**
 判断采用哪种异步回调方式
 微任务的优先级高,先尝试微任务
 1.首先尝试使用Promise.then(微任务)
 2.尝试使用MuationObserver(微任务)回调
 3.尝试使用 setImmediate(宏任务)回调
 4.最后尝试使用setTimeout(宏任务)回调
 */

//优雅降级

//微任务 判断当前运行环境是否有 promise 并且判断这个 promise 是不是浏览器自带的
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
    let counter = 1
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
    }
    isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
   // Fallback to setImmediate.
   // Technically it leverages the (macro) task queue,
   // but it is still a better choice than setTimeout.
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
 // Fallback to setTimeout.
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}
//调用nextTick函数
export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    //[() => { cb() }, () => { cb() }, () => { cb() }, () => { cb() }, () => { cb() }]
    //将回调函数推入回调队列
    callbacks.push(() => {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    //如果等待状态未锁上 要锁上 调用异步函数,准备等同步函数执行完成后,就开始执行回调函数队列
    if (!pending) {
        pending = true
        timerFunc()
    }
    // $flow-disable-line
    //如果没有提供回调,并且支持Promise,返回一个Promise
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}