zoukankan      html  css  js  c++  java
  • $nextTick 源码解析

    nextTickvue中重要的性能优化方式,解析实现原理可以有助于我们更好的理解框架。

    1. nextTick 的实现原理
    2. 为什么 vue 采用异步渲染?
    3. 响应式的数据 for 循环改变了1000次为什么视图只更新了一次?
    4. nextTick历史版本问题
    5. event loop

    带这以上问题来阅读源码,有助于我们思考。
    源码:

    /* @flow */
    /* globals MutationObserver */
    
    // 引入 noop、 handleError 错误处理、 isIE、 isIOS、 isNative 方法
    import { noop } from 'shared/util'
    import { handleError } from './error'
    import { isIE, isIOS, isNative } from './env'
    
    // 是否正在使用微任务
    export let isUsingMicroTask = false
    
    // 需要处理的事件队列
    const callbacks = []
    // 设置一个标记,如果已经有 timerFunc 被推送到任务队列中去则不再推送
    let pending = false
    
    // 执行事件队列中的事件
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    // 这里我们有使用微任务的异步延迟包装器
    // Here we have async deferring wrappers using microtasks.
    // 在 2.5 版本我们使用宏任务和微任务相结合
    // In 2.5 we used (macro) tasks (in combination with microtasks).
    // 然而,当状态在重新绘制之前更改时,它有一些微妙的问题
    // However, it has subtle problems when state is changed right before repaint 
    // (e.g. #6813, out-in transitions).
    // 另外,在事件处理程序中使用宏任务会导致一些无法回避的奇怪的行为
    // Also, using (macro) tasks in event handler would cause some weird behaviors
    // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
    // 所以我们现在重新使用微任务
    // So we now use microtasks everywhere, again.
    // A major drawback of this tradeoff is that there are some scenarios
    // where microtasks have too high a priority and fire in between supposedly
    // sequential events (e.g. #4521, #6690, which have workarounds)
    // or even between bubbling of the same event (#6566).
    
    // 设置一个函数指针,将该指针添加到任务队列,待主线程任务执行完毕后,
    // 再将任务队列中的 timerFunc 函数添加到执行栈中执行
    let timerFunc
    
    // The nextTick behavior leverages the microtask queue, which can be accessed
    // via either native Promise.then or MutationObserver.
    // MutationObserver has wider support, however it is seriously bugged in
    // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
    // completely stops working after triggering a few times... so, if native
    // Promise is available, we will use it:
    /* istanbul ignore next, $flow-disable-line */
    
    // 执行的优先顺序为 promise.then => MutationObserver => setImmediate => setTimeout
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        // In problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        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)
      }
    } else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    // 将回调函数 cb 包装成一个箭头函数 push 到事件队列 callbacks 中
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      // 向事件队列中添加箭头函数作为参数
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      /* pending 在事件队列执行的时候(flushCallbacks调用)才设为 false
        为了确保 timerFunc 函数把 flushCallbacks 添加到队列一次
        (flushCallbacks 被添加到队里至尚未执行这段时间内)
      */
      if (!pending) {
        pending = true
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    
      this.$nextTick(cb);
    

    1. nextTick 的实现原理**

    简单的概括,nextTick 只做了两件事情:

    1. 将回调函数 cb 包装处理为箭头函数添加到事件队列中
    2. 事件队列异步执行(执行的优先顺序为 promise.then => MutationObserver => setImmediate => setTimeout)

    2. 为什么 vue 采用异步渲染?

    this. message='更新完成1'//DOM更新一次
    this. message='更新完成2'//DOM更新两次
    this. message='更新完成3'//DOM更新三次
    
    this.message = '更改数据'
    this.age = 23
    this.name = 'hello'
    

    如果采用同步更新的话,vue 观察到数据改变就进行一次计算、渲染,那么以上就会重复三次这样的过程。
    对属性进行多次操作的情况,我们并不关心中间的过程发生了什么,只需要知道最后的结果。
    如果我们在对所有数据的操作执行完之后才执行计算、渲染就可以只执行一次,而 event loop 刚好具有这个特性。
    对所有数据的同步操作完成之后再进行渲染,可以减少不必要的计算、渲染

    3. 响应式的数据 for 循环改变了1000次为什么视图只更新了一次?

    这里牵扯到 vue 的响应式原理:
    vue 实例化的时候会创建一个 observe 实例,通过 Object.definePropertydata 设置 getset。 初始化编译的时候会触发 get 方法进行依赖收集,将观察者 watcher 对象添加到订阅者 dep 中。数据改变的时候会触发 set方法,通知 dep 中的 watcher 执行 update 方法, 将 watcher 添加到事件队列 queue 中,执行 nextTick(queue)
    这里有两个点需要注意:

    • 是否可以将相同的观察者 watcher 添加到 dep 中 ?
    • watcher 中的 update 方法是否可以将相同的 watcher 添加到 queue 中 ?

    答应是不可以

    watcher.js 部分源码如下:

     /**
       * Subscriber interface.
       * Will be called when a dependency changes.
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    /**
       * Add a dependency to this directive.
       */
      addDep (dep: Dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
          this.newDepIds.add(id)
          this.newDeps.push(dep)
          if (!this.depIds.has(id)) {
            dep.addSub(this)
          }
        }
      }
    

    addDep 方法是把表达式依赖添加到 dep 实例中, 可以看到会通过 dep.id 判断是否存在,不存在才添加.

    scheduler.js 部分源码如下:

    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // 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()
            return
          }
          nextTick(flushSchedulerQueue)
        }
      }
    }
    

    watcher 中的 update 会调用 queueWatcher 方法,当 watcher.id 不存在的时候(该watcher尚未添加到队列中)把 watcher 添加到 queue 中。
    调用 update 会开启一个 watcher 缓存队列,在缓存时去除重复数据,减少不必要的计算、渲染.

    • 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。
    • 如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。

    至此,for 循环多次为什么只渲染一次也就有答案了:
    update 的时候只会添加一个 watcher 到事件队列中,而且事件队列会通过调用 nextTick 异步执行

    4. nextTick历史版本问题

    • 6813, out-in transitions

    • 7109, #7153, #7546, #7834, #8109

    5. event loop

    只有熟悉 event loop 的特性才能更好的理解 nextTick,将两者相结合

  • 相关阅读:
    2019年11月4日随堂测试 最多输入字母统计
    写增删改查中间遇到的问题
    2019年12月9日下午自习成果
    2019年12月16日 分级考试
    2019年11月18日 JAVA期中考试 增删改查
    sql语言积累
    【转载】Java项目中常用的异常处理情况总结
    泛型
    C#数字格式化输出
    委托,Lambda的几种用法
  • 原文地址:https://www.cnblogs.com/hi-shepherd/p/12608624.html
Copyright © 2011-2022 走看看