>  기사  >  웹 프론트엔드  >  Vue 소스 코드의 일괄 비동기 업데이트 및 nextTick 원칙 분석

Vue 소스 코드의 일괄 비동기 업데이트 및 nextTick 원칙 분석

不言
不言원래의
2018-07-20 11:53:502465검색

이 글에서는 Vue 소스 코드의 일괄 비동기 업데이트 분석과 nextTick 원칙을 소개합니다. 이는 도움이 필요한 친구들이 참고할 수 있습니다.

vue는 이미 국내 프론트엔드 웹엔드의 3분의 1을 차지하고 있고, 일상적으로 사용하는 주요 기술 스택 중 하나이기도 합니다. 게다가 수많은 vue 소스 코드가 있는 이유도 궁금합니다. 최근 커뮤니티에 이런 글이 올라왔습니다. 이런 글의 경우, 이번 기회에 여러분의 글과 토론에서 몇 가지 자양분을 끌어내는 동시에 소스 코드를 읽을 때 제가 생각한 몇 가지 내용을 요약하여 다음과 같은 글을 작성하겠습니다. 내 생각 요약

Target Vue 버전: 2.5 .17-beta.02.5.17-beta.0

vue源码注释:https://github.com/SHERlocked93/vue-analysis

声明:文章中源码的语法都使用 Flow,并且源码根据需要都有删节(为了不被迷糊 @_@),如果要看完整版的请进入上面的github地址,本文是系列文章,文章地址见底部~

1. 异步更新

我们在依赖收集原理的响应式化方法 defineReactive 中的 setter 访问器中有派发更新 dep.notify() 方法,这个方法会挨个通知在 depsubs 中收集的订阅自己变动的watchers执行update。一起来看看 update 方法的实现:

// src/core/observer/watcher.js

/* Subscriber接口,当依赖发生改变的时候进行回调 */
update() {
  if (this.computed) {
    // 一个computed watcher有两种模式:activated lazy(默认)
    // 只有当它被至少一个订阅者依赖时才置activated,这通常是另一个计算属性或组件的render function
    if (this.dep.subs.length === 0) {       // 如果没人订阅这个计算属性的变化
      // lazy时,我们希望它只在必要时执行计算,所以我们只是简单地将观察者标记为dirty
      // 当计算属性被访问时,实际的计算在this.evaluate()中执行
      this.dirty = true
    } else {
      // activated模式下,我们希望主动执行计算,但只有当值确实发生变化时才通知我们的订阅者
      this.getAndInvoke(() => {
        this.dep.notify()     // 通知渲染watcher重新渲染,通知依赖自己的所有watcher执行update
      })
    }
  } else if (this.sync) {      // 同步
    this.run()
  } else {
    queueWatcher(this)        // 异步推送到调度者观察者队列中,下一个tick时调用
  }
}

如果不是 computed watcher 也非 sync 会把调用update的当前watcher推送到调度者队列中,下一个tick时调用,看看 queueWatcher

// src/core/observer/scheduler.js

/* 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则
 * 该watcher将被跳过,除非它是在队列正被flush时推送
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {     // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验
    has[id] = true
    queue.push(watcher)      // 如果没有正在flush,直接push到队列中
    if (!waiting) {          // 标记是否已传给nextTick
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

/* 重置调度者状态 */
function resetSchedulerState () {
  queue.length = 0
  has = {}
  waiting = false
}

这里使用了一个 has 的哈希map用来检查是否当前watcher的id是否存在,若已存在则跳过,不存在则就push到 queue 队列中并标记哈希表has,用于下次检验,防止重复添加。这就是一个去重的过程,比每次查重都要去queue中找要文明,在渲染的时候就不会重复 patch 相同watcher的变化,这样就算同步修改了一百次视图中用到的data,异步 patch 的时候也只会更新最后一次修改。

这里的 waiting 方法是用来标记 flushSchedulerQueue 是否已经传递给 nextTick 的标记位,如果已经传递则只push到队列中不传递 flushSchedulerQueuenextTick,等到 resetSchedulerState 重置调度者状态的时候 waiting 会被置回 false 允许 flushSchedulerQueue 被传递给下一个tick的回调,总之保证了 flushSchedulerQueue 回调在一个tick内只允许被传入一次。来看看被传递给 nextTick 的回调 flushSchedulerQueue 做了什么:

// src/core/observer/scheduler.js

/* nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)                    // 排序

  for (index = 0; index  MAX_UPDATE_COUNT) {     // 持续执行了一百次watch代表可能存在死循环
        warn()                                  // 进入死循环的警告
        break
      }
    }
  }
  resetSchedulerState()           // 重置调度者状态
  callActivatedHooks()            // 使子组件状态都置成active同时调用activated钩子
  callUpdatedHooks()              // 调用updated钩子
}

nextTick 方法中执行 flushSchedulerQueue 方法,这个方法挨个执行 queue 中的watcher的 run 方法。我们看到在首先有个 queue.sort() 方法把队列中的watcher按id从小到大排了个序,这样做可以保证:

  1. 组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。

  2. 一个组件的user watchers(侦听器watcher)比render watcher先运行,因为user watchers往往比render watcher更早创建

  3. 如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过

在挨个执行队列中的for循环中,index 这里没有将length进行缓存,因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue。

那么数据的修改从model层反映到view的过程:数据更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新视图

2. nextTick原理

2.1 宏任务/微任务

这里就来看看包含着每个watcher执行的方法被作为回调传入 nextTick 之后,nextTick 对这个方法做了什么。不过首先要了解一下浏览器中的 EventLoopmacro taskmicro task

vue 소스 코드 댓글: https://github.com/SHERlocked93/vue-analytic

Vue 소스 코드의 일괄 비동기 업데이트 및 nextTick 원칙 분석진술: 글의 소스코드 구문은 Flow를 사용하였으며, 소스코드는 필요에 따라 축약하였습니다. (혼란을 피하기 위해@_@), 풀버전을 보시려면 위의 github 주소를 입력해 주세요. 일련의 기사는 하단의 기사 주소를 참조하세요~

1. 비동기 업데이트

우리는 응답형 메서드 defineReactive의 원칙에 따라 <code>setter 접근자를 사용합니다. 에는 디스패치 업데이트 dep.notify() 메서드가 있습니다. 이 메서드는 depsubs에 수집된 감시자에게 하나씩 알립니다. 자신의 변경 사항을 구독하는 code>는 업데이트를 실행합니다. update 메소드의 구현을 살펴보겠습니다.

// src/core/util/next-tick.js

const callbacks = []     // 存放异步执行的回调
let pending = false      // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送

/* 挨个同步执行callbacks中回调 */
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i  {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  MessageChannel.toString() === '[object MessageChannelConstructor]'  // PhantomJS
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc      // fallback to macro
}
계산된 감시자 또는 sync가 아닌 경우 호출하는 현재 감시자 업데이트는 스케줄러에 푸시됩니다. 대기열에서는 다음 틱에서 호출됩니다. queueWatcher를 살펴보세요.
    // src/core/util/next-tick.js
    
    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)
        }
      })
      if (!pending) {
        pending = true
        if (useMacroTask) {
          macroTimerFunc()
        } else {
          microTimerFunc()
        }
      }
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    /* 强制使用macrotask的方法 */
    export function withMacroTask(fn: Function): Function {
      return fn._withTask || (fn._withTask = function() {
        useMacroTask = true
        const res = fn.apply(null, arguments)
        useMacroTask = false
        return res
      })
    }
  1. 여기에서는 has의 해시 맵이 사용됩니다. 현재 감시자의 ID가 존재하는지 확인하려면, 존재하지 않으면 건너뛰고, 존재하지 않으면 queue 대기열에 푸시하고 해시 테이블에 사용됩니다. 반복적인 추가를 방지하기 위해 다음 검사를 실시합니다. 이는 중복을 확인하기 위해 매번 대기열로 이동하는 것보다 더 문명화된 프로세스입니다. 이러한 방식으로 동일한 감시자에 대한 패치 변경이 반복되지 않습니다. , 동시에 100개의 수정이 이루어지더라도 보조 보기에 사용된 데이터는 비동기 패치 중에 마지막 수정으로만 업데이트됩니다.

    여기서 waiting 메소드는 flushSchedulerQueuenextTick의 태그 비트에 전달되었는지 여부를 표시하는 데 사용됩니다. flushSchedulerQueuenextTick에 전달하지 않으면 resetSchedulerStatewaiting이 다시 로 설정됩니다. /code>는 스케줄러 상태를 재설정합니다. >falseflushSchedulerQueue가 다음 틱의 콜백으로 전달되도록 허용합니다. 즉, flushSchedulerQueue 콜백을 보장합니다. 틱당 한 번만 전달될 수 있습니다. nextTick에 전달된 콜백 flushSchedulerQueue가 수행하는 작업을 살펴보겠습니다.
  2. <p>
      <span>{{ name }}</span>
      <button>change name</button>
      </p><p></p>
    
    <script>
      new Vue({
        el: &#39;#app&#39;,
        data() {
          return {
            name: &#39;SHERlocked93&#39;
          }
        },
        methods: {
          change() {
            const $name = this.$refs.name
            this.$nextTick(() => console.log(&#39;setter前:&#39; + $name.innerHTML))
            this.name = &#39; name改喽 &#39;
            console.log(&#39;同步方式:&#39; + this.$refs.name.innerHTML)
            setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
            this.$nextTick(() => console.log(&#39;setter后:&#39; + $name.innerHTML))
            this.$nextTick().then(() => console.log(&#39;Promise方式:&#39; + $name.innerHTML))
          }
        }
      })
    </script>
  3. nextTick 메서드 > 메서드에서 flushSchedulerQueue를 실행합니다. 이 메소드는 queue에 있는 watcher의 run 메소드를 하나씩 실행합니다. 먼저 대기열에 있는 감시자를 ID별로 작은 것부터 큰 것 순으로 정렬하는 queue.sort() 메서드가 있다는 것을 알 수 있습니다. 이를 통해 다음이 보장됩니다.

  4. 구성 요소 업데이트 순서는 상위 구성 요소가 항상 하위 구성 요소보다 먼저 생성되므로 상위 구성 요소에서 하위 구성 요소로의 순서입니다.

    🎜🎜사용자 감시자는 렌더링 감시자보다 먼저 생성되는 경우가 많기 때문에 구성 요소의 사용자 감시자(리스너 감시자)는 렌더링 감시자보다 먼저 실행됩니다.🎜🎜🎜🎜상위 구성 요소 감시자가 실행되는 동안 구성 요소가 삭제되면 감시자 실행이 be skiped🎜🎜
🎜실행 큐의 for 루프에서 하나씩 index 실행 중에 기존 watcher 객체가 처리되기 때문에 여기에 길이가 캐시되지 않습니다. 이 기간 동안 더 많은 감시자 개체가 대기열에 푸시될 수 있습니다. 🎜🎜그런 다음 모델 레이어에서 뷰에 반영되는 데이터 수정 과정: <code>Data Change-> Dep -> nextTick -> code>🎜 <h2>2. nextTick의 원리</h2> <h3>2.1 매크로 작업/마이크로 작업</h3>🎜여기에서는 각 감시자 실행을 포함하고 <code>nextTick에 전달되는 메서드를 살펴봅니다. 콜백 > 그 후 nextTick은 이 메서드를 사용하여 작업을 수행합니다. 하지만 먼저 브라우저의 EventLoop, 매크로 태스크, 마이크로 태스크의 개념을 이해해야 합니다. JS와 Node를 참조할 수 있습니다. js의 이벤트 루프에 대한 이 기사에서는 메인 스레드에서 후자 둘 사이의 실행 관계를 보여주는 그림이 있습니다. 동기화 작업: 🎜🎜🎜🎜 엔진은 먼저 매크로태스크 대기열에서 첫 번째 작업을 꺼냅니다. 실행이 완료된 후 마이크로태스크 대기열의 모든 작업을 꺼내어 모두 순서대로 실행합니다. 실행이 완료된 후 매크로태스크 대기열에서 첫 번째 작업을 다시 꺼냅니다. 🎜🎜🎜🎜 두 대기열의 모든 작업이 반복됩니다. 꺼냈다. 🎜

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

  • macro task :同步代码、setImmediateMessageChannelsetTimeout/setInterval

  • micro taskPromise.thenMutationObserver

有的文章把 micro task 叫微任务,macro task 叫宏任务,因为这两个单词拼写太像了 -。- ,所以后面的注释多用中文表示~

先来看看源码中对 micro task macro task 的实现: macroTimerFuncmicroTimerFunc

// src/core/util/next-tick.js

const callbacks = []     // 存放异步执行的回调
let pending = false      // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送

/* 挨个同步执行callbacks中回调 */
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i  {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  MessageChannel.toString() === '[object MessageChannelConstructor]'  // PhantomJS
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc      // fallback to macro
}

flushCallbacks 这个方法就是挨个同步的去执行callbacks中的回调函数们,callbacks中的回调函数是在调用 nextTick 的时候添加进去的;那么怎么去使用 micro taskmacro task 去执行 flushCallbacks 呢,这里他们的实现 macroTimerFuncmicroTimerFunc 使用浏览器中宏任务/微任务的API对flushCallbacks 方法进行了一层包装。比如宏任务方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) },这样在触发宏任务执行的时候 macroTimerFunc() 就可以在浏览器中的下一个宏任务loop的时候消费这些保存在callbacks数组中的回调了,微任务同理。同时也可以看出传给 nextTick 的异步回调函数是被压成了一个同步任务在一个tick执行完的,而不是开启多个异步任务。

注意这里有个比较难理解的地方,第一次调用 nextTick 的时候 pending 为false,此时已经push到浏览器event loop中一个宏任务或微任务的task,如果在没有flush掉的情况下继续往callbacks里面添加,那么在执行这个占位queue的时候会执行之后添加的回调,所以 macroTimerFuncmicroTimerFunc 相当于task queue的占位,以后 pending 为true则继续往占位queue里面添加,event loop轮到这个task queue的时候将一并执行。执行 flushCallbackspending 置false,允许下一轮执行 nextTick 时往event loop占位。

可以看到上面 macroTimerFuncmicroTimerFunc 进行了在不同浏览器兼容性下的平稳退化,或者说降级策略

  1. macroTimerFuncsetImmediate -> MessageChannel -> setTimeout 。首先检测是否原生支持 setImmediate ,这个方法只在 IE、Edge 浏览器中原生实现,然后检测是否支持 MessageChannel,如果对 MessageChannel 不了解可以参考一下这篇文章,还不支持的话最后使用 setTimeout
    为什么优先使用 setImmediate MessageChannel 而不直接使用 setTimeout 呢,是因为HTML5规定setTimeout执行的最小延时为4ms,而嵌套的timeout表现为10ms,为了尽可能快的让回调执行,没有最小延时限制的前两者显然要优于 setTimeout

  2. microTimerFuncPromise.then -> macroTimerFunc 。首先检查是否支持 Promise,如果支持的话通过 Promise.then 来调用 flushCallbacks 方法,否则退化为 macroTimerFunc
    vue2.5之后 nextTick 中因为兼容性原因删除了微任务平稳退化的 MutationObserver 的方式。

2.2 nextTick实现

最后来看看我们平常用到的 nextTick 方法到底是如何实现的:

// src/core/util/next-tick.js

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)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

/* 强制使用macrotask的方法 */
export function withMacroTask(fn: Function): Function {
  return fn._withTask || (fn._withTask = function() {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

nextTick 在这里分为三个部分,我们一起来看一下;

  1. 首先 nextTick 把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中,这么做是因为防止单个 cb 如果执行错误不至于让整个JS线程挂掉,每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。

  2. 然后检查 pending 状态,这个跟之前介绍的 queueWatcher 中的 waiting 是一个意思,它是一个标记位,一开始是 false 在进入 macroTimerFuncmicroTimerFunc 方法前被置为 true,因此下次调用 nextTick 就不会进入 macroTimerFuncmicroTimerFunc 方法,这两个方法中会在下一个 macro/micro tick 时候 flushCallbacks 异步的去执行callbacks队列中收集的任务,而 flushCallbacks 方法在执行一开始会把 pendingfalse,因此下一次调用 nextTick 时候又能开启新一轮的 macroTimerFuncmicroTimerFunc,这样就形成了vue中的 event loop

  3. 最后检查是否传入了 cb,因为 nextTick 还支持Promise化的调用:nextTick().then(() => {}),所以如果没有传入 cb 就直接return了一个Promise实例,并且把resolve传递给_resolve,这样后者执行的时候就跳到我们调用的时候传递进 then 的方法中。

Vue源码中 next-tick.js 文件还有一段重要的注释,这里就翻译一下:

在vue2.5之前的版本中,nextTick基本上基于 micro task 来实现的,但是在某些情况下 micro task 具有太高的优先级,并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。但是如果全部都改成 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。vue2.5之后版本提供的解决办法是默认使用 micro task,但在需要时(例如在v-on附加的事件处理程序中)强制使用 macro task

为什么默认优先使用 micro task 呢,是利用其高优先级的特性,保证队列中的微任务在一次循环全部执行完毕。

强制 macro task 的方法是在绑定 DOM 事件的时候,默认会给回调的 handler 函数调用 withMacroTask 方法做一层包装 handler = withMacroTask(handler),它保证整个回调函数执行过程中,遇到数据状态的改变,这些改变都会被推到 macro task 中。以上实现在 src/platforms/web/runtime/modules/events.js 的 add 方法中,可以自己看一看具体代码。

刚好在写这篇文章的时候思否上有人问了个问题 vue 2.4 和2.5 版本的@input事件不一样 ,这个问题的原因也是因为2.5之前版本的DOM事件采用 micro task ,而之后采用 macro task,解决的途径参考 中介绍的几个办法,这里就提供一个在mounted钩子中用 addEventListener 添加原生事件的方法来实现,参见 CodePen。

3. 一个例子

说这么多,不如来个例子,执行参见 CodePen

<p>
  <span>{{ name }}</span>
  <button>change name</button>
  </p><p></p>

<script>
  new Vue({
    el: &#39;#app&#39;,
    data() {
      return {
        name: &#39;SHERlocked93&#39;
      }
    },
    methods: {
      change() {
        const $name = this.$refs.name
        this.$nextTick(() => console.log(&#39;setter前:&#39; + $name.innerHTML))
        this.name = &#39; name改喽 &#39;
        console.log(&#39;同步方式:&#39; + this.$refs.name.innerHTML)
        setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
        this.$nextTick(() => console.log(&#39;setter后:&#39; + $name.innerHTML))
        this.$nextTick().then(() => console.log(&#39;Promise方式:&#39; + $name.innerHTML))
      }
    }
  })
</script>

执行以下看看结果:

同步方式:SHERlocked93 
setter前:SHERlocked93 
setter后:name改喽 
Promise方式:name改喽 
setTimeout方式:name改喽

为什么是这样的结果呢,解释一下:

  1. 同步方式: 当把data中的name修改之后,此时会触发name的 setter 中的 dep.notify 通知依赖本data的render watcher去 updateupdate 会把 flushSchedulerQueue 函数传递给 nextTick,render watcher在 flushSchedulerQueue 函数运行时 watcher.run 再走 diff -> patch 那一套重渲染 re-render 视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch 到视图上,所以获取视图上的DOM元素还是原来的内容。

  2. Setter 이전: 원래 콘텐츠가 Setter 이전에 인쇄되는 이유는 콜백을 Push할 때 nextTick이 호출되기 때문입니다. 콜백은 하나씩 배열한 다음 for 루프를 하나씩 실행하므로 대기열과 비슷한 개념으로 이름을 수정한 후 렌더링 감시자가 실행됩니다. schedulerQueue 대기열을 채우고 실행 함수 flushSchedulerQueuenextTick에 전달합니다. 이때 이미 pre-setter 함수를 콜백 대기열에 넣습니다. code>. 이 <code>cbsetter 사전 함수 다음에 콜백 대기열로 푸시되기 때문에 첫 번째 콜백에서 콜백을 실행할 때 in, First-out 기준으로 가 먼저 실행됩니다. 이때, Render Watcher의 <code>watcher.run은 실행되지 않으므로 인쇄됩니다. DOM 요소는 여전히 원본 콘텐츠입니다. nextTick 在被调用的时候把回调挨个push进callbacks数组,之后执行的时候也是 for 循环出来挨个执行,所以是类似于队列这样一个概念,先入先出;在修改name之后,触发把render watcher填入 schedulerQueue 队列并把他的执行函数 flushSchedulerQueue 传递给 nextTick ,此时callbacks队列中已经有了 setter前函数 了,因为这个 cb 是在 setter前函数 之后被push进callbacks队列的,那么先入先出的执行callbacks中回调的时候先执行 setter前函数,这时并未执行render watcher的 watcher.run,所以打印DOM元素仍然是原来的内容。

  3. setter后: setter后这时已经执行完 flushSchedulerQueue,这时render watcher已经把改动 patch 到视图上,所以此时获取DOM是改过之后的内容。

  4. Promise方式: 相当于 Promise.then 的方式执行这个函数,此时DOM已经更改。

  5. setTimeout方式: 最后执行macro task的任务,此时DOM已经更改。

注意,在执行 setter前函数 这个异步任务之前,同步的代码已经执行完毕,异步的任务都还未执行,所有的 $nextTick 函数也执行完毕,所有回调都被push进了callbacks队列中等待执行,所以在setter前函数 执行的时候,此时callbacks队列是这样的:[setter前函数flushSchedulerQueuesetter后函数Promise方式函数],它是一个micro task队列,执行完毕之后执行macro task setTimeout,所以打印出上面的结果。

另外,如果浏览器的宏任务队列里面有setImmediateMessageChannelsetTimeout/setInterval 各种类型的任务,那么会按照上面的顺序挨个按照添加进event loop中的顺序执行,所以如果浏览器支持MessageChannelnextTick 执行的是 macroTimerFunc,那么如果 macrotask queue 中同时有 nextTick 添加的任务和用户自己添加的 setTimeout 类型的任务,会优先执行 nextTick 中的任务,因为MessageChannel 的优先级比 setTimeout的高,setImmediate


Setter 이후:

Setter 이후 flushSchedulerQueue가 실행되었고, 렌더링 감시자가 패치하여 이때 얻은 DOM이 수정된 콘텐츠입니다.

Promise 메소드:

은 이 함수를 실행하는 Promise.then과 동일합니다. DOM이 변경되었습니다. setTimeout 메소드: #🎜🎜# 마지막으로 DOM이 변경되면 매크로 작업을 실행합니다. #🎜🎜##🎜🎜##🎜🎜#주의할 점은 pre-setter 함수의 비동기 작업을 실행하기 전에 동기 코드가 실행되었고 아직 비동기 작업이 실행되지 않았다는 점입니다. 모든 $nextTick 함수도 실행되었고 모든 콜백은 실행을 기다리기 위해 콜백 큐에 푸시되었으므로 pre-setter 함수가 실행될 때입니다. , 콜백 대기열은 다음과 같습니다: [setter 전 함수, flushSchedulerQueue, setter 후 함수, Promise 모드 함수 ], 이는 마이크로 작업 대기열이 완료 후 매크로 작업 setTimeout을 실행하므로 위의 결과가 인쇄됩니다. #🎜🎜##🎜🎜#또한 브라우저의 매크로 작업 대기열에 다양한 유형의 setImmediate, MessageChannel, setTimeout/setInterval이 포함되어 있는 경우 위의 순서대로 이벤트 루프에 추가된 순서대로 작업이 하나씩 실행되므로 브라우저가 MessageChannel를 지원하는 경우 nextTickmacroTimerFunc를 실행합니다. >, nextTick에 의해 추가된 작업과 사용자가 매크로태스크 대기열에 추가한 setTimeout 유형의 작업이 있는 경우 nextTick이 실행됩니다. MessageChannel의 첫 번째 작업은 setTimeout보다 우선순위가 높으며 setImmediate에도 동일하게 적용됩니다. #🎜🎜##🎜🎜#관련 추천: #🎜🎜##🎜🎜##🎜🎜##🎜🎜#Vue에서 mixin 사용 방법 분석#🎜🎜##🎜🎜##🎜🎜##🎜🎜 # #🎜🎜##🎜🎜##🎜🎜#Vue2.0 사용자 정의 지시어와 인스턴스 속성 및 메서드 #🎜🎜##🎜🎜##🎜🎜#

위 내용은 Vue 소스 코드의 일괄 비동기 업데이트 및 nextTick 원칙 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.