Vue源码解读之组件为何采用异步渲染 ( nextTick的实现原理 )

前言

有人说:是为了提高性能,对,根本上也是这么个道理 ;那到底是如何做的呢 ?

其实在vue中,响应式数据是组件级的,也就是说,每一次的更新都是渲染整个组件,如果是同步的话,根据我们前边理解的响应式数据原理,一旦修改了data属性,便会触发对应的 watcher,然后调用对应 watcher 下的 update 方法更新视图,那么结果显而易见,太频繁了 !如下代码:

// 省略多余模板语法
data () {
	a:1,
	b:2,
	c:3
}
//如果我们按照同步的逻辑,修改data属性,this.a = 10; this.b = 20; this.c = 30; 
//就会调用三次update渲染视图,岂不是很耗性能 ?而且体验也不好。

所以 vue 采用的是异步渲染 接下来,我们来了解一下 ;前边也有讲过响应式数据原理,不了解的童鞋可以回过头去看看Go,这里我就接着数据更新方法update开始;

src/croe/observer/watcher.js 166 行,这里的更新先不考虑计算属性和同步,我们直接看向queueWatcher

update () {
    /* istanbul ignore else */
    if (this.lazy) { // 计算属性  依赖的数据发生变化了 会让计算属性的watcher的dirty变成true
      this.dirty = true
    } else if (this.sync) { // 同步watcher
      this.run()
    } else {
      queueWatcher(this) // 将要更新的 watcher 放入队列
    }
}

src/core/observer.scheduler.js 164行,queueWatcher 方法;实现一个watcher 队列 ,每一次的update 都放入到队列中,然后进行统一异步处理 。 看代码:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id // 过滤 watcher,多个data属性依赖同一个watcher ,一个组件只有一个watcher
  if (has[id] == null) {
    has[id] = true 
    if (!flushing) {
      queue.push(watcher) // 将watcher放到队列中
    } else { // 通过对 id 的判断,这里的 id 是自加1,可查看 watcher.js 源码,
    		// 如果已经刷新了,则赋值当前的id , 如果id超过了,将运行如下代码
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) { // 如果不等了,则进行刷新 
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue() // 该方法做了刷新前的 beforUpdate 方法调用,然后 watcher.run() 
        return
      }
      nextTick(flushSchedulerQueue) // 在下一次tick中刷新 watcher 队列 (借用nextTick)
      								//(包含同一watcher的多个data属性),
      								// 这里的nextTick 就是我们的常用api => this.$nextTick() 
    }
  }
}

好了,通过源码简单的分析,明白为啥 vue 为啥采用异步更新了吧,原因很简单,因为vue是组件级更新视图,每一次update都要渲染整个组件,为了提高性能,采用了队列的形式,存储同一个watcher的所有data属性变化,然后统一调用nextTick 方法进行更新渲染(有且只调用一次)。

问题来了,nextTick 方法是异步的 ,那么它又是如何实现的异步更新呢 ?来看张图
在这里插入图片描述
从图来看,调用了 nextTick 之后,将watcher队列回调函数暂时存入了一个数组callbacks 中,然后才依次调用 timeFun()方法执行,而真正让watcher异步的关键就在这儿,我们通过代码来看一下:

首先进入 nextTick 函数 src/core/util/next-tick.js 87 行

export function nextTick (cb?: Function, ctx?: Object) { 
// flushSchedulerQueue 会使用 nextTick 保证当前视图渲染完成
  let _resolve
  callbacks.push(() => {  // 暂存 watcher 队列
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {  // 状态改变后,调用 timerFun() 方法
    pending = true
    timerFunc() // 重点,重点,重点! 我们进去看一下
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc 方法 src/core/util/next-tick.js 33 行 ,看如下代码,会Js的童鞋应该就可以看出来是什么东西;它对当前的环境进行了判断,如果支持promise 就用 promise 依次往下: MutationObserver , setImmediate , setTimeout 这四个分别都是异步解决方案,除了 setTimeout 是宏观任务以外,其它三个都是微观任务;

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks) // then 里边执行 flushCallbacks 
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available, 
  // e.g. PhantomJS, iOS7, Android 4.4 
  // (#6466 MutationObserver is unreliable in IE11) 
  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)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)   // setImmediate 回调里边
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)  // setTimeout 回调里边
  }
}

总结

nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick 会将方法存入
队列中,通过这个异步方法清空当前队列。 所以这个 nextTick 方法就是异步方法 。

而我们平常使用的api :vue.nextTick() 也是如此 .

发布了26 篇原创文章 · 获赞 53 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/HiSen_CSDN/article/details/104911040