vue源码学习12:异步更新原理

1,147 阅读2分钟

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

image.png

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进行执行渲染操作。并同时重置调度相关的参数,包括queuehaspending
  • 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方法了。

好了,今天的学习就到此结束了,很期待下一次学习数组更新的原理

历史相关文章