11

Vue中有个API是nextTick,官方文档是这样介绍作用的:

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

理解这部分内容,有助于理解Vue对页面的渲染过程,同时也可以了解到beforeUpdateupdated的使用。另外就是通过了解nextTick的调用了解vue内部是如何使用Promise的。这部分内容和之前介绍计算属性的内容也有关联,可以比照着看。

首先看一下我创建的例子:

    <!-- HTML 部分 -->
    <div id="test">
      <p>{{ name }}的年龄是{{ age }}</p>
      <!-- <p>
        {{ info }}
      </p> -->
      <div>体重<input type="text" v-model="age" /></div>
      <button @click="setAge">设置年龄为100</button>
    </div>
      // js 部分
      new Vue({
        el: '#test',
        data() {
          return {
            name: 'tuanzi',
            age: 2
          }
        },
        beforeUpdate() {
          console.log('before update')
          debugger
        },
        updated() {
          console.log('updated')
          debugger
        },
        methods: {
          setAge() {
            this.age = 190
            debugger
            this.$nextTick(() => {
              console.log('next tick', this.age)
              debugger
            })
          }
        }
      })

当页面渲染完成,点击按钮触发事件之后,都会发生什么呢~~

直接介绍计算属性的时候说过,当页面初次加载渲染,会调用模板中的值,这时会触发该值的getter设置。所以对于我们这里,data中的nameage都会订阅updateComponent这个方法,这里我们看下这个函数的定义:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

简而言之,这时用来渲染页面的,所以当代码执行到this.age = 190,这里就会触发agesetter属性,该属性会调用dep.notify方法:

  // 通知
  notify() {
    // stabilize the subscriber list first
    // 浅拷贝订阅列表
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order

      // 关闭异步,则subs不在调度中排序
      // 为了保证他们能正确的执行,现在就带他们进行排序
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

这里的this.subs就是页面初始化过程中,age这个属性收集到的依赖关系,也就是renderWatcher实例。接着调用renderWatcherupdate方法。

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    // debugger
    /* istanbul ignore else */
    if (this.lazy) {
      // 执行 computedWacher 会运行到这里
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 运行 renderWatcher
      queueWatcher(this)
    }
  }

那为了更好的理解这里,我把renderWatcher的实例化的代码也贴出来:

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate')
        }
      }
    },
    true /* isRenderWatcher */
  )

因此,renderWatcher是没有设置lazy这个属性的,同时我也没有手动设置sync属性,因此代码会执行到queueWatcher(this)。注意这里的this,当前属于renderWatcher实例对象,因此这里传递的this就是该对象。

// 将一个watcher实例推入队列准备执行
// 如果队列中存在相同的watcher则跳过这个watcher
// 除非队列正在刷新
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  debugger
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // 没有在刷新队列,则推入新的watcher实例
      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.

      // 队列已经刷新,则用传入的watcher实例的id和队列中的id比较,按大小顺序插入队列
      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
      }
      debugger
      nextTick(flushSchedulerQueue)
    }
  }
}

这段代码比较简单,就说一点。代码里有个判断是config.async,这是Vue私有对象上的值,默认的是true,因此代码会执行到nextTick这里,此时会传入一个回调函数flushSchedulerQueue,我们这里先不说,之后用的的时候再介绍。现在看看nextTick的实现。

const callbacks = []
let pending = false

export function nextTick(cb?: Function, ctx?: Object) {
  debugger
  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 => {
      _resolve = resolve
    })
  }
}

pendding用来判断是否存在等待的队列,callbacks是执行回调的队列。那对于此时此刻,就是向callbacks推入一个回调函数,其中要执行的部分就是flushSchedulerQueue。因为是初次调用这个函数,这里的就会调用到timerFunc

  let timerFunc
  
  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)
  }

现在毫无因为的是timerFunc这个函数会被调用。但是有个问题,p.then(flushCallbacks)这句话会执行么?来看个例子:

function callback() {
    console.log('callback')
}

let p = Promise.resolve()
function func() {
    p.then(callback)
}

console.log('this is start')

func()

console.log('this is pre promise 1')

let a = 1
console.log('this is pre promise 2')
console.log(a)

思考一下结果是什么吧。看看和答案是否一致:

说回上面,p.then(flushCallbacks)这句话在这里会执行,但是是将flushCallbacks这个方法推入了微任务队列,要等其他的同步代码执行完成,执行栈空了之后才会调用。所以对于renderWatcher来说,目前就算执行完了。

接下来代码执行到这里:

this.$nextTick(() => {
  console.log('next tick', this.age)
  debugger
})

看下$nextTick的定义:

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

这里定义$nextTick是定义在Vue的原型对象上,所以在页面中可以通过this.$nextTick调用,同时传入的this就是当前页的实例。所以看会nextTick定义的部分,唯一的区别是,这是的penddingfalse,因此不会再调用一次timerFunc

setAge里的同步代码都执行完了,因此就轮到flushCallbacks出场。来看下定义:

function flushCallbacks() {
  debugger
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  console.log(copies)
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

这里定义的位置和定义nextTick是在同一个文件里,因此penddingcallbacks是共享的。主要就看copies[i]()这一段。经过前面的执行,此时callbacks.length的值应该是2。copies[1]指的就是先前推进队列的flushSchedulerQueue

/**
 * Flush both queues and run the watchers.
 *
 * 刷新队列并且运行watcher
 */
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  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.

  // 给刷新队列排序,原因如下:
  // 1. 组件的更新是从父组件开始,子组件结束
  // 2. 组件的 userWatcher 的运行总是先于 renderWatcher
  // 3. 如果父组件的watcher运行期间,子组件被销毁了,后续运行可以跳过被销毁的子组件
  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]
    if (watcher.before) {
      watcher.before()
    }
    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
      }
    }
  }

watcher.before这个方法是存在的,先前的代码中有,在初始化renderWatcher时传入了这个参数。这里就调用了callHook(vm, 'beforeUpdate'),所以能看出来,此时beforeUpdate执行了。接着执行watcher.run()runWatcher类上定义的一个方法。

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    debugger
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

this.active初始化的值就是true,get方法之前的文章也提到过,这里再贴一遍代码:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    // debugger
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

这部分代码之前说过,这里就不再说了,只提一点,此时的this.getter执行的是updateComponent,其实也就是里面定义的vm._update(vm._render(), hydrating)。关于renderupdate我会在分析虚拟dom时介绍。

现在需要知道的是,页面此时会重新渲染,我在setAge方法中修改了age的值,当vm._update执行完,就会发现页面上的值变化了。那接着就执行callbacks中的下一个值,也就是我写在$nextTick中的回调函数,这个就很简单,没必要再说。点击按钮到现在新的页面渲染完成,执行的结果就是:

before update
updated
next tick 100

这里就把整个流程讲完了,但是我想到vue文档中说的:

在修改数据之后立即使用它,然后等待 DOM 更新

假设我现在要是把$nextTick放到修改值之前呢。把setAge修改一下。

  setAge() {
    this.$nextTick(() => {
      console.log('next tick', this.age)
      debugger
    })
    debugger
    this.age = 100
  }

思考一下,此时点击按钮,页面会打印出什么东西。按照逻辑,因为$nextTick写在了前面,因此会被先推进callbacks中,也就会被第一个执行。所以此时我以为打印出来的age还是2。但我既然都这样说了,那结果肯定是和我以为的不一样,但我有一部分想的没错,就是优先推入,优先调用。当我忘了一点,大家也可以会想一下,renderWatcher是如何被触发的?

$nextTick回调现在是进入了微任务队列,所以会继续执行接下来的赋值。此时会触发age设置的setter里的dep.notify。但在调用之前,新的值就已经传给age了。所以当$nextTick里的回调执行时,会触发agegetter,拿到的值就是新的值。

整个nextTick事件就介绍完了。


Aria
962 声望49 粉丝