Vue.js 비동기 업데이트 DOM 전략 및 nextTick 인스턴스에 대한 자세한 설명

小云云
풀어 주다: 2018-01-25 11:17:29
원래의
1418명이 탐색했습니다.

이 글은 주로 Vue.js 소스 코드의 비동기 업데이트 DOM 전략과 nextTick을 소개합니다. 관심 있는 친구들이 이를 참고하여 Vue.js를 더 잘 이해하는 데 도움이 되기를 바랍니다.

앞에 작성

저는 Vue.js에 관심이 많고 주로 작업하는 기술 스택이 Vue.js이기 때문에 지난 몇 달 동안 시간을 ​​내어 Vue.js 소스 코드를 공부하고 출력으로 요약을 만들었습니다.

기사 원본 주소: https://github.com/answershuto/learnVue.

학습 과정에서 Vue https://github.com/answershuto/learnVue/tree/master/vue-src에 중국어 댓글이 추가되었습니다. Vue 소스 코드를 배우고 싶은 다른 친구들에게 도움이 되었으면 좋겠습니다. .

이해에 차이가 있을 수 있습니다. 문제를 제기하고 지적하여 함께 배우고 발전하는 것을 환영합니다.

Operation DOM

vue.js를 사용할 때 다음과 같은 특정 비즈니스 시나리오로 인해 DOM을 운영해야 하는 경우가 있습니다.


<template>
 <p>
 <p ref="test">{{test}}</p>
 <button @click="handleClick">tet</button>
 </p>
</template>
로그인 후 복사
로그인 후 복사


export default {
 data () {
  return {
   test: &#39;begin&#39;
  };
 },
 methods () {
  handleClick () {
   this.test = &#39;end&#39;;
   console.log(this.$refs.test.innerText);//打印“begin”
  }
 }
}
로그인 후 복사

인쇄된 결과가 시작됩니다. 분명히 테스트를 "end"로 설정했지만 실제 DOM 노드의 innerText를 얻을 때 예상했던 "end"를 얻지 못하고 이전 값인 "begin"을 얻습니까?

Watcher Queue

질문을 통해 Vue.js 소스 코드의 Watch 구현을 찾았습니다. 특정 반응형 데이터가 변경되면 해당 setter 함수는 클로저에서 Dep에게 알리고 Dep는 관리하는 모든 Watch 개체를 호출합니다. Watch 개체의 업데이트 구현을 트리거합니다. 업데이트 구현을 살펴보겠습니다.


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

Vue.js는 기본적으로 DOM 업데이트의 비동기 실행을 사용한다는 것을 발견했습니다.

업데이트가 비동기적으로 실행되면 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의 소스코드를 보면 Watch 객체가 뷰를 바로 업데이트하지 않고, 이때 queue에 push된 상태인 것을 발견했습니다. Watch 개체는 계속해서 이 큐에 푸시됩니다. 다음 틱을 기다릴 때 이러한 Watch 개체가 순회되어 제거되고 뷰가 업데이트됩니다. 동시에, 중복된 ID를 가진 감시자는 대기열에 여러 번 추가되지 않습니다. 왜냐하면 최종 렌더링 중에는 데이터의 최종 결과에만 신경 쓰면 되기 때문입니다.

그럼 다음 틱은 뭐죠?

nextTick

vue.js는 실제로 위에서 호출된 nextTick인 nextTick 함수를 제공합니다.

nextTick의 구현은 비교적 간단합니다. 실행 목적은 함수를 마이크로태스크나 태스크에 푸시한 다음 현재 스택이 실행된 후 nextTick에 의해 전달된 함수를 실행하는 것입니다. ), 소스 코드를 살펴보세요:


/**
 * 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尾部推入这个函数,等待调用执行。
 */
 if (typeof Promise !== &#39;undefined&#39; && 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&#39;t completely break, but
  // it can get stuck in a weird state where callbacks are pushed into the
  // microtask queue but the queue isn&#39;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 !== &#39;undefined&#39; && (
 isNative(MutationObserver) ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === &#39;[object MutationObserverConstructor]&#39;
 )) {
 // 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, &#39;nextTick&#39;)
  }
  } else if (_resolve) {
  _resolve(ctx)
  }
 })
 if (!pending) {
  pending = true
  timerFunc()
 }
 if (!cb && typeof Promise !== &#39;undefined&#39;) {
  return new Promise((resolve, reject) => {
  _resolve = resolve
  })
 }
 }
})()
로그인 후 복사

queueNextTick 인터페이스를 반환하는 즉시 실행 함수입니다.

수신 cb는 콜백에 푸시되어 저장되고, 그런 다음 타이머Func가 실행됩니다(보류는 타이머Func가 다음 틱 전에 한 번만 실행되도록 보장하는 상태 표시입니다).

timerFunc란 무엇인가요? ㅋㅋㅋ 환경.

여기서 설명하세요. TimerFunc를 가져오는 세 가지 방법이 있습니다: Promise, MutationObserver 및 setTimeout.

Promise를 먼저 사용하고, Promise가 없을 때는 MutationObserver를 사용하세요. 이 두 메소드의 콜백 함수는 setTimeout보다 먼저 실행되므로 먼저 사용됩니다.

환경이 위의 두 가지 메소드를 지원하지 않는 경우 setTimeout이 사용되며 이 함수는 작업 종료 시 푸시되고 호출이 실행될 때까지 기다립니다.

마이크로태스크를 먼저 사용해야 하는 이유는 무엇인가요? Zhihu에 대한 Gu Yiling의 답변에서 배웠습니다.

JS의 이벤트 루프는 실행될 때 작업과 마이크로 작업을 구별합니다. 엔진은 실행을 위해 대기열에서 작업을 가져오기 전에 먼저 실행을 완료합니다. 마이크로태스크 큐.


setTimeout 콜백은 실행을 위해 새 작업에 할당되고 Promise 해석기 및 MutationObserver 콜백은 setTimeout에 의해 생성된 작업보다 먼저 실행될 새 마이크로태스크에서 실행되도록 배열됩니다.


새 마이크로태스크를 생성하려면 먼저 Promise를 사용하세요. 브라우저가 이를 지원하지 않으면 MutationObserver를 사용해 보세요.


정말 작동하지 않습니다. 작업을 생성하려면 setTimeout만 사용할 수 있습니다.


마이크로태스크를 사용하는 이유는 무엇인가요?


HTML 표준에 따르면 각 작업이 실행된 후 UI가 다시 렌더링되고 마이크로 작업에서 데이터 업데이트가 완료되며 현재 작업이 끝나면 최신 UI를 얻을 수 있습니다.


반대로 데이터 업데이트를 위해 새 작업을 생성하면 렌더링이 두 번 수행됩니다.

Gu Yiling의 Zhihu 답변을 참조하세요

첫 번째는 Promise입니다. (Promise.resolve()).then()은 마이크로태스크에 콜백을 추가할 수 있습니다.

MutationObserver는 textNode의 새 DOM 객체를 생성하고 MutationObserver를 사용하여 DOM 그리고 콜백 함수를 지정합니다. DOM이 변경되면 콜백이 트리거됩니다. 즉, textNode.data = String(counter)인 경우 콜백이 추가됩니다.

setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。

综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。

flushSchedulerQueue


/*Github:https://github.com/answershuto*/
/**
 * Flush both queues and run the watchers.
 */
 /*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/
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&#39;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&#39;s watcher run,
 // its watchers can be skipped.
 /*
 给queue排序,这样做可以保证:
 1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
 2.一个组件的user watchers比render watcher先运行,因为user watchers往往比render watcher更早创建
 3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。
 */
 queue.sort((a, b) => a.id - b.id)

 // do not cache length because more watchers might be pushed
 // as we run existing watchers
 /*这里不用index = queue.length;index > 0; index--的方式写是因为不要将length进行缓存,因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/
 for (index = 0; index < queue.length; index++) {
 watcher = queue[index]
 id = watcher.id
 /*将has的标记删除*/
 has[id] = null
 /*执行watcher*/
 watcher.run()
 // in dev build, check and stop circular updates.
 /*
  在测试环境中,检测watch是否在死循环中
  比如这样一种情况
  watch: {
  test () {
   this.test++;
  }
  }
  持续执行了一百次watch代表可能存在死循环
 */
 if (process.env.NODE_ENV !== &#39;production&#39; && has[id] != null) {
  circular[id] = (circular[id] || 0) + 1
  if (circular[id] > MAX_UPDATE_COUNT) {
  warn(
   &#39;You may have an infinite update loop &#39; + (
   watcher.user
    ? `in watcher with expression "${watcher.expression}"`
    : `in a component render function.`
   ),
   watcher.vm
  )
  break
  }
 }
 }

 // keep copies of post queues before resetting state
 /**/
 /*得到队列的拷贝*/
 const activatedQueue = activatedChildren.slice()
 const updatedQueue = queue.slice()

 /*重置调度者的状态*/
 resetSchedulerState()

 // call component updated and activated hooks
 /*使子组件状态都改编成active同时调用activated钩子*/
 callActivatedHooks(activatedQueue)
 /*调用updated钩子*/
 callUpdateHooks(updatedQueue)

 // devtool hook
 /* istanbul ignore if */
 if (devtools && config.devtools) {
 devtools.emit(&#39;flush&#39;)
 }
}
로그인 후 복사

flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图

为什么要异步更新视图

来看一下下面这一段代码


<template>
 <p>
 <p>{{test}}</p>
 </p>
</template>
로그인 후 복사


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

现在有这样的一种情况,created的时候test的值会被++循环执行1000次。

每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。

如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。

所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。
保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。

访问真实DOM节点更新后的数据

所以我们需要在修改data中的数据后访问真实的DOM节点更新后的数据,只需要这样,我们把文章第一个例子进行修改。


<template>
 <p>
 <p ref="test">{{test}}</p>
 <button @click="handleClick">tet</button>
 </p>
</template>
로그인 후 복사
로그인 후 복사


export default {
 data () {
  return {
   test: &#39;begin&#39;
  };
 },
 methods () {
  handleClick () {
   this.test = &#39;end&#39;;
   this.$nextTick(() => {
    console.log(this.$refs.test.innerText);//打印"end"
   });
   console.log(this.$refs.test.innerText);//打印“begin”
  }
 }
}
로그인 후 복사

使用Vue.js的global API的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。

相关推荐:

jQuery中DOM节点操作方法总结

DOM简介及节点、属性、查找节点

几种jQuery查找dom的方法


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

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