nextTick原理学习

一、为什么用nextTick

(1)js执行原理Eventloop

首先js是单线程的,所谓单线程,就是同一时间只能处理一件事情。JS中的任务分为同步任务和异步任务,其中异步任务分为宏任务和微任务。

所有同步任务都在主线程上执行,形成一个执行栈。而异步任务则会形成任务队列,宏任务进入宏队列,微任务进入微队列。

执行顺序:

①执行同步代码;

②等待所有的同步任务执行完毕,执行栈清空

③开始读取任务队列的任务,先从微队列取队首任务放入执行栈中执行

④继续取直到微队列任务完毕,如果执行过程中又产生了微任务则加入队列末尾,这个任务也会在这个周期执行

⑤微队列和执行栈都为空,则取宏队列队首任务放入栈中执行

⑥执行完毕,执行栈为空,重复③-⑤

(2)Vue数据驱动视图更新

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

nextTick 的本质是为了利用 JavaScript 的异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

二、nextTick作用

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

比如,动态生成文本框,实现自动聚焦功能;引入swiper库,需要等挂载的DOM生成后再生成swiper对象。

三、Vue响应式原理+nextTick实现异步更新策略简易版源码分析

(1)实现发布者

class Dep{
    static target=null   //暂时存放需要被加入到dep中的watcher
    constructor(){
        this.subs=[];
    }
    addSubs(watcher){
        this.subs.push(watcher)
    }
    notify(){
        for(let i=0;i<this.subs.length;i++){
            this.subs[i].update();
        }
    }
 }

(2)实现Observer实现对data中的属性进行数据劫持,在get中进行依赖手机,在set中通知更新

class Observer{
     constructor(data){
        if(typeof data=='object'){
            this.walk(data);
        }
     }
     walk(obj){
         const keys=Object.keys(obj);
         for (let i = 0; i < keys.length; i++) {
             this.defineReactive(obj, keys[i])
         }
     }
     defineReactive(obj,key){
         if(typeof obj[key]=='object'){
             this.walk(obj[key]);
         }
         const dep=new Dep();
         let val=obj[key];
         Object.defineProperty(obj, key, {
             enumerable: true,
             configurable: true,
             //get代理将Dep.target即Watcher对象添加到依赖集合中
             get: function reactiveGetter () {
               if (Dep.target) {
                 dep.addSubs(Dep.target);
               }
               return val;
             },
             set: function reactiveSetter (newVal) {
                  val=newVal;
                  dep.notify()
             } 
           })
     }
 }

(3)实现watcher,当触发更新执行回调时,不立即执行更新回调,而是将更新回调放到UpdateQueue异步更新队列中,并且队列中只存放一个相同的watcher(has对象进行判断),当本次事件循环结束(通过this.vm.waiting控制),调用this.nextTick,传入清空异步更新队列的函数。

let uid=0
 class Watcher{
     constructor(vm,key,cb){
        this.vm=vm;
        this.key=key;
        this.uid=uid++;
        this.cb=cb;
        //调用get,添加依赖
        Dep.target=this;
        this.value=vm.$data[key];
        Dep.target=null;
     }
     update(){
         if(this.value!==this.vm.$data[this.key]){
             this.value=this.vm.$data[this.key];
             if(!this.vm.waiting){//控制变量,控制每次事件循环期间只添加一次flushUpdateQueue到callbacks
                this.vm.$nextTick(this.vm.flushUpdateQueue); 
                this.vm.waiting=true;
             }
             //不是立即执行run方法,而是放入updateQueue队列中
             if(!has[this.uid]){
                 has[this.uid]=true;
                 updateQueue.push(this);
             }
         }
     }
     run(){
         this.cb(this.value);
     }
 }

(4)实现Vue类,内含全局api nextTick,nextTick接收一个回调函数,通过promise.then或setTimeOut延迟回调的执行,这里将回调函数放入到callbacks数组中,在微任务中清空callbacks函数即调用该回调函数,这个回调函数又执行清空异步更新队列的任务

const updateQueue=[];//异步更新队列
  let has={};//控制变更队列中不保存重复的Watcher
  const callbacks=[];
  let pending=false;
 class Vue{
    constructor(options){
        this.waiting=false
        this.$el=options.el;
        this._data=options.data;
        this.$data=this._data;
        this.$nextTick=this.nextTick;
        new Observer(this._data);
    }
    //简易版nextTick
    nextTick(cb){
         callbacks.push(cb);
         if(!pending){//控制变量,控制每次事件循环期间只执行一次flushCallbacks
             pending=true;
             setTimeout(()=>{
                 //会在同步代码(上一次宏任务)执行完成后执行
                 this.flushCallbacks();
             })
         }
     }
    //清空UpdateQueue队列,更新视图
    flushUpdateQueue(vm){
        while(updateQueue.length!=0){
           updateQueue.shift().run();
        }
        has={};
        vm.waiting=false;
    }
    //清空callbacks
    flushCallbacks(){
       while(callbacks.length!=0){
         callbacks.shift()(this);//传入当前vm实例,使得flushUpdateQueue能获取到
      }
      pending=false;
    }
 }

 

四、nextTick源码

将传入的回调函数包装成异步任务,nextTick 提供了四种异步方法 ,因为微任务优先于宏任务执行,所以优先级为

Promise.then > MutationObserver > setImmediate > setTimeOut(fn,0)

源码:

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.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
// nextTick 回调中的 nextTick 应该放在下一轮执行,
// 如果不将 callbacks 复制一份就可能一直循环 

参考:https://blog.csdn.net/web220507/article/details/125141403

posted @ 2022-09-21 00:34  陈雪佩  阅读(672)  评论(0编辑  收藏  举报