首页 > 其他分享 >Vue.nextTick的使用的原理

Vue.nextTick的使用的原理

时间:2022-08-15 11:45:01浏览次数:56  
标签:nextTick Vue DOM 回调 callbacks 原理 执行

我们都知道JS有一个处理事件的机制 也就是事件循环机制

以及同步和异步

事件循环的顺序,决定js代码的执行顺序。事件循环如下

在执行完微任务之后才会去进行下一个宏任务

vue中关于dom实际上也是异步的

如何理解呢?

写给代码你就好理解了

<template>
  <div>
    <div>{{count}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>
<script>
export default {
    data () {
        return {
            number: 0
        };
    },
    methods: {
        handleClick () {
            for(let i = 0; i < 10000; i++) {
                this.count++;
            }
        }
    }
}
</script>

如上 想想看Vue中数据驱动视图,本质上就是数据的变化会引起DOM操作改变浏览器的显示

上面的count数据不断变化 变化了1W次 那么岂不是要执行1W次的DOM操作吗?显然这会让浏览器十分的奔溃,浪费了很多的操作

于是vue就决定让这些数据变化完 等到count已经加完1w次了之后再执行DOM 那么这样的话DOM只更新一次也同样达到了效果 显示会变成1W

具体是如何呢

我们来看Vue官方文档的解释

并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列queueWatcher里,然后在 nextTick 后执行 flushSchedulerQueue处理当 count 增加 10000 次时,vue内部会先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行。并不需要在下一个 tick 的时候执行 10000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 10000 即可。

而有的时候我们就需要在DOM更新完成之后再执行操作 但是因为DOM更新是异步的 我们的写的代码又是同步的 就会出现错乱 我们的代码并不会执行!那么我们把操作变成异步的不就好了吗?

这当然是一个办法 最简单的莫过于使用setTimout把时间调成0 这样就完美的实现了 setTimout内的回调函数执行是异步的 会在等DOM执行完毕之后执行

但是这个问题vue官方也了解到了 并给出了解决方案

也就是我们要说nextTick

我们还是先看官方文档上面是怎么讲的

作用:在下一次DOM更新结束后执行其指定的回调

什么时候用:在改变数据后,要基于更新后的DOM进行某些操作时,要在nextTick所指定的回调函数中执行

代码应用
<template>
  <div class="test">
    <p ref='msg' id="msg">{{msg}}</p>
  </div>
</template>
 
<script>
export default {
  name: 'Test',
  data () {
    return {
      msg:"hello world",
    }
  },
  methods: {
    changeMsg() {
      this.msg = "hello Vue"  // vue数据改变,改变了DOM里的innerText
      let msgEle = this.$refs.msg.innerText  //后续js对dom的操作
      console.log(msgEle)  // hello world
      // 输出可以看到data里的数据修改后DOM并没有立即更新,后续的DOM不是最新的
      
      this.$nextTick(() => {
        console.log(this.$refs.msg.innerText) // hello Vue
      })
      this.$nextTick().then(() => {
        console.log(this.$refs.msg.innerText) // hello Vue
      })
    },
    changeMsg2() {
      this.$nextTick(() => {
        console.log(this.$refs.msg.innerText) // 1.hello world 
      })
      this.msg = "hello Vue" // 2.
      console.log(this.$refs.msg.innerText) // hello world
      this.$nextTick().then(() => {
        console.log(this.$refs.msg.innerText) // hello Vue
      })
      // nextTick中先添加的先执行,执行1后,才会执行2(Vue操作Dom的异步)
    }
  }
}
</script>

可能大家对changeMsg2并不是那么理解

我这边先说一下他们在控制台输出的顺序、

hello world 34行代码

hello world 31行代码

hello vue 36行代码

有人不是说next Tick不是异步吗?为什么会这样

nextTick1:注意其虽然是放在$nextTick的回调中,在下一个tick执行,但是他的位置是在this.name = 'hellow world'的前。也就是说,他的cb会App组件的派发更新(flushSchedulerQueue)更先进入队列,当nextTick1打印时,App组件还未派发更新,所以拿到的还是旧的DOM值。

那么nextTick的原理是什么?

由上一节我们知道,Vue中 数据变化 => DOM变化 是异步过程,一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher(Vue源码中的Wacher类是用来更新Dep类收集到的依赖的)推送进这个队列。

如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。

nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback),JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。

因此对next cilk的源码解读如下

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

//  上面三行与核心代码关系不大,了解即可
//  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
//  handleError 错误处理函数
//  isIE, isIOS, isNative 环境判断函数,
//  isNative 判断某个属性或方法是否原生支持,如果不支持或通过第三方实现支持都会返回 false


export let isUsingMicroTask = false     // 标记 nextTick 最终是否以微任务执行

const callbacks = []     // 存放调用 nextTick 时传入的回调函数
let pending = false     // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
    // 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false
    // 


// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
    callbacks.push(() => {
        if (cb) {   // 对传入的回调进行 try catch 错误捕获
            try {
                cb.call(ctx)
            } catch (e) {    // 进行统一的错误处理
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    // 如果当前没有在 pending 的回调,
    // 就执行 timeFunc 函数选择当前环境优先支持的异步方法
    if (!pending) {
        pending = true
        timerFunc()
    }
    
    // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
    // 在返回的这个 promise.then 中 DOM 已经更新好了,
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}


// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行
// setImmediate 在 IE10 和 node 中支持

// 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次

let timerFunc   
// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // 支持 promise
    const p = Promise.resolve()
    timerFunc = () => {
    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
    }
    // 标记当前 nextTick 使用的微任务
    isUsingMicroTask = true
    
    
    // 如果不支持 promise,就判断是否支持 MutationObserver
    // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
    let counter = 1
    // new 一个 MutationObserver 类
    const observer = new MutationObserver(flushCallbacks) 
    // 创建一个文本节点
    const textNode = document.createTextNode(String(counter))   
    // 监听这个文本节点,当数据发生变化就执行 flushCallbacks 
    observer.observe(textNode, { characterData: true })
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)  // 数据更新
    }
    isUsingMicroTask = true    // 标记当前 nextTick 使用的微任务
    
    
    // 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => { setImmediate(flushCallbacks)  }
} else {

    // 以上三种都不支持就选择 setTimeout
    timerFunc = () => { setTimeout(flushCallbacks, 0) }
}
// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
    pending = false    
    const copies = callbacks.slice(0)    // 拷贝一份 callbacks
    callbacks.length = 0    // 清空 callbacks
    for (let i = 0; i < copies.length; i++) {    // 遍历执行传入的回调
        copies[i]()
    }
}

// 为什么要拷贝一份 callbacks

// 用 callbacks.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到在 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果在 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
// 而 nextTick 回调中的 nextTick 应该放在下一轮执行,
// 否则就可能出现一直循环的情况,
// 所以需要将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调


目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。

nextTick的调用方式

  1. 回调函数方式:Vue.nextTick(callback)
  2. Promise方式:Vue.nextTick().then(callback)
  3. 实例方式:vm.$nextTick(callback)

 

标签:nextTick,Vue,DOM,回调,callbacks,原理,执行
From: https://www.cnblogs.com/tomxiao/p/16587739.html

相关文章

  • JSP概念和JSP原理
    JSP入门学习概念:JavaServerPages:Java服务器端页面可以理解为:一个特殊的页面,其中既可以指定定义html标签,又可以定义java代码用于简化书写<html><head>......
  • vlan技术前提下,交换机原理?
    转发机制、对有无目标mac的帧操作、广播组播帧、同一接口学习多个mac基于源MAC地址学习,基于目标MAC地址转发对于没有目标MAC地址表项的帧,向本VLAN的其他所有接口转发......
  • vue2 绑定数组,变化无法更新view的解决方法
    vue绑定数组,更新数组的内容时,view没有更新,多数是因为直接给数组內的数据赋值了,如:this.student[i].name="JackFung";这样做vue是不会触发视图更新的。根据vue的官方文......
  • VUE学习-监听事件
    监听事件事件处理方法可以用v-on指令监听DOM事件,并在触发时运行一些JavaScript代码。<divid="app"> <buttonv-on:click="counter+=1">Add1</button> <p>The......
  • VUE学习-自定义指令
    自定义指令有的情况下,你仍然需要对普通DOM元素进行底层操作,这时候就会用到自定义指令。<divid="directive-demo"> <inputv-focus/></div>全局注册Vue.direct......
  • Session原理分析以及Session的细节
    Session原理分析session的实现是依赖于cookie的当客户端第一次请求会话对象时,服务器会创建一个Session对象,并为该Session对象分配一个唯一的SessionID(用来标识这......
  • 会话技术_Cookie快速入门和会话技术Cookie原理分析
    会话技术会话:一次会话中包含多次请求和响应。一次会话:浏览器第一次给服务器资源发送请求,会话建立,知道有一方断开为止功能:在一次会话的范围内的多次请求间,共享数据......
  • vue源码解析
    先进行语义解析各种vue命令生成模板语法树,再根据模板语法树使用createRender函数(render函数可使用自己定义的)创建render函数,在创建的同时使用闭包(函数柯里化)将模板语法......
  • Vue2.x全家桶
    Vue2.x1、Vue简介1.1、官网英文官网:https://vuejs.org/中文官网:https://cn.vuejs.org/1.2、介绍与描述1、Vue是一套用来动态构建用户界面的渐进式JavaScript框架......
  • ForkJoinPool的使用及基本原理
    一、简介ForkJoinPool是自Java7开始,提供的一个用于并行执行的任务框架。其主旨是将大任务分成若干小任务,之后再并行对这些小任务进行计算,最终汇总这些任务的结果,得到最终......