Vue nextTick 数据-DOM更新原理及简单实现

1,177 阅读2分钟

参考: Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!

看了大佬的文章,第一次记住了,隔了一段时间回想发现又忘了,写一篇文章来记录学习过程,以及简单实现一个nextTick。

文章的核心其实是纠正了一个常见的误区,先抛出一个问题: 为什么在Vue中修改了数据之后,要使用$nextTick才能获取到修改的DOM值 👇戳这里看示例代码

export default {
    data() {
        return {
                inputVal: 1
        }
    },
    mounted() {
        this.inputVal = 2
        console.log(document.getElementById('input').value) // 1
        this.$nextTick(() => {
                console.log(documen.getElementById('input').value) // 2
        })
    },
    render() {
        return h('input', {
                attrs: {
                        id: 'input'
                },
                props: {
                        value: this.inputVal
                }
        })
    }	
}

问题一: DOM操作是同步的还是异步的?

是同步的!!!

DOM操作是同步的,在本质上,DOM也只是一个对象而已,这里的同步是指在修改DOM对象时,在数据层面上,你的下一行代码就可以拿到它的修改结果,但是视觉上可不一定,因为要想看到页面上渲染出对应的元素,还需要浏览器的进一步Render操作,这个Render才是异步,不能把概念搞混淆。

问题二:既然DOM操作是同步的,那为什么Vue还需要nextTick才可以拿到改变后的元素值?

因为Vue对DOM的操作行为是异步的!!!

也就是说 下面这段代码, Vue对input的DOM操作流程是这样的:

this.inputVal = 2 -> 把渲染Watcher添加到队列queueWatcher中 -> 调用nextTick(flushSchedulerQueue) 在下一个tick清空Watcher任务队列🌟异步🌟 -> 触发渲染Watcher,改变DOM

mounted() {
    this.inputVal = 2   // 对DOM的操作在下一个tick才生效
    console.log(document.getElementById('input').value) // 1
    this.$nextTick(() => {
            console.log(document.getElementById('input').value) // 2
    })
},

Vue使用nextTick来实现任务的调度,它的本质是开启了一个微任务,this.$nextTick会把传进来的回调函数保存起来,但不会立即执行,而是推入队列中,在下一个tick执行

问题三:简单实现一个nextTick

结合源码,简化了一些逻辑,实现了一个简单的nextTick,它的原理也十分简单,调用nextTick时把cb保存起来,

// next-tick.js
const callbacks = [];
let pending = false;

// 执行所有callback
function flashCallbacks() {
     pending = false;
    // 赋值一份callbacks,如果在nextTick中又调用了nextTick,要将其放入下一个任务队列
    let copies = callbacks.slice(0)

    for(let i=0; i<copies.length; i++) {
            copies[i]();
    }
}

let timerFunc;

// timerFunc是真正需要执行的微任务 
// TODO: 兼容性降级: Promise -> mutationObserver -> setImmediate -> setTimeout
 let p = Promise.resolve();
 timerFunc = () => {
   p.then(() => {
     flashCallbacks();
   });
 };

// nextTick
export function nextTick(cb, ctx) {
  callbacks.push(() => {
      cb.call(ctx);
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

问题四:nextTick和普通的Promise有什么区别?

Promise每一次链式调用then都会开启一个新的微任务,而nextTick在同一个tick内执行时,会把所有的回调都添加到同一个微任务中。 看一个例子

// test.js
import { nextTick } from 'next-tick.js'
nextTick(() => {
  console.log(1);
  nextTick(() => {
    console.log(4);
  });
});

Promise.resolve()
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(6);
  });

nextTick(() => {
  console.log(2);
  nextTick(() => {
    console.log(5);
  });
});

最后输出的结果为

1
2
3
4
5
6