当通过obj.key = ‘new val’ 更新值时,会触发setter的拦截,从而检测新值和旧值是否相等,如果相等什么也不做,如果不想等,则更新值,然后由dep通知watcher进行更新。所以,异步更新的入口就是setter中最后调用的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()
}
}
/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)
}
}
/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)
}
}
}
/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
}
}
}
/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)
}
}
/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]()
}
}
/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
}
/src/core/observer/watcher.js
/**
* 由刷新队列函数flushSchedulerQueue调用,如果是同步watch,则由this.update直接调用,完成如下几件事:
* 1.执行实例化watcher传递的第二个参数,updateComponent或者获取this.xx的一个函数(parsePath返回的函数)
* 2.更新旧值为新值
* 3.执行实例化watcher 时传递的第三个参数,比如用户watcher的回掉函数
*/