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