You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * 通知 dep 中的所有 watcher,执行 watcher.update() 方法 */notify(){// stabilize the subscriber list firstconstsubs=this.subs.slice()if(process.env.NODE_ENV!=='production'&&!config.async){// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort((a,b)=>a.id-b.id)}// 遍历当前dep收集到的所有watcher 让这些watcher依次执行update方法for(leti=0,l=subs.length;i<l;i++){subs[i].update()}}
exportfunctionqueueWatcher(watcher: Watcher){constid=watcher.id// 拿到当前watcher的idif(has[id]==null){// 判重,watcher不会重复入队 --> 也就是说在一个周期内,一个数据多次触发更新的时候 这个数据的watcher不会重复入队// 除此以外,例如 renderWatcher,一个 watcher 同时是多个数据的 watcher,它也不会被重复入队has[id]=trueif(!flushing){// 如果flushig = false 表示当前 watcher 队列没有在被刷新了,watcher直接入队queue.push(watcher)}else{// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.// watcher 队列正在被刷新,这时候这个 watcher 入队就需要特殊操作了// 因为queue在刷新的时候会根据watcher id由小到大进行排序,然后再从最小的watcher开始执行。因此中途新插入了一个watcher的话// 我们需要保证watcher id仍然是有序的,因此有了下面这个while循环和splice// TODO: 为什么要保证 watcher id 有序? leti=queue.length-1while(i>index&&queue[i].id>watcher.id){i--}queue.splice(i+1,0,watcher)}// queue the flushif(!waiting){// waiting 为 false 的时候走这里,表示当前浏览器的异步任务队列中没有 flushScheulerQueue 函数waiting=trueif(process.env.NODE_ENV!=='production'&&!config.async){// 同步执行的时候 直接去刷新watcher队列// 同步执行的时候性能会大打折扣// 主要是为了兼容 SSR,SSR的时候会需要同步执行 flushSchedulerQueue()return}// 我们熟悉的那个nextTick,this.$nextTick或者Vue.nextTick// 通过这么一层封装,我们会把 flushSchecheulerQueue 函数放到浏览器的异步任务队列中nextTick(flushSchedulerQueue)}}}
queueWatcher 函数本质上就是完成以下几个操作
首先判重,确保在一个周期内,就算数据多次触发更新,这个数据的 watcher 不会重复进入队列
其次将 watcher 插入 watcher 队列
需要按照 watcher id 进行插入
如果当前队列中这个 id 已经被略过了 那么立马执行这个 watcher
最后调用 nextTick(flushSchedulerQueue)
当然,这里我们留了一个小问题,为什么需要按照 watcher id 的顺序进行入队和执行?这个问题在下一节就会解答。
flushSchedulerQueue
functionflushSchedulerQueue(){currentFlushTimestamp=getNow()// 将flushing设置为true -- 表示现在的watcher队列正在被刷新flushing=trueletwatcher,id// Sort queue before flush.// This ensures that:// 1. Components are updated from parent to child. (because parent is always// created before the child)// 2. A component's user watchers are run before its render watcher (because// user watchers are created before the render watcher)// 3. If a component is destroyed during a parent component's watcher run,// its watchers can be skipped.queue.sort((a,b)=>a.id-b.id)// do not cache length because more watchers might be pushed// as we run existing watchers// for循环遍历队列 执行 watcher 的 run 方法。注意这里用的是queue.length 为啥不在外面先存一个len = queue.length呢?// 因为刷新队列的时候 watcher 队列可能会放入新的watcher,因此queue.length不可以写死!for(index=0;index<queue.length;index++){watcher=queue[index]// 首先执行before钩子,before钩子是创建watcher实例的时候在第四个参数的Obj里传入的if(watcher.before){watcher.before()}// 清空缓存 表示当前watcher已经被执行了, 当该watcher再次入队的时候 就可以成功入队了id=watcher.idhas[id]=null// 执行run方法 最终触发更新函数 比如updateComponent或者获取this.xx (xx为用户watch的第二个参数)watcher.run()}// keep copies of post queues before resetting stateconstactivatedQueue=activatedChildren.slice()constupdatedQueue=queue.slice()resetSchedulerState()// waiting 和 flushing 重新设置为 false;重置 has 缓存对象// call component updated and activated hookscallActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)}
lettimerFunc// 就是一个有降级的func,如果浏览器有promise就用promise,如果实在啥都没有了 就用setTimeout()// The nextTick behavior leverages the microtask queue, which can be accessed// via either native Promise.then or MutationObserver.// MutationObserver has wider support, however it is seriously bugged in// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It// completely stops working after triggering a few times... so, if native// Promise is available, we will use it:/* istanbul ignore next, $flow-disable-line */// 注意:这些逻辑在next-tick.js被引入的时候就会执行了 --> 可以加一行 alert(1)验证一下// 首先 promiseif(typeofPromise!=='undefined'&&isNative(Promise)){constp=Promise.resolve()timerFunc=()=>{p.then(flushCallbacks)// In problematic UIWebViews, Promise.then doesn't completely break, but// it can get stuck in a weird state where callbacks are pushed into the// microtask queue but the queue isn't being flushed, until the browser// needs to do some other work, e.g. handle a timer. Therefore we can// "force" the microtask queue to be flushed by adding an empty timer.if(isIOS)setTimeout(noop)}isUsingMicroTask=true}elseif(!isIE&&typeofMutationObserver!=='undefined'&&(isNative(MutationObserver)||// PhantomJS and iOS 7.xMutationObserver.toString()==='[object MutationObserverConstructor]')){// Use MutationObserver where native Promise is not available,// e.g. PhantomJS, iOS7, Android 4.4// (#6466 MutationObserver is unreliable in IE11)letcounter=1constobserver=newMutationObserver(flushCallbacks)consttextNode=document.createTextNode(String(counter))observer.observe(textNode,{characterData: true})timerFunc=()=>{counter=(counter+1)%2textNode.data=String(counter)}isUsingMicroTask=true}elseif(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){// Fallback to setImmediate.// Technically it leverages the (macro) task queue,// but it is still a better choice than setTimeout.timerFunc=()=>{setImmediate(flushCallbacks)}}else{// Fallback to setTimeout.timerFunc=()=>{setTimeout(flushCallbacks,0)}}
/** * Scheduler job interface. * Will be called by the scheduler. *//** * 由 刷新队列函数 flushSchedulerQueue 调用,完成如下几件事: * 1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数) * 2、更新旧值为新值 * 3、执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数 */run(){if(this.active){constvalue=this.get()if(value!==this.value||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value)||this.deep){// set new valueconstoldValue=this.valuethis.value=valueif(this.user){// 用户watcher 再执行一下watch回调// watch(() => {}, (val, oldVal) => {})constinfo=`callback for watcher "${this.expression}"`invokeWithErrorHandling(this.cb,this.vm,[value,oldValue],this.vm,info)}else{this.cb.call(this.vm,value,oldValue)}}}}
watcher.get()
get(){pushTarget(this)// Dep.target = this 也就是收集依赖 -- 为啥执行更新的时候又要重新收集依赖呢?// 当响应数据被更新的时候才会走到这一步 此时需要对新的 value 进行依赖收集letvalueconstvm=this.vmtry{// 执行实例化watcher传递进来的第二个参数// 有可能是个函数,比如 实例化渲染 watcher时传递的 updateComponent函数 然后进入patch阶段// 也有可能是用户watcher 传递的可能是一个key 也可能是读取this.key的函数// 触发读取操作后,被getter拦截 进行依赖收集// 举一个例子,如果执行了updateComponent的话,那么会触发update和render,然后会触发页面中被用到的数据的setter,响应式的触发依赖收集等步骤value=this.getter.call(vm,vm)// 关键的是这一步骤!!! this.getter就是刚刚实例化watcher时传递进来的第二个参数,即computed的getter函数}catch(e){if(this.user){handleError(e,vm,`getter for watcher "${this.expression}"`)}else{throwe}}finally{// "touch" every property so they are all tracked as// dependencies for deep watchingif(this.deep){traverse(value)}popTarget()this.cleanupDeps()}returnvalue// 用户watcher的时候会返回一个执行结果}
Vue2 源码解析 —— 异步更新
在上文中我们已经知道了 Vue 会对页面渲染的所有属性都进行依赖收集,当对数据进行更新操作时,比如
obj.key = 'new val'
就会触发 setter 的拦截,从而检测新值和旧值是否相等,如果相等什么也不做,如果不相等,则更新值,然后由 dep 通知 watcher 进行更新。所以,异步更新的入口点就是 setter 中最后调用的dep.notify()
方法。然而,是否是数据一更新 Vue 就马上执行 Watchers 定义的 Callback 函数呢?想象一下,在一个宏任务中,我们可能同时要修改一系列的属性,如果每修改一个属性我们就立即运行相关的 Watchers,那么就有可能导致
因此,为了使运行更流畅,在 setter 触发后,回调函数不会立即运行,而是进入到 Vue 的异步更新队列。也就是说,Vue 有异步更新的特点。
notify()
setter 中的 dep.notify() 如上,注意到在依赖收集阶段每一个属性对应的 Dep 对象也收集了所有监听它的 Watcher 储存到 subs 属性中,因此这一步的本质就是让所有监听自己的 Watchers 都执行
update
方法update()
在 update 方法中,我们主要根据 watcher 的配置项,来决定接下来该怎么行动
{sync: true}
的 watcher,那么会直接执行run()
方法,执行 watcher 创建时声明的回调函数queueWatcher(this)
,将当前的这个 watcher 对象放入到一个异步队列中queueWatcher(watcher: Watcher)
queueWatcher 函数本质上就是完成以下几个操作
nextTick(flushSchedulerQueue)
当然,这里我们留了一个小问题,为什么需要按照 watcher id 的顺序进行入队和执行?这个问题在下一节就会解答。
flushSchedulerQueue
flushSchedulerQueue 函数其实就做了三件事情
而在这段代码中也解释了,为什么我们需要给 watcher 按序插入队列和执行
nextTick (cb?: Function, ctx?: Object)
这里的 nextTick 函数其实也就是我们常常使用的 Vue.nextTick 函数!
完成了这些事情:
timerFunc
timerFunc 本质上是一个函数,但是 Vue 需要通过不断检测当前运行时的能力决定 timerFunc 是一个怎样的函数。因此这里其实是一个层层降级的赋值过程。优先级是 Promise -> MutationObserver -> setImmediate -> setTimeout
从这里我们可以看出来我们本质上是要异步执行
flushCallbacks
函数flushCallbacks()
三件事情:
因此,在这里我们刚刚的
flushCallbacks
函数才真正的被执行。watcher.run()
flushSchedulerQueue 会执行所有的 watcher 的 run 函数
watcher.get()
get
方法乍一看很简单,但其实在很多场合被调用,因此其实是非常重要的方法!Case 1: 当我们 initWatcher 的时候,会触发 this.get()
在进入这个函数之前,我们已经成功新创建了一个 watcher,但是这个 watcher 和我们需要监听的属性之间还没有产生任何关联, 我们监听的属性在 initData 的时候被处理过了,已经有了一个属于它的 Dep 对象,我们希望将自己这个 watcher 给添加到这个 Dep.subs 中,这样,当我们监听的属性的 setter 被触发的时候,就会有 dep.notify(),我们新的 watcher 就会响应。因此,我们在 get() 方法中调用了 this.getter(), 在 getter 的时候就会有 dep.depend() 操作,实现上文的目的
Case 2: 从 computed 属性的视角来看,调用 get() 函数的时机
假设我们有一个 key 叫做 computedAge, 还有一个 data 叫做 age。当我们第一次
{{ computedAge }}
的时候,触发 computedGetter -> watcher.evaluate() -> watcher.get() -> this.getter()。其中,
this.getter()
会获取 age 的值,触发 age 的 getter,此时,age 的 dep 中就会有 computedAge 的 watcher。然后,每次获取 computedAge 的时候,因为 dirty = false,因此会直接返回缓存的值,不会再次触发 getter。后续,当 age 更新的时候,dep.notify(),会更新所有的相关的 watcher,其中就包括了 computedAge 的 watcher,并且 dirty = true,当我们再次获取 commputedAge 的时候,就会重复这个流程
Case 3: 在 mountComponent 方法中
在 mountComponent 方法中,会执行一个
new Watcher(vm, updateComponent, noop, cb, true)
。而这个就是 render watcher,它触发了 get 方法,在 get 方法中,会调用 this.getter(),也就是 updateComponent。updateComponent 中会调用 vm._update(vm._render(), hydrating),其中 vm._render() 会触发 vm 上的各种属性的 getter。因此,收集了所有的属性的依赖。
总结
1. 为什么 Vue.nextTick(cb) 中,我们可以访问到更新后的 DOM?
当我们更新数据的时候,数据的更新是同步的,而 DOM 的更新是异步的,因此在下面这个例子中,如果我们直接数据更新后立刻访问 DOM,我们得到的仍然是旧的值。
而官方提供了一个 nextTick 方法,可以让其中的回调函数访问到更新后的 DOM!这是怎么做到的呢?
我们注意到 nextTick 的本质就是将回调函数放入到 callbacks 队列中。而当数据更新的时候,我们首先会将 flushSchedulerQueue 给放入到队列中,然后再将此时的 cb 放入到队列中。
队列中的函数会按照顺序执行,执行 flushSchedulerQueue 的时候 user watchers 和 render watcher 都会被执行,而在执行完 render watcher 之后,DOM 的数据已经更新了!此后再执行 cb,访问到的就是更新后的 DOM。
2. Vue 数据更新的时候到底发生了什么?
首先这个数据立马被 set 更新(同步数据更新),其 setter 被触发,会 notify 所有的 watcher。
将这些 watcher 放入到一个全局的队列中,然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。
如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。
flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。
flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。
3. 为什么 timerFunc 倾向于使用 Promise 而不是 setTimeout?
这其实和浏览器的渲染规则有关,Promise 的话,我们的 flushCallbacks 函数会被放在 microtask 队列中,而 setTimeout 则会将函数放在下一个 macrotask 队列中。
而浏览器渲染的时候,要求当前的 microTask queue 被清空,因此,将 flushCallbacks 放在 microtask queue 中,可以确保当浏览器要渲染的时候,里面的 render watcher 已经被执行了,DOM 已经被更新了。
相应的,如果用 setTimeout 来处理这些 watcher 回调的话,当浏览器渲染的时候,可能并没有执行 render watcher 中的 DOM 更新部分,那么用户就会感知到掉帧。尤其是,如果前一个 macroTask 耗时特别长,那么掉帧的概率就会急剧增大。
至此,Vue 的响应式原理和异步更新部分就已经讲解完了,这两个特性在源代码中的位置相近并且逻辑存在一定的耦合。这篇文章和下面两篇文章一起阅读,可以帮助促进理解!
The text was updated successfully, but these errors were encountered: