首页 > 其他分享 >每日一题之Vue的异步更新实现原理是怎样的?

每日一题之Vue的异步更新实现原理是怎样的?

时间:2023-01-09 10:14:53浏览次数:79  
标签:nextTick 异步 Vue queue 任务 let 一题

最近面试总是会被问到这么一个问题:在使用vue的时候,将for循环中声明的变量i从1增加到100,然后将i展示到页面上,页面上的i是从1跳到100,还是会怎样?答案当然是只会显示100,并不会有跳转的过程。

怎么可以让页面上有从1到100显示的过程呢,就是用setTimeout或者Promise.then等方法去模拟。

讲道理,如果不在vue里,单独运行这段程序的话,输出一定是从1到100,但是为什么在vue中就不一样了呢?

for(let i=1; i<=100; i++){
    console.log(i);
}

这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。

JS运行机制

众所周知,JS是基于事件循环的单线程的语言。
执行的步骤大致是:

  1. 当代码执行时,所有同步的任务都在主线程上执行,形成一个执行栈
  2. 在主线程之外还有一个任务队列(task queue),只要异步任务有了运行结果就在任务队列中放置一个事件;
  3. 一旦执行栈中所有同步任务执行完毕(主线程代码执行完毕),此时主线程不会空闲而是去读取任务队列。此时,异步的任务就结束等待的状态被执行。
  4. 主线程不断重复以上的步骤。 我们把主线程执行一次的过程叫一个tick,所以nextTick就是下一个tick的意思,也就是说用nextTick的场景就是我们想在下一个tick做一些事的时候。

所有的异步任务结果都是通过任务队列来调度的。而任务分为两类:宏任务(macro task)和微任务(micro task)。它们之间的执行规则就是每个宏任务结束后都要将所有微任务清空。
常见的宏任务有setTimeout/MessageChannel/postMessage/setImmediate,微任务有MutationObsever/Promise.then

nextTick原理

派发更新

大家都知道vue的响应式的靠依赖收集和派发更新来实现的。在修改数据之后的派发更新过程,会触发setter的逻辑,执行dep.notify()

// src/core/observer/watcher.js
class Dep {
    notify() {
        //subs是Watcher的实例数组
        const subs = this.subs.slice()
        for(let i=0, l=subs.length; i<l; i++){
            subs[i].update()
        }
    }
}

遍历subs里每一个Watcher实例,然后调用实例的update方法,下面我们来看看update是怎么去更新的:

class Watcher {
    update() {
        ...
        //各种情况判断之后
        else{
            queueWatcher(this)
        }
    }
}

update执行后又走到了queueWatcher,那就继续去看看queueWatcher干啥了(希望不要继续套娃了:

//queueWatcher 定义在 src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0

export function queueWatcher(watcher: Watcher) {
    const id = watcher.id
    //根据id是否重复做优化
    if(has[id] == null){
        has[id] = true
        if(!flushing){
            queue.push(watcher)
        }else{
            let i=queue.length - 1
            while(i > index && queue[i].id > watcher.id){
                i--
            }
            queue.splice(i + 1, 0, watcher)
        }

        if(!waiting){
            waiting = true
            //flushSchedulerQueue函数: Flush both queues and run the watchers
            nextTick(flushSchedulerQueue)
        }
    }
}

这里queue在pushwatcher时是根据idflushing做了一些优化的,并不会每次数据改变都触发watcher的回调,而是把这些watcher先添加到⼀个队列⾥,然后在nextTick后执⾏flushSchedulerQueue

flushSchedulerQueue函数是保存更新事件的queue的一些加工,让更新可以满足Vue更新的生命周期。

这里也解释了为什么for循环不能导致页面更新,因为for是主线程的代码,在一开始执行数据改变就会将它push到queue里,等到for里的代码执行完毕后i的值已经变化为100时,这时vue才走到nextTick(flushSchedulerQueue)这一步。

参考 前端进阶面试题详细解答

nextTick源码

接着打开vue2.x的源码,目录core/util/next-tick.js,代码量很小,加上注释才110行,是比较好理解的。

const callbacks = []
let pending = false

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
    timerFunc()
  }

首先将传入的回调函数cb(上节的flushSchedulerQueue)压入callbacks数组,最后通过timerFunc函数一次性解决。

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
    }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

timerFunc下面一大片if else是在判断不同的设备和不同情况下选用哪种特性去实现异步任务:优先检测是否原生⽀持Promise,不⽀持的话再去检测是否⽀持MutationObserver,如果都不行就只能尝试宏任务实现,首先是setImmediate,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的话最后就会降级为 setTimeout 0。

这⾥使⽤callbacks⽽不是直接在nextTick中执⾏回调函数的原因是保证在同⼀个 tick 内多次执⾏nextTick,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。

nextTick使用

nextTick不仅是vue的源码文件,更是vue的一个全局API。下面来看看怎么使用吧。

当设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环tick中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

官网用例:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据

vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

并且因为$nextTick() 返回一个 Promise 对象,所以也可以使用async/await 语法去处理事件,非常方便。

标签:nextTick,异步,Vue,queue,任务,let,一题
From: https://www.cnblogs.com/bbxiaxia1998/p/17036118.html

相关文章

  • 前端二面经典vue面试题指南
    v-model的原理?我们在vue项目中主要使用v-model指令在表单input、textarea、select等元素上创建双向数据绑定,我们知道v-model本质上不过是语法糖,v-model在内部为......
  • 百度前端经典vue面试题整理
    子组件可以直接改变父组件的数据吗?子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的prop都将会刷......
  • 每日一题之Vue数据劫持原理是什么?
    什么是数据劫持?定义:数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。简单地说,就是当我们触发函数的时候动......
  • 13.VUE
    VUE笔记目录:(https://www.cnblogs.com/wenjie2000/p/16378441.html)视频教程(P146~P148)本篇是使用的vue2。虽然vue3.x已经出了,目前但对于后端人员来说了解一些vue2就足......
  • 关于NET异步的理解
    1、包含async、await关键字及Task相关方法,async和await必须成对使用(Task无强制要求)。2、异步是为了解决执行耗时操作所导致的线程阻塞。3、当在你的method中调用NET提供......
  • 异步编程的历史演进
    或许你也听说了,摩尔定律失效了。技术的发展不会永远是指数上升,当芯片的集成度越来越高,高到1平方毫米能集成几亿个晶体管时,也就是人们常说的几纳米工艺,我们的半导体行业就......
  • vue使用vite配置跨域以及环境配置详解
    vue使用vite配置跨域以及环境配置详解如何配置跨域,代理域名区分开发环境和生产环境,以及预发布环境可以做什么事补充:解决跨域常用方法一、VUE中常用proxy来解决跨域......
  • Flutter 陈航 23-事件循环 Event Loop 异步 线程 Isolate
    本文地址目录目录目录23|单线程模型怎么保证UI运行流畅?EventLoop事件循环模型微任务队列异步任务Future简单案例官方综合案例改造后的案例异步函数同步等待await不......
  • vue相同路由跳转,数据不刷新问题
    问题的出现    vue-router的切换不同于传统的页面的切换。路由之间的切换,其实就是组件之间的切换,不是真正的页面切换。这也会导致一个问题,就是引用相同组件的时候,会......
  • day05-Vue02
    Vue027.修饰符7.1基本说明修饰符(Modifiers)是以.指明的后缀,指出某个指令以特殊方式绑定官方文档:修饰符Vue中的修饰符有:事件修饰符按键修饰符系统修饰符事件修......