• Vue源码学习(4)- 异步更新


    当通过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()
      }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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)
      }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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)
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    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
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    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)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    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]()
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    watcher.run

    /src/core/observer/watcher.js

    /**
      * 由刷新队列函数flushSchedulerQueue调用,如果是同步watch,则由this.update直接调用,完成如下几件事:
      * 	1.执行实例化watcher传递的第二个参数,updateComponent或者获取this.xx的一个函数(parsePath返回的函数)
      * 	2.更新旧值为新值
      * 	3.执行实例化watcher 时传递的第三个参数,比如用户watcher的回掉函数
    */
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    C语言从入门到精通 第二章(数据的表现形式)
    vue2 vant-ui 实现搜索过滤、高亮功能
    点云从入门到精通技术详解100篇-基于三维点云的路况语义分割(续)
    Go 什么是循环依赖
    SpringMVC(一)SpringMVC 简介
    产品经理或项目经理考PMP,薪资会不会提高?
    QT QTableView 委托:垂直表头
    VS编译的时候不生成Release文件夹
    循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(9) -- 实现系统动态菜单的配置和权限分配
    java开发中 防止重复提交的几种方案
  • 原文地址:https://blog.csdn.net/weixin_44374938/article/details/127871799