【Ts重构Vue】04-异步渲染

795 阅读3分钟

如何避免多次patch,提高性能?

在复杂Vue项目中,可能会同时修改多个响应式属性,每次属性修改都会触发watch.update函数进行重新渲染,性能问题非常严峻,如何提高Vue的性能呢?

我们的编码目标是下面的demo能够成功渲染,并且最终字体颜色为yellowrenderCout的值为2。

let renderCount = 0;
let v = new Vue({
  el: '#app',
  data () {
    return {
      color: "red"
    }
  },
  render (h) {
    console.log('render:', ++renderCount)
    return h('h1', {style: {color: this.color}}, 'hello world!')
  }
})

setTimeout(() => {
  v.color = 'black'
  v.color = 'yellow'
}, 2000)

JS事件循环

JavaScript是单线程的,为避免单线程阻塞,JS设有异步事件队列。事件循环主要有2个步骤:

  1. 添加消息:异步事件会被推入事件队列等待执行,如setTimeout(fn, 1000),1秒后fn函数被推入事件队列。

  2. 执行消息:当主线程执行完所有同步任务后,接着取出所有微任务执行,再取出宏任务执行,反复循环执行。

异步渲染

回顾上面的demo,我们同步修改颜色属性,因此是否可以将watch.update方法设置为异步事件,等待所有属性修改完后再执行渲染函数?

v.color = 'black'
v.color = 'yellow'

首先我们修改update方法,执行update方法时调用queueWatcher将实例推入队列中:

class Watch {
  update() {
    queueWatcher(this)
  }
  run() {
    this.getAndInvoke(this.cb)
  }

  private getAndInvoke(cb: Function) {
    let vm: Vue = this.vm
    // let value = this.getter.call(vm, vm)
    let value = this.get()
    if (value !== this.value) {
      if (this.options!.user) {
        cb.call(vm, value, this.value)
      } else {
        cb.call(this.vm)
      }
      this.value = value
    }
  }
}

在模块内声明queue队列,用于存储待更新的watch实例;声明hasAddQueue对象保证不重复添加实例。最后调用并调用nextTick方法(等价于fn => setTimeout(fn, 0))。

let queue: ArrayWatch = []
let hasAddQueue: any = {}

function queueWatcher(watch: Watch): void {
  if (!isTruth(hasAddQueue[watch.id])) {
    hasAddQueue[watch.id] = true
    if (!flush) {
      queue.push(watch)
    } else {
      queue.push(watch)
    }

    if (!wait) {
      wait = true
      nextTick(flushQueue)
    }
  }
}

当JS执行完同步任务后,取出flushQueue开始执行。函数从queue队列中取出watch实例,并调用run方法开始渲染。

function flushQueue(): void {
  flush = true

  try {
    for (let i = 0; i < queue.length; ++i) {
      let w: Watch = queue[i]
      hasAddQueue[w.id] = null
      w.run()
    }
  } catch (e) {
    console.log(e)
  }
}

Vue执行异步渲染的逻辑

Vue实例化后,将data.color设为响应式的。

当执行v.color = 'black'时,触发执行dep.notify -> watch.update -> queueWatcher

当执行v.color = 'yellow'时,触发执行dep.notify -> watch.update -> queueWatcher

在执行queueWatcher函数时,借助全局变量hasAddQueue保证了同一个watch实例不会被重复添加

当所有同步任务执行完后,JS取出异步事件flushQueue开始执行,随后调用watch.run完成渲染。

总结

通过异步事件更新渲染,减少render的次数,大大提高了性能。在实际项目中,Vue.$nextTick也非常重要,如在nextTick的回调中获取更新后的真实dom。

杠精一下

JS和Nodejs的事件循环区别?

系列文章

【Ts重构Vue】00-Ts重构Vue前言

【Ts重构Vue】01-如何创建虚拟节点

【Ts重构Vue】02-数据如何驱动视图变化

【Ts重构Vue】03-如何给真实DOM设置样式

【Ts重构Vue】04-异步渲染

【Ts重构Vue】05-实现computed和watch功能