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源码学习- 异步更新的主要内容,如果未能解决你的问题,请参考以下文章

从Vue.js源码看异步更新DOM策略及nextTick

Vue2.0 $nextTick源码理解

Vue2.0 $nextTick源码理解

vue源码解读Observer/Dep/Watcher是如何实现数据绑定的

从Vue2源码看diff算法

从Vue2源码看diff算法