vue this.$nextTick核心原理剖析

使用vue编写商城页面时,添加和修改使用同一模态框,点击修改按钮对话框打开并将数据赋值,点击添加按钮打开对话框,编写代码如下:

		handleAdd(){
            this.dialogFormVisible = true
            this.$nextTick(() => {
                this.$refs['form'].resetFields()
                this.addrForm.id = null
            })
            console.log(444,this.addrForm);
        },
        handleUpdate(addrInfo){
            this.handleAdd()
            this.addrForm = Object.assign({},addrInfo)
            console.log(22222,this.addrForm);
        },

然而上述代码并不符合预期结果,先点击修改按钮对话框表单有值再点击添加按钮打开对话框表单依旧有值,这是因为上述代码的执行顺序并不是我们所想的,首先了解一下$nextTick。
vue降级策略
优先选择微任务microtask(promise和mutationObserver),不支持的情况下,才不得不降级选用宏任务macrotask(setImmediate, MessageChannel, setTimeout)。
看一个例子


    

{{message}}

vue this.$nextTick核心原理剖析_第1张图片
如上图所示,执行顺序是 this.message、 this.$nextTick()、第一个promise、第二个promise、setTimeout()
第四步优先第二步输出了 444 和 p标签,从这里我们可以看出,chrome浏览器环境下 nextTick内部是执行了微任务的,所以优先setTimeout输出了。
同样是微任务,第三步的promise晚于第四步输出,这跟我们改变数据触发watcher更新的先后有关。nextTick中的callback啥时候执行,取决于数据是在什么时候发生了改变。

首先,当响应式数据发生变化时,会触发它的setter,从而通知Dep去调用相关的watch对象,从而触发watch的update函数进行视图更新。

update () {
    /* istanbul ignore else */
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        /*同步则执行run直接渲染视图*/
        this.run()
    } else {
        /*异步推送到观察者队列中,下一个tick时调用。*/
        queueWatcher(this)
    }
}

update调用了一个queueWatcher方法

 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
  /*获取watcher的id*/
  const id = watcher.id
  /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      /*如果没有flush掉,直接push到队列中即可*/
      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 >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

可以看出,queueWatcher方法内部主要做的就是将watcher push到了queue队列当中。同时当waiting为false时,调用了一次 nextTick方法, 同时传入了一个参数 flushSchedulerQueue,其实这个参数是具体的队列更新函数,也就是说更新dom操作就是在这里面做的。而这个waiting状态的作用,很明显是为了保证nextTick(flushSchedulerQueue)只会执行一次。后续再通过this.xxx改变数据,只会加入将相关的watcher加入到队列中,而不会再次执行nextTick(flushSchedulerQueue)。

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.
  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
      }
    }
  }
/**
 1. Defer a task to execute it asynchronously.
 */
 /*
    延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
    这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
    目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
  /*存放异步执行的回调*/
  const callbacks = []
  /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
  let pending = false
  /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
  let timerFunc

  /*下一个tick时的回调*/
  function nextTickHandler () {
    /*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
    pending = false
    /*执行所有callback*/
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }

  // 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 if */

  /*
    这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
    优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。
    如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
    参考:https://www.zhihu.com/question/55364497
  */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    /*使用Promise*/
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 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)
    }
  } else if (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 IE11, iOS7, Android 4.4
    /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    /*使用setTimeout将回调推入任务队列尾部*/
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  /*
    推送到队列中下一个tick时执行
    cb 回调函数
    ctx 上下文
  */
  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    /*cb存到callbacks中*/
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

在这个函数内,我们可以看到

1.nextTick是一个立即执行函数,也就是说这个函数在定义的时候就已经自动执行一次了,而自动执行时,return function queueNextTick前面的代码也已经执行了。这也是nextTick第一次执行
2.定义了一个函数timerFunc,这是个关键函数,因为这个函数是怎样的,决定了我们的nextTick内部最终是执行了微任务,还是执行了宏任务。(定义nextTick函数时就定义了)
3.定义了一个nextTickHandler函数,作用是执行我们调用nextTick时,所传进来的callback回调函数,也就是说当执行this.$nextTick()时,内部传递进来的这个函数,就是在nextTickHandler内被执行的。(定义nextTick函数时就定义了))

4.return了一个函数queueNextTick,当平常调用this.$nextTick(cb)时以及上面调用nextTick(flushSchedulerQueue),实际上调用了这个queueNextTick

这个时候,继续看queueNextTick,作用:
1.将传入进来的callback回调函数,push到了callbacks数组中,为后面nextTickHandler函数执行callback做准备
2.当pending为false时,调用了timerFunc函数

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    /*使用Promise*/
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 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)
    }
  } else if (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 IE11, iOS7, Android 4.4
    /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    /*使用setTimeout将回调推入任务队列尾部*/
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

可以看出,timerFunc内部定义了一些异步函数,视当前执行环境的不同,timerFunc内部执行的异步函数不同,他内部可能是promise, 可能是mutationObserver, 可能是setTimeout。(当前例子是在chrome浏览器下,timerFunc内部是Promise无疑)。但可以看出,不管内部是什么异步函数,它都在异步的回调中执行了nextTickHandler,而nextTickHandler是决定我们调用this.$nextTick(() => {})时,内部回调函数啥时候执行的关键。

故可以得出结论,timerFunc内部的异步函数的回调啥时候执行,this.$nextTick()内的回调就啥时候执行

梳理代码流程

mounted() {
     // 第一步
     this.message = 'aaa'

     // 第二步
     setTimeout(() => {
         console.log('222')
     })

     // 第三步
     Promise.resolve().then((res) => {
         console.log('333')
     })

     // 第四步
     this.$nextTick(() => {
         console.log('444')
         console.log(this.$refs.dom)
     })

     // 第五步
     Promise.resolve().then((res) => {
         console.log('555')
     })
 }
  1. this.message = ‘aaa’ 执行,响应式数据发生变化,触发setter, 从而进一步触发watcher的update方法
  2. update方法内执行了queueWatcher函数,将相关watcher push到queue队列中。并执行了nextTick(flushSchedulerQueue) 。此时,这里是第一次执行了nextTick方法。此时,代码中的this.$nextTick()还并没有执行,只执行了this.message = ‘aaa’ , 但是vue内部自动执行了一次nextTick方法,并将flushSchedulerQueue当作参数传入了
  3. nexTick内部代码执行,实际上是执行了queueNextTick,传入了一个flushSchedulerQueue函数,将这个函数加入到了callbacks数组中,此时数组中只有一个cb函数flushSchedulerQueue。
  4. pending状态初始为false,故执行了timerFunc,
  5. timerFunc一旦执行,发现内部是一个promise异步回调,就加入到微任务队列了,这是微任务队列中的第一个任务啊。但注意,此时,callbacks内的回调函数还并没有执行,要等这个微任务执行的时候,callbcaks内的回调函数才会执行
  6. 碰到setTimeout,加入宏任务队列,
  7. 碰到第一个Promise, 加入微任务队列,此时微任务队列中就有两个微任务。我们现在加入的这个是第二个
  8. 此时this.$nextTick()函数执行,相当于就是调用了queueNextTick,并传入了一个回调函数。此时注意了
function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    /*cb存到callbacks中*/
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }

之前,我们执行过一次queueNextTick,那么pending状态就变为true了,那么timerFunc这个时候不会再执行了,而此时唯一做的操作就是将传入的回调函数加入到了callbacks数组当中。
所以,实际timerFunc这个函数的执行,是在this.message = ‘aaa’ 执行的时候调用的,也就意味着,timerFunc内的异步回调, 是在 this.message = ‘aaa’ 时被加入到了微任务队列当中,而不是this.$nextTick()执行时被加入到微任务队列的。所以这也就是为什么说pending状态是解决问题的关键,因为他决定了,异步回调啥时候加入到微任务队列

而this.$nextTick(cb)执行时,唯一的作用就是将cb回调函数加入到了callbacks数组当中,那么在微任务队列被执行的时候,去调用callbacks中的回调函数时,就会调用到现在加入的这个回调函数啊
9. 继续,碰到第二个promise(console.log(555)的这个),又加入到微任务队列中。
10. 此时,微任务队列中存在3个任务,第一个是timerFunc中的promise回调,第二个是console.log(333)的那个promise回调,第三个是console.log(555)的那个promise回调。
11. 故,同步代码执行完成后,优先清空微任务队列,先执行了第一个微任务,也就是timeFunc内的那个微任务
而这个微任务一执行,就调用了nextTickHandler, nextTickHandler是不是就依次执行了callbacks中的回调函数啊,此时callbacks中有两个回调函数,第一个就是flushSchedulerQueue,用于更新dom,第二个就是我们传进来的这个

 // 第四步
     this.$nextTick(() => {
         console.log('444')
         console.log(this.$refs.dom)
     })

所以,我们第二个回调函数执行时,dom已经更新了。然后才输出 444 和 p标签
12. 然后再取出第二个微任务去执行,就输出了333
13. 再取出第三个微任务去执行,就输出了555
14. 再之后,微任务队列清空,开始下一轮循环,取出宏任务队列中的setTimeout的回调并执行,输出222。

通过上述例子分析,调用顺序已经明了,因此,将我们的代码稍作修改

handleAdd(){
            this.dialogFormVisible = true
            this.$nextTick(() => {
                this.$refs['form'].resetFields()
                this.addrForm.id = null
            })
            console.log(444,this.addrForm);
        },
        handleUpdate(addrInfo){
            this.handleAdd()
            setTimeout(() => {
              this.addrForm = Object.assign({},addrInfo)
              console.log(22222,this.addrForm);
            })  
        },

通过以上修改,实现了先清空表单再进行赋值的操作,使原始的逻辑能够实现。

你可能感兴趣的:(vue.js,javascript,前端)