在vue源码学习11:响应式原理和依赖收集一文中,实现了使用Watcher类和De类实现数据和视图的响应式,以及了解了vue中是如何进行依赖收集。
最后留下了一个问题:如果频繁的更新一个watcher,页面也会频繁的渲染。
在vue的源码中,是使用一种异步的机制进行页面更新的。今天要学习的就是异步更新原理
要解决的性能问题
在watcher中有一个update方法,一旦数据频繁更新,这个update方法就会多次被执行。
update() {
// 这个方法会频繁操作
this.get()
}
get() {
// get做的事情就是渲染页面
pushTarget(this)
this.getter()
popTarget()
}
我们知道this.getter
方法调用的事下面这个方法
updateComponent = () => {
// 1. 通过render生成虚拟dom
vm._update(vm._render()) // 后续更新可以调动updateComponent方法
// 2. 虚拟Dom生成真实Dom
}
这样带来的后果是,性能就会非常低下。为了提升效率,Vue做了两件事情:
- 每次更新时,把watcher缓存下来
- 如果多次跟新的是一个watcher,合并成一个,一起渲染页面
queueWatcher
queueWatcher
顾名思义,这是一个watcer的队列。
我们在observer文件夹下建立了一个scheduler.js
scheduler是一个有关调度程序的js文件
// 调度工作scheduler.js
import { nextTick } from '../util';
// 1. 去重 2. 防抖
let queue = []
let has = {} // 列表维护存放了哪些watcher
function flushSchedulerQueue() {
for (let i = 0; i < queue.length; i++) {
queue[i].run()
}
queue = []
has = {}
pending = false
}
let pending = false
export function queueWatcher(watcher) {
// 多次更新,会收到多个watcher
const id = watcher.id
if (has[id] == null) {
queue.push(watcher)
has[id] = true
// 开启一次更新操作 批处理 (防抖)
if (!pending) {
nextTick(flushSchedulerQueue, 0)
pending = true
}
}
}
这个文件scheduler.js
中相关变量方法作用如下:
- queue:一个队列,用来存放要变更的watcher
- has:用来存放watcher对应的id,id值用来去重,如果id存在于这个has对象中,则不再想queue队列存放,否则将会存放
- flushSchedulerQueue:刷新调度队列,执行这个方法,将会对queue中的所有watcher进行执行渲染操作。并同时重置调度相关的参数,包括
queue
、has
、pending
- pending:渲染中,这是一个类似于锁的概念,如果pending为false,则开启一次批处理操作,直到所有的watcher被执行完成之后,pending状态将被重置
nextTick方法
在scheduler中使用了一个nextTick
方法,代码如下
const callbacks = []
function flushCallbacks() {
callbacks.forEach(cb => cb())
waiting = false
}
function timer(flushCallbacks) {
let timerFn = () => { }
if (Promise) {
timerFn = () => {
Promise.resolve().then(flushCallbacks)
}
} else if (MutationObserver) {
// 这个也是微任务
let textNode = document.createTextNode(1)
let observe = new MutationObserver(flushCallbacks)
observe.observe(textNode, {
characterData: true
})
timerFn = () => {
textNode.textContent = 3
}
} else if (setImmediate) {
timerFn = () => {
setImmediate(flushCallbacks)
}
} else {
timerFn = () => {
setTimeout(flushCallbacks, 0)
}
}
timerFn()
}
export function nextTick(cb) {
callbacks.push(cb) // 先修改数据 flush(先执行) / 后用户调用vm.$nextTick(后)
if (!waiting) {
timer(flushCallbacks)
waiting = true
}
}
通俗的解释一下这段代码,就是:用一个微任务
或者宏任务
,去执行一组watcher。timer方法中的代码是对浏览器微任务、宏任务的兼容处理。这个处理在Vue3.0中没有做兼容了。
详细的梳理如下:
scheduler.js中传入一flushSchedulerQueue的回调函数cb
,收到cb后,存入callbacks队列。
如果当前不是waiting态,则启动一个微任务(不兼容则启用宏任务),去执行这个回调,然后重置wating状态
$nextTick
Vue中常常用的this.$nextTick(),用的也是这个nextTick方法。
也就是说在vue初始化的时候,会在prototype上面挂载一个nextTick方法。
import { nextTick } from './util';
export function lifecycleMixin(Vue) {
// ...其他代码
Vue.prototype.$nextTick = nextTick
}
如此,我们就可以通过vue的实例来调用$nextTick方法了。
好了,今天的学习就到此结束了,很期待下一次学习数组更新的原理
。