依赖更新
什么时候更新?
通过 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 = false
let flushing = false
function queueWatcher (watcher) {
if (has[id] == null) { // 判断 watcher 是否已经添加过
has[id] = true
if (!flushing) {
queue.push(watcher) // watcher 推入队列
} else {
// 按照 已排好序的 队列,找到位置插入新的 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
// 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 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
has[ watcher.id ] = null
watcher.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.target
this.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 宏微正在运行中,还未执行,当执行完时 重置为 false
let timerFunc
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (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 = 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)
}
} else if (typeof setImmediate !== 'undefined') {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
nextTick (cb, ctx) {
let _resolve
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
timerFunc()
}
}
- 通过判断
pending
来确定是否需要注册宏微任务
当第一次注册的时候,把 pending
设置为 true,表示任务队列已经在开始了,同一时期内无需注册了
然后在 任务队列 执行完毕之后,再把 pending
设置为 false(在 flushCallbacks
中)
- callbacks 是一个数组,用于存放各种 需要异步执行的函数,例如:
就会把你设置的这个回调,放到 callbacks 数组中
- flushCallbacks 方法
1、复制一遍 callbacks
2、把 原来 callbacks 清空
3、遍历 逐个执行 复制出来 callbacks
这个方法是直接用于 宏微任务 执行的实际函数
Diff 比较 更新过程中的比较算法
- Diff 作用?
- 如何比较?
- 为什么这么比较?