Vue源码学习- 异步更新
Posted ~往无前
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue源码学习- 异步更新相关的知识,希望对你有一定的参考价值。
当通过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 first
const 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] = true
if(!flushing)
//当前没有处于刷新队列状态,watcher直接入队
queue.push(watcher)
else
//已经在刷新队列
//从队列末尾开始倒序遍历,根据当前watcher.id 找到大于它的watcher.id的位置,然后将自己插入到该位置之后的下一个位置
//即将当前的watcher放入到已排列的队列中,且队列仍是有序的
let i = queue.length-1
while(i>index && queue[i].id>watcher.id)
i--
queue.splice(i+1,0,watcher)
if(!waiting)
waiting = true
if(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 = 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))
//再就是setImmediate,它其实已经是一个宏任务,但仍然比setTimeout要好
timrFunc = () =>
setImmediate(flushCallbacks)
else
//最后没办法,则使用setTimeout
timerFunc = () =>
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 =false
const copies = callbacks.slice(0)
callbacks.length=0
for(let i = 0;i<copies.length; i++)
copies[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 = true
let watcher,id
/**
* 刷新队列之前给队列排序,可以保证:
* 1.组件的更新顺序为从父级到子级,因为父组件总是在子组件之前被创建
* 2.一个组件的用户watcher在其渲染watcher之前被执行,因为用户watcher先于渲染watcher创建
* 3.如果一个组件在其父组件的watcher执行期间被销毁,则它的watcher可以被跳过
* 排序以后在刷新队列期间新进来的watcher也会按顺序进入队列的合适位置
*/
queue.sort((a,b) => a.id - b.id)
//这里直接使用了queue.length 动态计算队列的长度,没有缓存长度,是因为在执行现有的watcher期间可能会被push进新的watcher
for(index=0; index < queue.length;i++)
watcher = queue[index]
//执行before狗子,在使用vm.$watch或者watch选项时可以通过配置项(options.before)传递
if(watcher.before)
watcher.before()
//将缓存的watcher清除
id = watcher.id
has[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 hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
if(devtools && config.devtools)
devtools.emti('flush')
/**
* Reset the scheduler's state
*/
function resetSchedulerState()
index = queue.length - activatedChildren.length = 0
has =
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的回掉函数
*/
以上是关于Vue源码学习- 异步更新的主要内容,如果未能解决你的问题,请参考以下文章