zoukankan      html  css  js  c++  java
  • Vue之nextTick原理

    前言

    我们都知道vue是数据驱动视图,而vue中视图更新是异步的。在业务开发中,有没有经历过当改变了数据,视图却没有按照我们的期望渲染?而需要将对应的操作放在nextTick中视图才能按照预期的渲染,有的时候nextTick也不能生效,而需要利用setTimeout来解决?

    搞清楚这些问题,那么就需要搞明白以下几个问题:
    1、vue中到底是如何来实现异步更新视图;
    2、vue为什么要异步更新视图;
    3、nextTick的原理;
    4、nextTick如何来解决数据改变视图不更新的问题的;
    5、nextTick的使用场景。

    以下分享我的思考过程。

    Vue中的异步更新DOM

    Vue中的视图渲染思想

    vue中每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

    如果对vue视图渲染的思想还不是很清楚,可以参考这篇defineProperty实现视图渲染用defineProty模拟的Vue的渲染视图,来了解整个视图渲染的思想。

    Vue异步渲染思想和意义

    但是Vue的视图渲染是异步的,异步的过程是数据改变不会立即更新视图,当数据全部修改完,最后再统一进行视图渲染。

    在渲染的过程中,中间有一个对虚拟dom进行差异化的计算过程(diff算法),大量的修改带来频繁的虚拟dom差异化计算,从而导致渲染性能降低,异步渲染正是对视图渲染性能的优化。

    Vue异步渲染视图的原理

    • 依赖数据改变就会触发对应的watcher对象中的update
     /**
       * 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)
        }
      }
    • 默认的调用queueWatcher将watcher对象加入到一个队列中
    /**
     * Push a watcher into the watcher queue.
     * Jobs with duplicate IDs will be skipped unless it's
     * pushed when the queue is being flushed.
     */
    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
          nextTick(flushSchedulerQueue)
        }
      }
    }
    

    当第一次依赖有变化就会调用nextTick方法,将更新视图的回调设置成微任务或宏任务,然后后面依赖更新对应的watcher对象都只是被加入到队列中,只有当nextTick回调执行之后,才会遍历调用队列中的watcher对象中的更新方法更新视图。

    这个nextTick和我们在业务中调用的this.$nextTick()是同一个函数。

    if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }

    flushSchedulerQueue刷新队列的函数,用于更新视图

    function flushSchedulerQueue () {
      flushing = true
      let watcher, id
    
      // Sort queue before flush.
      // This ensures that:
      // 1. Components are updated from parent to child. (because parent is always
      //    created before the child)
      // 2. A component's user watchers are run before its render watcher (because
      //    user watchers are created before the render watcher)
      // 3. If a component is destroyed during a parent component's watcher run,
      //    its watchers can be skipped.
      queue.sort((a, b) => a.id - b.id)
    
      // do not cache length because more watchers might be pushed
      // as we run existing watchers
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        id = watcher.id
        has[id] = null
        watcher.run()
        // in dev build, check and stop circular updates.
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
          circular[id] = (circular[id] || 0) + 1
          if (circular[id] > MAX_UPDATE_COUNT) {
            warn(
              'You may have an infinite update loop ' + (
                watcher.user
                  ? `in watcher with expression "${watcher.expression}"`
                  : `in a component render function.`
              ),
              watcher.vm
            )
            break
          }
        }
      }

    那么nextTick到底是个什么东西呢?

    nextTick的原理

    vue 2.5中nextTick的源码如下(也可以跳过源码直接看后面的demo,来理解nextTick的用处):

    /**
     * Defer a task to execute it asynchronously.
     */
    export const nextTick = (function () {
      const callbacks = []
      let pending = false
      let timerFunc
    
      function nextTickHandler () {
        pending = false
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
          copies[i]()
        }
      }
    
      // An asynchronous deferring mechanism.
      // In pre 2.4, we used to use microtasks (Promise/MutationObserver)
      // but microtasks actually has too high a priority and fires in between
      // supposedly sequential events (e.g. #4521, #6690) or even between
      // bubbling of the same event (#6566). Technically setImmediate should be
      // the ideal choice, but it's not available everywhere; and the only polyfill
      // that consistently queues the callback after all DOM events triggered in the
      // same loop is by using MessageChannel.
      /* istanbul ignore if */
      if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
        timerFunc = () => {
          setImmediate(nextTickHandler)
        }
      } else if (typeof MessageChannel !== 'undefined' && (
        isNative(MessageChannel) ||
        // Phantomjs
        MessageChannel.toString() === '[object MessageChannelConstructor]'
      )) {
        const channel = new MessageChannel()
        const port = channel.port2
        channel.port1.onmessage = nextTickHandler
        timerFunc = () => {
          port.postMessage(1)
        }
      } else
      /* istanbul ignore next */
      if (typeof Promise !== 'undefined' && isNative(Promise)) {
        // use microtask in non-DOM environments, e.g. Weex
        const p = Promise.resolve()
        timerFunc = () => {
          p.then(nextTickHandler)
        }
      } else {
        // fallback to setTimeout
        timerFunc = () => {
          setTimeout(nextTickHandler, 0)
        }
      }
    
      return function queueNextTick (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)
          }
        })
        if (!pending) {
          pending = true
          timerFunc()
        }
        // $flow-disable-line
        if (!cb && typeof Promise !== 'undefined') {
          return new Promise((resolve, reject) => {
            _resolve = resolve
          })
        }
      }
    })()

    用下面这个demo来感受依赖更新时和nextTick的关系以及nextTick的用处:

     function isNative(Ctor) {
         return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
     }
    
     const nextTick = (function () {
         let pending = false;
         let callbacks = []
         let timerFunc
    
         function nextTickHandler() {
             pending = false
             const copies = callbacks.slice(0)
             callbacks.length = 0
             for (let i = 0; i < copies.length; i++) {
                 copies[i]()
             }
         }
    
         if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
             timerFunc = () => {
                 setImmediate(nextTickHandler)
             }
         } else if (typeof MessageChannel !== 'undefined' && (
                 isNative(MessageChannel) ||
                 // Phantomjs
                 MessageChannel.toString() === '[object MessageChannelConstructor]'
             )) {
             const channel = new MessageChannel()
             const port = channel.port2
             channel.port1.onmessage = nextTickHandler
             timerFunc = () => {
                 port.postMessage(1)
             }
         } else
             /* istanbul ignore next */
             if (typeof Promise !== 'undefined' && isNative(Promise)) {
                 // use microtask in non-DOM environments, e.g. Weex
                 const p = Promise.resolve()
                 timerFunc = () => {
                     p.then(nextTickHandler)
                 }
             } else {
                 // fallback to setTimeout
                 timerFunc = () => {
                     setTimeout(nextTickHandler, 0)
                 }
             }
    
         console.log('timerFunc:', timerFunc)
         return function queueNextTick(cb, ctx) {
             callbacks.push(() => {
               if (cb) {
                 cb.call(ctx)
                }
             })
             // console.log('callbacks:', callbacks)
             if (!pending) {
                 pending = true
                 console.log('pending...', true)
                 timerFunc()
             }
         }
     })()
    
     //  模拟异步视图更新
     // 第一次先将对应新值添加到一个数组中,然后调用一次nextTick,将读取数据的回调作为nextTick的参数
     // 后面的新值直接添加到数组中
     console.time()
     let arr = []
     arr.push(99999999)
     nextTick(() => {
         
         console.log('nextTick one:', arr, arr.length)
     })
    
     function add(len) {
         for (let i = 0; i < len; i++) {
             arr.push(i)
             console.log('i:', i)
         }
     }
    
     add(4)
     //  console.timeEnd()
     //  add()
     //  add()
     nextTick(() => {
         arr.push(888888)
         console.log('nextTick two:', arr, arr.length)
     })
     add(8)的值之后
     console.timeEnd()

    在chrome运行结果如下:

    可以看到第二个nextTick中push的值最后渲染在add(8)的值之后,这也就是nextTick的作用了,nextTick的作用就是用来处理需要在数据更新(在vue中手动调用nextTick时对应的是dom更新完成后)完才执行的操作。

    nextTick的原理:
    首先nextTick会将外部传进的函数回调存在内部数组中,nextTick内部有一个用来遍历这个内部数组的函数nextTickHandler,而这个函数的执行是异步的,什么时候执行取决于这个函数是属于什么类型的异步任务:微任务or宏任务。

    主线程执行完,就会去任务队列中取任务到主线程中执行,任务队列中包含了微任务和宏任务,首先会取微任务,微任务执行完就会取宏任务执行,依此循环。nextTickHandler设置成微任务或宏任务就能保证其总是在数据修改完或者dom更新完然后再执行。(js执行机制可以看promise时序问题&js执行机制)

    为什么vue中对设置函数nextTickHandler的异步任务类型会有如下几种判断?

    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
             timerFunc = () => {
                 setImmediate(nextTickHandler)
             }
         } else if (typeof MessageChannel !== 'undefined' && (
                 isNative(MessageChannel) ||
                 // PhantomJS
                 MessageChannel.toString() === '[object MessageChannelConstructor]'
             )) {
             const channel = new MessageChannel()
             const port = channel.port2
             channel.port1.onmessage = nextTickHandler
             timerFunc = () => {
                 port.postMessage(1)
             }
         } else
             /* istanbul ignore next */
             if (typeof Promise !== 'undefined' && isNative(Promise)) {
                 // use microtask in non-DOM environments, e.g. Weex
                 const p = Promise.resolve()
                 timerFunc = () => {
                     p.then(nextTickHandler)
                 }
             } else {
                 // fallback to setTimeout
                 timerFunc = () => {
                     setTimeout(nextTickHandler, 0)
                 }
             }

    浏览器环境中常见的异步任务种类,按照优先级:

    • macro task:同步代码、setImmediate、MessageChannel、setTimeout/setInterval
    • micro task:Promise.then、MutationObserver

    而为什么最后才判断使用setTimeout?
    vue中目的就是要尽可能的快地执行回调渲染视图,而setTimeout有最小延迟限制:如果嵌套深度超过5级,setTimeout(回调,0)就会有4ms的延迟。

    所以首先选用执行更快的setImmediate,但是setImmediate有兼容性问题,目前只支持Edge、Ie浏览器:

    可以用同样执行比setTimeout更快的宏任务MessageChannel来代替setImmediate。MessageChannel兼容性如下:

    当以上都不支持的时候,就使用new Promise().then(),将回调设置成微任务,Promise不支持才使用setTimeout。

    资源搜索网站大全 https://www.renrenfan.com.cn 广州VI设计公司https://www.houdianzi.com

    总结:

    nextTick就是利用了js机制执行任务的规则,将nextTick的回调函数设置成宏任务或微任务来达到在主线程的操作执行完,再执行的目的。

    在vue中主要提供对依赖Dom更新完成后再做操作的情况的支持

     

    nextTick的使用场景

    当改变数据,视图没有按预期渲染时;都应该考虑是否是因为本需要在dom执行完再执行,然而实际却在dom没有执行完就执行了代码,如果是就考虑使用将逻辑放到nextTick中,有的时候业务操作复杂,有些操作可能需要更晚一些执行,放在nextTick中仍然没有达到预期效果,这个时候可以考虑使用setTimeout,将逻辑放到宏任务中。

    基于以上分析,可以列举几个nextTick常用到的使用场景:

    • 在created、mounted等钩子函数中使用时。
    • 对dom进行操作时,例如:使用$ref读取元素时
            // input 定位
            scrollToInputBottom() {
                this.$nextTick(() => {
                    this.$refs.accept_buddy_left.scrollTop =
                        this.$refs.accept_buddy_left.scrollTop + 135
                    this.$refs.accept_buddy_ipt[
                        this.$refs.accept_buddy_ipt.length - 1
                    ].$refs.ipt.focus()
                })
            },
    • 计算页面元素高度时:
            // 监听来自 url 的期数变化,跳到该期数
            urlInfoTerm: {
                immediate: true,
                handler(val) {
                    
                    if (val !== 0) {
                        this.$nextTick(function() {
                            //     计算期数所在位置的高度
                            this.setCellsHeight()
                            //设置滚动距离
                            this.spaceLenght = this.getColumnPositionIndex(
                                this.list,
                            )
                            setTimeout(() => {
                                this.setScrollPosition(val)
                            }, 800)
                        })
                    }
                },
  • 相关阅读:
    weblogic12c 2021.4.20 季度补丁 SPB
    一顿debug猛如虎,原来内存OOM
    JDK记录一下
    213. 打家劫舍 II-动态规划-中等
    5526. 最多可达成的换楼请求数目-回溯-困难
    1584. 连接所有点的最小费用-图/最小生成树-中等
    Java-泛型的限制
    Java-泛型-桥方法
    889. 根据前序和后序遍历构造二叉树-树-中等
    1109. 航班预订统计-差分数组-中等
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/14092492.html
Copyright © 2011-2022 走看看