Vue源码学习(4)- 异步更新
admin
2024-01-28 14:23:15

当通过obj.key = ‘new val’ 更新值时,会触发setter的拦截,从而检测新值和旧值是否相等,如果相等什么也不做,如果不想等,则更新值,然后由dep通知watcher进行更新。所以,异步更新的入口就是setter中最后调用的dep.notify()方法。

目的

  • 深入理解Vue的异步更新机制
  • nextTick的原理

dep.notify

/src/core/observer/dep.js

/*** 通知 dep 中的所有 watcher,执行 watcher.update() 方法*/
notify () {// stabilize the subscriber list firstconst subs = this.subs.slice()// 遍历 dep 中存储的 watcher,执行 watcher.update()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
}

watcher.update

/src/core/observer/watcher.js

/*** 根据 watcher 配置项,决定接下来怎么走,一般是 queueWatcher*/
update () {/* istanbul ignore else */if (this.lazy) {// 懒执行时走这里,比如 computed// 将 dirty 置为 true,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果this.dirty = true} else if (this.sync) {// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run // 方法进行更新// 这个属性在官方文档中没有出现this.run()} else {// 更新时一般都这里,将 watcher 放入 watcher 队列queueWatcher(this)}
}

queueWatcher

/src/core/observer/scheduler.js

/*** 将watcher 放入watcher队列
*/
export function queueWatcher (watcher: Watcher){const id =watcher.id//如果watcher已经存在,则跳过,不会重复入队if(has[id] == null){//缓存watcher.id ,用于判断watcher 是否已经入队has[id] = trueif(!flushing){//当前没有处于刷新队列状态,watcher直接入队queue.push(watcher)}else{//已经在刷新队列//从队列末尾开始倒序遍历,根据当前watcher.id 找到大于它的watcher.id的位置,然后将自己插入到该位置之后的下一个位置//即将当前的watcher放入到已排列的队列中,且队列仍是有序的let i = queue.length-1while(i>index && queue[i].id>watcher.id){i--}queue.splice(i+1,0,watcher)}if(!waiting){waiting = trueif(process.env.NODE_ENV !== 'production' && !config.async){//直接刷新调度队列//一般不会走这儿,Vue默认是异步执行,如果要改为同步执行,性能会大打折扣flushSchedulerQueue()return}/*** 熟悉的 nextTick =. vm.$nextTick、Vue.nextTick* 1.将回调函数(flushScheduleQueue) 放入callbacks数组* 2.通过pending控制向浏览器任务队列中添加flushCallbacks函数*/nextTick(flushSchedulerQueue)}}}

nextTick

/src/core/util/next-tick.js

const callbacks = []
let pending = false/*** 完成两件事:* 1. 用try catch 包装 flushSchedulerQueue函数,然后将其放入callbacks数组* 如果pending 为false,表示现在浏览器的任务队列中 没有flushCallbacks函数* 如果pending 为true,则表示浏览器的任务队列中已经被放入了flushCallbacks函数* 待执行 flushCallbacks函数时,pending会被再次置为false,表示下一个flushCallbacks函数可以进入浏览器的任务队列了* pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个flushCallbacks函数* cb 接收一个回调函数=> flushSchedulerQueue* ctx 上下文
*/
export function nextTick(cb? :Function ,ctx?: Object){let _resolve//用callbacks数组存储经过包装的cb函数callbacks.push(()=>{if(cb){//用try catch 包装回调函数,便于错误捕获try{cb.call(ctx)}catch(e){handleError(e,ctx,'nextTick')}}else if(_resolve){_resolve(ctx)}})if(!pending){pending = true//执行timerFunc,在浏览器的任务队列中(首选微任务队列)繁缛flushCallbacks函数timerFunc()}if(!cb && typeof Promise !== 'undefined'){return new Promise(resolve=>{_resolve = resolve}}
}

timerFunc

/src/core/util/next-tick.js

//作用就是将flushCallbacks函数放入浏览器的异步任务队列中
let timerFunc
if(typeof Promise !== 'undefined' && isNative(Promise)){const p = Promise.resolve()//首选Promise.resolve().then()timerFunc = () =>{//在微任务队列中放入flushCallbacks函数p.then(flushCallbacks)if(isIOS)  setTimeout(noop)}isUsingMicroTask = true	
}else if(!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString()==='[Object MutationObserverConstructor]')){// MutationObserver 次之let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode,{characterData: true})timerFunc = () =>{counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
}else if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){//再就是setImmediate,它其实已经是一个宏任务,但仍然比setTimeout要好timrFunc = () =>{setImmediate(flushCallbacks)}
}else{//最后没办法,则使用setTimeouttimerFunc = () =>{setTimeout(flushCallbacks,0)}
}

flushCallbacks

/src/core/util/next-tick.js

const callbacks = []
let pending = false
/*** 做了三件事*  1.将pending置为false*  2.清空callbacks数组*  3.执行callbacks数组中的每一个函数(比如flushSchedulerQueue、用户调用nextTick传递的回调函数)
*/
function flushCallbacks(){pending =falseconst copies = callbacks.slice(0)callbacks.length=0for(let i = 0;icopies[i]()}
}

flushSchedulerQueue

/src/core/observer/scheduler.js

/*** 刷新队列,由flushcallbacks函数负责调用,主要做了如下两件事* 	1.更新flushing为true,表示正在刷新队列,在此期间往队列中push新的watcher时需要特殊处理(将其放在队列的合适位置)* 	2.按照队列中watcher.id从小到大排序,保证先创建的watcher先执行,也配合第一步* 	3.遍历watcher队列,依次执行watcher.before,watcher.run,并清除缓存的watcher
*/
function flushSchedulerQueue(){currentFlushTimestamp = getNow()//标志现在正在刷新队列flushing = truelet watcher,id/*** 刷新队列之前给队列排序,可以保证:* 	1.组件的更新顺序为从父级到子级,因为父组件总是在子组件之前被创建* 	2.一个组件的用户watcher在其渲染watcher之前被执行,因为用户watcher先于渲染watcher创建* 	3.如果一个组件在其父组件的watcher执行期间被销毁,则它的watcher可以被跳过* 排序以后在刷新队列期间新进来的watcher也会按顺序进入队列的合适位置*/queue.sort((a,b) => a.id - b.id)//这里直接使用了queue.length 动态计算队列的长度,没有缓存长度,是因为在执行现有的watcher期间可能会被push进新的watcherfor(index=0; index < queue.length;i++){watcher = queue[index]//执行before狗子,在使用vm.$watch或者watch选项时可以通过配置项(options.before)传递if(watcher.before){watcher.before()}//将缓存的watcher清除id = watcher.idhas[id] = null//执行watcher.run,最后触发更新函数,比如updateComponent 或者获取 this.xx(xx为用户watch的第二个参数),当然第二个参数也有可能是一个函数,那就直接执行watcher.run()}const actieatedQueue = activatedChildren.slice()const updateQueue = queue.slice()/*** 重置调度状态* 	1.重置has缓存对象,has={}* 	2.waiting = flushing =false,表示刷新队列结束* 	  waiting = flushing = false,表示可以向callbacks数组中放入新的flushSchedulerQueue函数,并且可以向浏览器的任务队列放入下一个flushCallbacks函数了*/resetAchedulerState()//call comoponent updated and activated hookscallActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)// devtool hookif(devtools && config.devtools){devtools.emti('flush')}
}
/*** Reset the scheduler's state
*/
function resetSchedulerState(){index = queue.length - activatedChildren.length = 0has = {}if( process.env.NODE_ENV !== 'production'){circular = {}}waiting = flushing = false
}

watcher.run

/src/core/observer/watcher.js

/*** 由刷新队列函数flushSchedulerQueue调用,如果是同步watch,则由this.update直接调用,完成如下几件事:* 	1.执行实例化watcher传递的第二个参数,updateComponent或者获取this.xx的一个函数(parsePath返回的函数)* 	2.更新旧值为新值* 	3.执行实例化watcher 时传递的第三个参数,比如用户watcher的回掉函数
*/

相关内容

热门资讯

永鼎股份控股子公司拟增资扩股引... 永鼎股份(600105)12月22日晚公告,公司控股子公司鼎芯光电拟通过增资扩股方式引入外部投资者。...
预测今年演唱会音乐节带动消费超... 来源:@央视财经微博 【预测今年#演唱会音乐节带动消费超...
中创智领(00564)拟设立中... 中创智领(00564)发布公告,为了落实公司产业与投资双轮驱动发展的战略,健全完善以股权投资、创业孵...
天玑科技:公司及相关责任人涉嫌... 天玑科技12月22日晚间公告,公司于近日收到上海市虹口区人民检察院送达的《起诉书》,上海市虹口区人民...
2025中国辉煌足迹火炬传承跑... 中国青年报客户端讯(中青报·中青网记者 郭剑)2025中国辉煌足迹火炬传承跑(湖南江永站)日前在上甘...