【Vue源码分析——nextTick的使用及实现源码】

11 篇文章 0 订阅

Vue2源码分析



前言

在开发中,也会遇到用nextTick的情况,面试中也经常考到。因此,总结了下nextTick的使用及实现原理。
我们先来看一个场景:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <div id="app" style="width:120px; height:100px">
            <div style="color: red">
                {{name}}
            </div>
            <span>{{age}}</span>
        </div>
        <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
        <script>
            const vm = new Vue({
                data: {
                    name: 'lisa',
                    age: 20,
                },
                el:'#app', // 将数据解析到el元素上
            })
            vm.$mount('#app')
            // nextTick不是创建了一个异步任务,而是将任务维护到任务队列中
           
            vm.name = '赵丽颖'
            vm.$nextTick(()=>{
                console.log('nextTick中', app.innerHTML)
            })
           console.log('非nextTick',app.innerHTML)
        </script>
    </body>   
</html>

在这里插入图片描述
神奇的事情发生了,明明把name改为赵丽颖了,但是没有使用nextTick的话,获取的Dom还是以前的,而不是最新的Dom。

此外,created里面、一些第三方插件都会遇到同样的情况。

说起原因,还得从头说起。


一、知识铺垫

1、异步更新

vue的响应式更新,并不是数据变化之后Dom立即变化,而是按照一定的策略更新的。

Vue是异步更新的,因此,要获得更新后的Dom,要使用nextTick来获取。
为什么要这么设计呢?

我们知道,根据数据响应式原理:会给每一个属性配置Object.defineProperty,并在读取数据时(也就是在get中)收集依赖,在更新数据时(也就是在set中)触发依赖。这个依赖指的是watcher(类),在读取数据时把它存起来,更新数据时通知watcher更新数据。

试想一下,如果一个属性被修改了多次,就会多次触发watcher:

setTimeout(()=> {
  vm.name='shelly'
  vm.name = 'lisax'
  vm.age = 18
}, 3000)

那岂不是要多次更新Dom,这样就很浪费性能。因此,Vue开启了一个队列,并缓冲在同一事件循环中发生的所有数据变更。通过同一个watcher被多次触发,只会被推入队列一次。这种缓存时去重对于避免不必要的计算和dom操作是非常重要的。实现代码如下:

update() { // 更新数据时触发
	if (this.lazy) {
	     // 如果是计算属性 依赖的值变化了,就标识计算属性是脏值了
	     this.dirty = true
	 } else {
	     queueWatcher(this); // 把当前的watcher暂存起来
	 }
}

let queue = [];
let has = {};
let pending = false; // 防抖

// setTimeout中的回调函数,执行一次刷新操作
function flushSchedulerQueue() {
    let flushQueue = queue.slice(0);
    queue = []; // 重置
    has = {}; // 重置
    pending = false; // 重置
    flushQueue.forEach(q => q.run()); // 依次执行队列中的事件
}

// 数据更新时先暂存watcher
function queueWatcher(watcher) {
    const id = watcher.id;
    if (!has[id]) { // 如果watcher已经存在,则不需要加入队列
        queue.push(watcher); // 更新数据时,不立马更新Dom,使用队列把watcher缓存起来
        has[id] = true;
        // 不管update执行多少次,最终只执行一轮刷新操作
        if (!pending) {
            setTimeout(flushSchedulerQueue, 0); // 利用setTimeout进行回调,根据事件循环机制,setTimeout会在同步代码后面执行,详见下文的事件循环
            // nextTick(flushSchedulerQueue, 0); 后文实现nextTick
            pending = true;
        }
    }
}

2、事件循环机制

JS是一门单线程语言,那就意味着一次只能执行一个任务且按顺序执行。如果有耗时任务,也必须等着它执行完了才能执行下一个任务。问题来了,浏览网页的时候,某个高清图片需要加载很久,那网页岂不是卡着等图片加载完才能做别的操作?
显然不是这样的,JS设计者设计了一种执行机制:事件循环机制(Event Loop),以实现单线程非阻塞的方法。

我们先了解下任务。任务可分为同步任务、异步任务,同步任务可以立即执行,一般会直接进入主线程中执行。异步任务指不进入主线程而进入任务队列的任务,一般是比较耗时的,如setTimeout、ajax请求等。异步任务再细分下,可分为宏任务、微任务:

  • 宏任务(macro-task):包括整体代码script、setTimeout、setInterval、ajax、DOM事件

  • 微任务(micro-task):Promise.then、Node 环境下的process.nextTick

然后,事件循环机制是如何执行任务的呢?进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

详细内容查看我的另一篇博客:【JS执行机制——事件循环机制】

二、nextTick的实现

1、实现nextTick方法
在初始化的时候先把nextTick绑定在Vue的原型上,这样vm的实例就可以调用了:

import { nextTick } from './observe/watcher'
export function initStateMixin(Vue) {
    Vue.prototype.$nextTick = nextTick
}
// watcher.js
let callbacks = []; // 队列
let waiting = false;// 防抖,标识当前是否有 nextTick 在执行,同一时间只能有一个执行
export function nextTick(cb) { //  cb就是使用时用户传过来的方法
    callbacks.push(cb); // 维护nextTick中的callback方法
    if (!waiting) {
        setTimeout(()=>{
          flushCallbacks(); // 最后一起刷新
        // timerFunc()
        },0)
        waiting = true;
    }
}
// setTimeout中的回调函数,执行一次刷新操作
function flushCallbacks() {
    //debugger
    waiting = false;
    let cbs = callbacks.slice(0);
    callbacks = [];
    cbs.forEach(cb => cb()); // 按照顺序依次执行
}

2、使用promise优化
用setTimeout性能耗费比微任务大,且比微任务后面执行。因此,尝试使用promise进行优化:

let timerFunc;
timerFunc = () => {
	Promise.resolve().then(flushCallbacks)
}
export function nextTick(cb) {
    //debugger
    callbacks.push(cb); // 维护nextTick中的callback方法
    if (!waiting) {
        //setTimeout(()=>{
        //  flushCallbacks(); // 最后一起刷新
        timerFunc() // 这样就是微任务了
        //},0)
        waiting = true;
    }
}

上文异步更新中queueWatcher方法中的setTimeout,就可以复用nextTick方法了:

function queueWatcher(watcher) {
    const id = watcher.id;
    if (!has[id]) {
        queue.push(watcher);
        has[id] = true;
        console.log(queue)
        // 不管update执行多少次,最终只执行一轮刷新操作
        if (!pending) {
            // setTimeout(flushSchedulerQueue, 0);
            nextTick(flushSchedulerQueue, 0);
            pending = true;
        }
    }
}

3、优雅降级
有些浏览器不兼容promise,比如ie浏览器。所以内部采用了优雅降级的方式:

let timerFunc;
if (Promise) {
    timerFunc = () => {
        Promise.resolve().then(flushCallbacks)
    }
} else if (MutationObserver) { // 如果promise不支持,就使用MutationObserver
    let observe = new MutationObserver(flushCallbacks)
    let textnode = document.createTextNode(1);
    observe.observe(textnode, {
        characterData:true
    })
    timerFunc = () => {
        textnode.textContent = 2;
    }
} else if (setImmediate) { // 再不支持,使用setImmediate
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    timerFunc = () => { // 再不支持,使用setTimeout
        setTimeout(flushCallbacks)
    }
}
export function nextTick(cb) {
    callbacks.push(cb); // 维护nextTick中的callback方法
    if (!waiting) {
        timerFunc()
        waiting = true;
    }
}

4、还有一个问题就是,使用nextTick的时候,你得这么使用:

vm.name = 'Lisa'
vm.$nextTick(()=>{ // 要在更新数据的后面使用
  console.log(app.innerHTML)
})

因为:更新数据时,先把更新Dom的事件放入队列,然后再把nextTick事件放入队列。队列先进先出,这样才能依次执行,nextTick才能获取更新后的Dom。


总结

1、nextTick是Vue提供的一个全局API,由于Vue的异步更新策略导致我们对数据的修改不会更新,如果此时想要获取更新后的Dom,就需要使用这个方法。

2、nextTick实现原理并不算复杂,即在一次事件循环中,更新了数据,把更新Dom的操作放入队列中,使用了nextTick,则把nextTick里的回调放入队列中,执行完所有的同步代码后,去执行微任务,即依次调用队列里的函数。

3、nextTick不是创建了一个异步任务,而是将任务维护到任务队列中。

源码地址:小Demo手写Vue2
官网源码:官网网址

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js的$nextTick函数是用于在DOM更新之后执行异步操作的方法。该方法的实现使用了微任务队列,即将异步操作推入到微任务队列中,在DOM更新后执行异步操作。$nextTick方法是Vue.js响应式系统的重要部分,它确保了Vue.js组件的异步行为和数据响应式。 具体的实现过程如下: 1. 首先检查是否支持原生的Promise对象,如果支持,则直接返回Promise.resolve()。 2. 如果不支持原生的Promise对象,则创建一个新的Promise对象。 3. 将一个空函数推入微任务队列中。 4. 在新创建的Promise对象的resolve回调中,再次推入一个空函数到微任务队列中。 5. 当浏览器执行到微任务队列中的空函数时,DOM更新已经完成,可以执行异步操作了。 下面是$nextTick方法的源代码: ```javascript Vue.prototype.$nextTick = function(fn: Function) { return nextTick(fn, this) } ``` 其中,nextTick是一个工具函数,它实现了具体的异步操作逻辑: ```javascript 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() } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } ``` 该函数定义了一个callbacks数组用于存储异步操作,当调用nextTick函数时,将回调函数push进callbacks数组中,然后判断是否有待执行的异步操作,如果没有,则通过macroTimerFunc或microTimerFunc函数执行异步操作。 最后,如果调用$nextTick方法时没有传入回调函数,则会返回一个新的Promise对象,用于异步操作的等待和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值