依赖更新
什么时候更新?
通过 Object.defainePropetry 设置 set 函数,当 属性 被重新赋值 或 值变化 时,则会触发 set 函数,此时就可以 set 函数里做更新操作了,通过调用 【dep.netify】通知收集到的所有依赖(watcher) 源码如下:
function defineReactive (obj, key, val) {// 在闭包中实例化 Dep 对象,用于收集 watcher 对象const dep = new Dep()// ...Object.defineProperty(obj, key, {get: function reactiveGetter () {// ...if (Dep.target) {// 进行依赖收集,将观察者实例(watcher)放入 dep.sub 数组中dep.depend()}return value},set: function reactiveSetter (newVal) {// ...// dep.notify 将通知所有保存在 dep.subs 的 watcher 实例dep.notify()}})}
需要注意的是:动态添加的 属性(property )或移除 不会触发更新,所有 响应式 的属性、对象 都必须提前声明,哪怕是一个空值,对于对象,对于数组。
如何更新?
【dep.netify】方法被触发后将遍历逐个 调用 watcher.update 方法。源码如下:
class Dep {constructor () {// 存放 watcher 依赖的数组this.subs = []}notify () {const subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update() // 触发 watcher update 方法}}}
update 方法 调用后就直接更新页面吗?当然不是。会先开启一个 队列(Queue 本质是一个数组),将发在 同一事件循环 内所有更新的 watcher 推入队列内,如果同一个 wathcer 被多次触发,根据 watcher.id 判断已经存在 队列 里则不会 推入。然后使用 异步方法(nextTick) 在下一次 事件循环“Tick”中,执行 队列 中的所有 wathcer.run() 方法。源码如下:
update 方法
class Watcher {update () {// ...queueWatcher(this)}}
queueWatcher 方法将 wathcer 推入队列
let has = {} // 用于 watcher 去处重处理let queue = [] // 存放 watcher 队列let waiting = falselet flushing = falsefunction queueWatcher (watcher) {if (has[id] == null) { // 判断 watcher 是否已经添加过has[id] = trueif (!flushing) {queue.push(watcher) // watcher 推入队列} else {// 按照 已排好序的 队列,找到位置插入新的 watcherlet i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}}if(!waiting) {waiting = true// nextTick 方法,将注册 flushSchedulerQueue 回调函数 在下一次 Tick 进行触发nextTick(flushSchedulerQueue)}}
- waiting 标记,为 true 时表示,已经把 flushSchedulerQueue 注册到 微任务上,开始执行逐个执行 队列的 watcher 直到全部将 队列 全部清空后,才重置为 false。
- flushing 标记,为 true 时表示,队列正在执行更新,执行前 会先给所有 watcher 按照 watcher.id 进行 升序排序。此时再有 wathcer 进来,根据自身 watcher.id 从 队列 再 按照排好的顺序插入。直到全部将 队列 全部清空后,才重置为 false
- nextTick 方法,将 flushSchedulerQueue 注册到 微任务 执行
flushSchedulerQueue 方法 逐个执行 队列 watcher
function flushSchedulerQueue () {// 给 watcher 按 id 升序排序queue.sort((a, b) => a.id - b.id)let watcherfor (index = 0; index < queue.length; index++) {watcher = queue[index]has[ watcher.id ] = nullwatcher.run() // 执行更新}}
- 为什么要给 watcher 按照 id(id是自增的) 升序排序?
这样做可以保证,组件是 从父组件到子组件 的顺序更新,因为父组件总是比子组件先创建
一个组件的 user watchers(用户创建的)会比 render watcher(页面更新) 先运行,因为 user watchers 往往比 render watcher 更早创建
如果一个子组件 在父组件 watcher 运行期间被销毁,它的 watcher 执行将被跳过
- 为何 从父组件到子组件 的顺序更新呢?
因为 父组件跟子组件是有联系的 如:props ,所以,父组件必须先更新,把最新数据传给 子组件,子组件再更新,此时才能获取最新的数据。
- 使用 队列、微任务异步更新 目的是什么?
- 减少重复更新。去除重复的 watcher ,避免不必要计算和 DOM 操作
- 加快异步代码的执行。Micro Task(微任务) 执行,会比 Macro task(宏任务) 更早执行,所以优先使用
- 避免频繁地更新。缓存在同一事件循环中发生的所有数据变更
根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 Micro Task(微任务)中就完成数据更新, 当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。 参考:https://www.zhihu.com/question/55364497/answer/144215284
- run 方法 执行更新,主要是执行 watcher 创建之前保存的 更新函数(页面渲染、computed、watch …),执行时页面渲染时会将会 重新收集新的依赖,并且清除 原来存在,本次渲染不存在的依赖,完成依赖收集之后得到 新的 VNode 将会跟 旧的 VNode 进行 Diff 比较,找出最小的更新点,进行 原生的DOM 操作更新,至此完成 数据 到 页面的更新过程。源码如下:
class Watcher{get () {pushTarget(this) // 将自身设置给 Dep.target,用以依赖收集// ...value = this.getter.call(vm, vm) // 执行更新函数,重新收集依赖//...popTarget() // 取出并设置给 Dep.targetthis.cleanupDeps() // 清除上一次存在,但本次渲染不存在的依赖return value}// ...run () {const value = this.get() // 执行更新函数,重新收集依赖// ..const oldValue = this.value // 保存旧的 value 值// ..this.value = value // 设置新的 value 值// 触发更新后回调this.cb.call(this.vm, value, oldValue)}// ...}
nextTick 方法实现原理
前面提到 Vue 使用 nextTick 方法 ,在内部维护了一个 任务队列 去配合 宏微任务 实现异步更新,其目的是:
减少 宏微任务的注册。尽量把所有的异步执行代码 放在一个 宏微任务中,减少消耗,如果一个异步代码就注册一个 宏微任务的话,那么完全执行完代码肯定慢得多
加快异步代码的执行。优先使用 微任务 使其异步代码能尽快执行,内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。
避免频繁地更新。Vue 中就算修改多次数据,页面还是只会更新一次。避免多次修改数据导致 多次频繁更新页面,实现让多次修改只用更新最后一次
下面是具体源码:
let isUsingMicroTask = false // 是否是 微任务const callbacks = [] // 存放 异步所需执行的函数let pending = false // 判断 宏微任务 是否在运行中,为 true 宏微正在运行中,还未执行,当执行完时 重置为 falselet timerFuncfunction flushCallbacks () {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}}/** 尝试使用原生的 Promise.then、MutationObserver 和 setImmediate* 如果执行环境不支持,则会采用 setTimeout(fn, 0)*/if (typeof Promise !== 'undefined'){const p = Promise.resolve()timerFunc = () => {p.then(flushCallbacks)}} else if (!isIE && typeof MutationObserver !== 'undefined') {let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}} else if (typeof setImmediate !== 'undefined') {timerFunc = () => {setImmediate(flushCallbacks)}} else {timerFunc = () => {setTimeout(flushCallbacks, 0)}}nextTick (cb, ctx) {let _resolvecallbacks.push(() => {cb.call(ctx)})if (!pending) {pending = truetimerFunc()}}
- 通过判断
pending来确定是否需要注册宏微任务
当第一次注册的时候,把 pending 设置为 true,表示任务队列已经在开始了,同一时期内无需注册了
然后在 任务队列 执行完毕之后,再把 pending 设置为 false(在 flushCallbacks 中)
- callbacks 是一个数组,用于存放各种 需要异步执行的函数,例如:

就会把你设置的这个回调,放到 callbacks 数组中
- flushCallbacks 方法
1、复制一遍 callbacks
2、把 原来 callbacks 清空
3、遍历 逐个执行 复制出来 callbacks
这个方法是直接用于 宏微任务 执行的实际函数
Diff 比较 更新过程中的比较算法
- Diff 作用?
- 如何比较?
- 为什么这么比较?
