Vue의 DOM 비동기 업데이트 전략과 nextTick 메커니즘에 대한 자세한 설명

小云云
풀어 주다: 2018-02-22 11:15:25
원래의
5165명이 탐색했습니다.

이 기사에서는 주로 VueDOM 비동기 업데이트 전략과 nextTick 메커니즘에 대한 분석을 공유합니다. 독자는 특정 지식이 필요합니다. Vue JavaScript 이벤트 루프 모델에 대한 경험과 친숙함을 사용합니다. 그것이 모두에게 도움이 되기를 바랍니다. Vue中的DOM异步更新策略和nextTick机制的解析,需要读者有一定的Vue使用经验并且熟悉掌握JavaScript事件循环模型。希望能帮助到大家。

引入:DOM的异步更新

<template>
  <p>
    <p ref="test">{{test}}</p>
    <button @click="handleClick">tet</button>
  </p>
</template>
로그인 후 복사
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText);//打印“begin”
        }
    }
}
로그인 후 복사

打印的结果是begin而不是我们设置的end。这个结果足以说明VueDOM的更新并非同步。

Vue官方文档中是这样说明的:

可能你还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。

简而言之,就是在一个事件循环中发生的所有数据改变都会在下一个事件循环的Tick中来触发视图更新,这也是一个“批处理”的过程。(注意下一个事件循环的Tick有可能是在当前的Tick微任务执行阶段执行,也可能是在下一个Tick执行,主要取决于nextTick函数到底是使用Promise/MutationObserver还是setTimeout

Watcher队列

Watcher的源码中,我们发现watcherupdate其实是异步的。(注:sync属性默认为false,也就是异步)

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

queueWatcher(this)函数的代码如下:

 /*将一个观察者对象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 {
        ...
        }
        // queue the flush
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}
로그인 후 복사

这段源码有几个需要注意的地方:

  1. 首先需要知道的是watcher执行update的时候,默认情况下肯定是异步的,它会做以下的两件事:

  • 判断has数组中是否有这个watcherid

  • 如果有的话是不需要把watcher加入queue中的,否则不做任何处理。

  • 这里面的nextTick(flushSchedulerQueue)中,flushScheduleQueue函数的作用主要是执行视图更新的操作,它会把queue中所有的watcher取出来并执行相应的视图更新。

  • 核心其实是nextTick函数了,下面我们具体看一下nextTick到底有什么用。

  • nextTick

    nextTick函数其实做了两件事情,一是生成一个timerFunc,把回调作为microTaskmacroTask参与到事件循环中来。二是把回调函数放入一个callbacks队列,等待适当的时机执行。(这个时机和timerFunc不同的实现有关)

    首先我们先来看它是怎么生成一个timerFunc把回调作为microTaskmacroTask的。

    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)
        }
    }
    로그인 후 복사

    值得注意的是,它会按照PromiseMutationObserversetTimeout优先级去调用传入的回调函数。前两者会生成一个microTask任务,而后者会生成一个macroTask。(微任务和宏任务)

    之所以会设置这样的优先级,主要是考虑到浏览器之间的兼容性(IE没有内置Promise)。另外,设置Promise最优先是因为Promise.resolve().then回调函数属于一个微任务,浏览器在一个Tick中执行完macroTask后会清空当前Tick所有的microTask再进行UI渲染,把DOM更新的操作放在Tick执行microTask的阶段来完成,相比使用setTimeout生成的一个macroTask会少一次UI的渲染。

    nextTickHandler函数,其实才是我们真正要执行的函数。

    function nextTickHandler () {
        pending = false
        /*执行所有callback*/
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
            copies[i]()
        }
    }
    로그인 후 복사
    로그인 후 복사

    这里的callbacks变量供nextTickHandler消费。而前面我们所说的nextTick函数第二点功能中“等待适当的时机执行”,其实就是因为timerFunc的实现方式有差异,如果是PromiseMutationObservernextTickHandler回调是一个microTask,它会在当前Tick的末尾来执行。如果是setTiemoutnextTickHandler回调是一个macroTask,它会在下一个Tick

    소개: DOM의 비동기 업데이트

    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
            })
        }
    }
    로그인 후 복사
     /*将一个观察者对象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 {
          ...
        }
        // queue the flush
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    로그인 후 복사
    로그인 후 복사
    인쇄된 결과는 우리가 설정한 end 대신 begin입니다. 이 결과는 VueDOM 업데이트가 동기적이지 않다는 것을 보여주기에 충분합니다. 🎜🎜이 내용은 Vue 공식 문서에 설명되어 있습니다. 🎜
    아직 눈치 채지 못했을 수도 있지만 VueDOM 업데이트를 비동기적으로 수행합니다. 데이터 변경 사항이 관찰되자마자 Vue는 대기열을 열고 동일한 이벤트 루프에서 발생하는 모든 데이터 변경 사항을 버퍼링합니다. 동일한 감시자가 여러 번 트리거되면 대기열에 한 번만 푸시됩니다. 버퍼링 중 이러한 중복 제거는 불필요한 계산과 DOM 작업을 방지하는 데 중요합니다. 그런 다음 다음 이벤트 루프 "tick"에서 Vue는 큐를 플러시하고 실제(중복 제거된) 작업을 수행합니다.
    🎜간단히 말하면, 하나의 이벤트 루프에서 발생하는 모든 데이터 변경은 다음 이벤트 루프의 Tick에서 보기 업데이트를 트리거합니다. 이는 또한 "일괄 처리" 프로세스입니다. (이벤트 루프의 다음 Tick은 현재 Tick 마이크로태스크 실행 단계에서 실행될 수도 있고, 다음 Tick에서 실행될 수도 있습니다. >. 이는 주로 nextTick 함수가 Promise/MutationObserver 또는 setTimeout)🎜

    Watcher 대기열

    🎜을 사용하는지에 따라 달라집니다. Watcher의 소스 코드에서 watcher업데이트가 실제로 비동기적임을 확인했습니다. (참고: sync 속성의 기본값은 비동기인 false입니다.) 🎜
    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)
    }
    로그인 후 복사
    로그인 후 복사
    🎜queueWatcher(this) 함수의 코드는 다음과 같습니다. : 🎜
    <p id="example">
        <p ref="test">{{test}}</p>
        <button @click="handleClick">tet</button>
    </p>
    로그인 후 복사
    로그인 후 복사
    🎜this 소스 코드에서 주의해야 할 사항이 여러 가지 있습니다. 🎜
    1. 🎜 가장 먼저 알아야 할 것은 watcher code>는 <code>update 시간을 실행하며 기본적으로 확실히 비동기식이며 다음 두 가지 작업을 수행합니다. 🎜
    • 🎜 판사는 배열을 가지고 있습니다. 이 감시자에 대한 id🎜
    • 🎜가 있습니까? 그렇다면 추가할 필요가 없습니다. 감시자에 추가하세요. 그렇지 않으면 처리가 수행되지 않습니다. 🎜
  • 🎜nextTick(flushSchedulerQueue)에서 flushScheduleQueue의 기능은 주로 뷰 업데이트 작업을 수행하는 것입니다. queue의 모든 watcher를 확인하고 해당 뷰 업데이트를 수행합니다. 🎜
  • 🎜핵심은 실제로 nextTick 함수입니다. nextTick이 어떤 용도로 사용되는지 자세히 살펴보겠습니다. 🎜
  • nextTick

    🎜nextTick 함수는 실제로 두 가지 작업을 수행합니다. 하나는 timerFunc를 생성하고 콜백을 다음과 같이 사용하는 것입니다. microTask 또는 macroTask는 이벤트 루프에 참여합니다. 두 번째는 콜백 함수를 콜백 대기열에 넣고 적절한 실행 시간을 기다리는 것입니다. (이 타이밍은 timerFunc의 다양한 구현과 관련이 있습니다.) 🎜🎜먼저 timerFunc를 생성하고 콜백을 microTask로 사용하는 방법을 살펴보겠습니다. code> 또는 macroTask. 🎜
    var vm = new Vue({
        el: '#example',
        data: {
            test: 'begin',
        },
        methods: {
            handleClick() {
                this.test = 'end';
                console.log('1')
                setTimeout(() => { // macroTask
                    console.log('3')
                }, 0);
                Promise.resolve().then(function() { //microTask
                    console.log('promise!')
                })
                this.$nextTick(function () {
                    console.log('2')
                })
            }
        }
    })
    로그인 후 복사
    로그인 후 복사
    🎜Promise, MutationObserversetTimeout의 우선순위에 따라 수신 콜백 함수를 호출한다는 점은 주목할 가치가 있습니다. 전자 두 개는 microTask 작업을 생성하고 후자는 macroTask를 생성합니다. (마이크로태스크 및 매크로태스크) 🎜🎜이런 우선순위를 설정하는 이유는 주로 브라우저 간의 호환성을 고려하기 위함입니다(IE에는 Promise가 내장되어 있지 않습니다). 또한 Promise.resolve().then 콜백 함수가 마이크로태스크에 속하고 브라우저가 Tick에서 macroTask를 실행한 후 현재 Tick의 모든 microTask가 지워지고 UI가 렌더링됩니다. TickmicroTask를 실행하는 단계에 DOM 업데이트 작업을 배치합니다. setTimeout macroTaskUI 렌더링을 한 번 줄입니다. 🎜🎜 nextTickHandler 함수는 실제로 우리가 실제로 실행하고 싶은 함수입니다. 🎜
    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
            })
        }
    }
    로그인 후 복사
    로그인 후 복사
    🎜여기의 callbacks 변수는 nextTickHandler에서 사용하기 위한 것입니다. 앞서 언급한 nextTick 함수의 두 번째 함수인 "적절한 실행 시간을 기다리는 중"은 실제로는 timerFunc 구현의 차이 때문입니다. PromiseMutationObservernextTickHandler 콜백은 microTask이며, 현재 Tick이 끝날 때 실행됩니다. setTiemout인 경우 nextTickHandler 콜백은 다음 Tick에서 실행되는 macroTask입니다. 🎜

    还有就是callbacks中的成员是如何被push进来的?从源码中我们可以知道,nextTick是一个自执行的函数,一旦执行是return了一个queueNextTick,所以我们在调用nextTick其实就是在调用queueNextTick这个函数。它的源代码如下:

    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
            })
        }
    }
    로그인 후 복사

    可以看到,一旦调用nextTick函数时候,传入的function就会被存放到callbacks闭包中,然后这个callbacksnextTickHandler消费,而nextTickHandler的执行时间又是由timerFunc来决定。

    我们再回来看Watcher中的一段代码:

     /*将一个观察者对象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 {
          ...
        }
        // queue the flush
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    로그인 후 복사
    로그인 후 복사

    这里面的nextTick(flushSchedulerQueue)中的flushSchedulerQueue函数其实就是watcher的视图更新。每次调用的时候会把它pushcallbacks中来异步执行。

    另外,关于waiting变量,这是很重要的一个标志位,它保证flushSchedulerQueue回调只允许被置入callbacks一次。

    所以,也就是说DOM确实是异步更新,但是具体是在下一个Tick更新还是在当前Tick执行microTask的时候更新,具体要看nextTcik的实现方式,也就是具体跑的是Promise/MutationObserver还是setTimeout

    附:nextTick源码带注释),有兴趣可以观摩一下。

    这里面使用PromisesetTimeout来执行异步任务的方式都很好理解,比较巧妙的地方是利用MutationObserver执行异步任务。MutationObserverH5的新特性,它能够监听指定范围内的DOM变化并执行其回调,它的回调会被当作microTask来执行,具体参考MDN,。

    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)
    }
    로그인 후 복사
    로그인 후 복사

    可以看到,通过借用MutationObserver来创建一个microTasknextTickHandler作为回调传入MutationObserver中。  
    这里面创建了一个textNode作为观测的对象,当timerFunc执行的时候,textNode.data会发生改变,便会触发MutatinObservers的回调函数,而这个函数才是我们真正要执行的任务,它是一个microTask

    注:2.5+版本的VuenextTick进行了修改,具体参考下面“版本升级”一节。

    关于Vue暴露的全局nextTick

    继续来看下面的这段代码:

    <p id="example">
        <p ref="test">{{test}}</p>
        <button @click="handleClick">tet</button>
    </p>
    로그인 후 복사
    로그인 후 복사
    var vm = new Vue({
        el: '#example',
        data: {
            test: 'begin',
        },
        methods: {
            handleClick() {
                this.test = 'end';
                console.log('1')
                setTimeout(() => { // macroTask
                    console.log('3')
                }, 0);
                Promise.resolve().then(function() { //microTask
                    console.log('promise!')
                })
                this.$nextTick(function () {
                    console.log('2')
                })
            }
        }
    })
    로그인 후 복사
    로그인 후 복사

    Chrome下,这段代码执行的顺序的1、2、promise、3

    可能有同学会以为这是1、promise、2、3,其实是忽略了一个标志位pending

    我们回到nextTick函数returnqueueNextTick可以发现:

    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
            })
        }
    }
    로그인 후 복사
    로그인 후 복사

    这里面通过对pending的判断来检测是否已经有timerFunc这个函数在事件循环的任务队列等待被执行。如果存在的话,那么是不会再重复执行的。

    最后异步执行nextTickHandler时又会把pending置为false

    function nextTickHandler () {
        pending = false
        /*执行所有callback*/
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
            copies[i]()
        }
    }
    로그인 후 복사
    로그인 후 복사

    所以回到我们的例子:

    handleClick() {
        this.test = &#39;end&#39;;
        console.log(&#39;1&#39;)
        setTimeout(() => { // macroTask
            console.log('3')
        }, 0);
        Promise.resolve().then(function() { //microTask
            console.log('promise!')
        })
        this.$nextTick(function () {
            console.log('2')
        })
    }
    로그인 후 복사

    代码中,this.test = 'end'必然会触发watcher进行视图的重新渲染,而我们在文章的Watcher一节中就已经有提到会调用nextTick函数,一开始pending变量肯定就是false,因此它会被修改为true并且执行timerFunc。之后执行this.$nextTick其实还是调用的nextTick函数,只不过此时的pendingtrue说明timerFunc已经被生成,所以this.$nextTick(fn)只是把传入的fn置入callbacks之中。此时的callbacks有两个function成员,一个是flushSchedulerQueue,另外一个就是this.$nextTick()的回调。

    因此,上面这段代码中,在Chrome下,有一个macroTask和两个microTask。一个macroTask就是setTimeout,两个microTask:分别是VuetimerFunc(其中先后执行flushSchedulerQueuefunction() {console.log('2')})、代码中的Promise.resolve().then()

    版本升级带来的变化

    上面讨论的nextTick实现是2.4版本以下的实现,2.5以上版本对于nextTick的内部实现进行了大量的修改。

    独立文件

    首先是从Vue 2.5+开始,抽出来了一个单独的文件next-tick.js来执行它。

    microTask与macroTask

    在代码中,有这么一段注释:

    Vue의 DOM 비동기 업데이트 전략과 nextTick 메커니즘에 대한 자세한 설명

    其大概的意思就是:在Vue 2.4之前的版本中,nextTick几乎都是基于microTask实现的(具体可以看文章nextTick一节),但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask

    具体做法就是,在Vue执行绑定的DOM事件时,默认会给回调的handler函数调用withMacroTask方法做一层包装,它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM更新)的任务都会被推到macroTask

    function add$1 (event, handler, once$$1, capture, passive) {
        handler = withMacroTask(handler);
        if (once$$1) { handler = createOnceHandler(handler, event, capture); }
        target$1.addEventListener(
            event,
            handler,
            supportsPassive
            ? { capture: capture, passive: passive }
            : capture
        );
    }
    
    /**
     * Wrap a function so that if any code inside triggers state change,
     * the changes are queued using a Task instead of a MicroTask.
     */
    function withMacroTask (fn) {
        return fn._withTask || (fn._withTask = function () {
            useMacroTask = true;
            var res = fn.apply(null, arguments);
            useMacroTask = false;
            return res
        })
    }
    로그인 후 복사

    而对于macroTask的执行,Vue优先检测是否支持原生setImmediate(高版本IE和Edge支持),不支持的话再去检测是否支持原生MessageChannel,如果还不支持的话为setTimeout(fn, 0)

    最后,写一段demo来测试一下:

    <p id="example">
        <span>{{test}}</span>
        <button @click="handleClick">change</button>
    </p>
    로그인 후 복사
    var vm = new Vue({
        el: '#example',
        data: {
            test: 'begin',
        },
        methods: {
            handleClick: function() {
                this.test = end;
                console.log('script')
                this.$nextTick(function () { 
                    console.log('nextTick')
                })
                Promise.resolve().then(function () {
                    console.log('promise')
                })
            }
        }
    })
    로그인 후 복사

    Vue 2.5+中,这段代码的输出顺序是scriptpromisenextTick,而Vue 2.4输出scriptnextTickpromisenextTick执行顺序的差异正好说明了上面的改变。

    MessageChannel

    Vue 2.4版本以前使用的MutationObserver来模拟异步任务。而Vue 2.5版本以后,由于兼容性弃用了MutationObserver

    Vue 2.5+版本使用了MessageChannel来模拟macroTask。除了IE以外,messageChannel的兼容性还是比较可观的。

    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = () => {
    port.postMessage(1)
    }
    로그인 후 복사

    可见,新建一个MessageChannel对象,该对象通过port1来检测信息,port2发送信息。通过port2的主动postMessage来触发port1onmessage事件,进而把回调函数flushCallbacks作为macroTask参与事件循环。

    MessageChannel VS setTimeout

    为什么要优先MessageChannel创建macroTask而不是setTimeout

    HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。

    Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而MessageChannel的延迟明显是小于setTimeout的。对比传送门

    为什么要异步更新视图

    看下面的代码:

    <template>
      <p>
        <p>{{test}}</p>
      </p>
    </template>
    로그인 후 복사
    export default {
        data () {
            return {
                test: 0
            };
        },
        mounted () {
          for(let i = 0; i < 1000; i++) {
            this.test++;
          }
        }
    }
    로그인 후 복사

    现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次<code>++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue实现了一个queue队列,在下一个Tick(或者是当前Tick的微任务阶段)的时候会统一执行queueWatcherrun。同时,拥有相同idWatcher不会被重复加入到该queue中去,所以不会执行1000Watcherrun。最终更新视图只会直接将test对应的DOM0变成1000。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个Tick(或者是当前Tick的微任务阶段)的时候调用,大大优化了性能。

    应用场景

    在操作DOM节点无效的时候,就要考虑操作的实际DOM节点是否存在,或者相应的DOM是否被更新完毕。

    比如说,在created钩子中涉及DOM节点的操作肯定是无效的,因为此时还没有完成相关DOM的挂载。解决的方法就是在nextTick函数中去处理DOM,这样才能保证DOM被成功挂载而有效操作。

    还有就是在数据变化之后要执行某个操作,而这个操作需要使用随数据改变而改变的DOM时,这个操作应该放进Vue.nextTick

    之前在做慕课网音乐webApp的时候关于播放器内核的开发就涉及到了这个问题。下面我把问题简化:

    现在我们要实现一个需求是点击按钮变换audio标签的src属性来实现切换歌曲。

    <p id="example">
        <audio ref="audio"
               :src="url"></audio>
        <span ref="url"></span>
        <button @click="changeUrl">click me</button>
    </p>
    로그인 후 복사
    const musicList = [
        'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112003137.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
        'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112002493.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
        'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112004168.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3'
    ];
    var vm = new Vue({
        el: '#example',
        data: {
            index: 0,
            url: ''
        },
        methods: {
            changeUrl() {
                this.index = (this.index + 1) % musicList.length
                this.url = musicList[this.index];
                this.$refs.audio.play();
            }
        }
    });
    로그인 후 복사

    毫无悬念,这样肯定是会报错的:

    Uncaught (in promise) DOMException: The element has no supported sources.
    로그인 후 복사

    原因就在于audio.play()是同步的,而这个时候DOM更新是异步的,src属性还没有被更新,结果播放的时候src属性为空,就报错了。

    解决办法就是在play的操作加上this.$nextTick()

    this.$nextTick(function() {
        this.$refs.audio.play();
    });
    로그인 후 복사

    위 내용은 Vue의 DOM 비동기 업데이트 전략과 nextTick 메커니즘에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    관련 라벨:
    원천:php.cn
    본 웹사이트의 성명
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
    인기 튜토리얼
    더>
    최신 다운로드
    더>
    웹 효과
    웹사이트 소스 코드
    웹사이트 자료
    프론트엔드 템플릿