Vue.nextTick(this.$nextTick) 与响应式数据的原理

news/2024/7/19 14:03:57 标签: vue, js

前言

我们知道,在Vue中,修改响应式数据是异步的。即如果修改后想获取到DOM的更新,需要在nextTick回调函数中才能得到。这样做的主要目的是为了节省性能。

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的

当你设置 vm.someData = ‘new value’,该组件不会立即重新渲染

源码解析

以下是nextTick的源码:

// 各种错误判断和兼容代码略过了。
function nextTick (cb, ctx) {
  // 你传入的回调函数会被依次放入到callbacks数组中。
  callbacks.push(function () {
     cb.call(ctx);
  });
  if (!pending) {
    pending = true;
    timerFunc();
  }
}
// Vue.nextTick 和 this.$nextTick是等价的。
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
};
Vue.nextTick = nextTick;

可见,nextTick函数传入的回调都被push到一个数组中了,那么这个数组callbacks中的回到函数是什么时间执行的呢?

其实当你第一次调用nextTick就已经执行了,但是被放入到微任务队列中

// 这是上边nextTick中 timerFunc的实现 
var p = Promise.resolve();
timerFunc = function () {
  p.then(flushCallbacks);
};

可以看到第一次调用nextTick就会执行timerFunc函数,而p已经是fullfiled的状态,也就是会把flushCallbacks回调函数放入到微任务队列中。当执行栈中的代码执行完毕就会立即执行微任务队列中的flushCallbacks。由于这个过程是异步的,即callbacks可能还会再此期间被推入更多的回调,届时一起执行。

// flushCallbacks  就是这么简单,原封不动搬过来了
// 说白了就是callbacks中的挨个执行。
function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

好了,既然到了这里,那么问题来了。为什么nextTick就能获取到DOM的更新呢?不难想象,那一定是响应式数据已经更新过了。

响应式数据的原理

那么再来看下当给响应式数据赋值直到DOM更新都发生了什么。
在这里插入图片描述
图片来源于官网,setter会拦截响应式数据的设置,任何数据获取的地方,包括Watch,Computed,以及模板,都是一个Watcher。当数据变更的时候,被通知(Notify)进行相应的重新渲染(re-render)或其他逻辑。

其实不妨先和大家直说了,如上图所示,响应式数据的改变当然也是异步的。思路和nextTick基本是一致的,都是准备一个队列,当第一次进行赋值的时候,将执行的时机延迟到微任务队列中,在此期间,任何对于响应式数据的变更,都会放入到同一队列中,当执行时机到来的时候,一起执行。更有甚者,这个异步实现的方法同样是nextTick,见如下源码:

// 当赋值发生的时候会触发每个Watcher的更新,但更新并不是立即执行的,而是放入到队列中,见下面的queueWatcher函数
Watcher.prototype.update = function update() {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

将需要更新的Watcher放入到一个队列中,使用同样的实现即nextTick方法去实现异步,当执行到微任务队列时候一并更新。

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // 这里异步的实现竟然和nextTick调用同一个方法。
    // 这里可以看到同一个Watcher
    if (!waiting) {
      waiting = true;
      nextTick(flushSchedulerQueue);
    }
  }
}

既然二者是相同的实现,那么这就解释了为什么当为响应式数据赋值之后,虽然他是异步的,但nextTick回调中却可以得到更新的数据。因为待执行的nextTick异步队列在响应式数据赋值Watcher的异步队列之后。固nextTick可以得到DOM更新。

DOM更新并非渲染

这里有必要说一句,DOM更新并非是指页面渲染,DOM更新之后,会和渲染引擎(webkit、Blink)进行交互,由后者完成更新,就是通常所说的重排重绘合成的浏览器渲染流程。但我们可以获取到DOM更新的时机就是在变更DOM之后。

下面这个页面alert可以弹出ok,但页面仍旧是一片空白,即渲染未发生,但已经可以获取到DOM更新了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<script>
  window.addEventListener('DOMContentLoaded', () => {
    document.body.append('ok');
    alert(document.body.innerHTML); // ok
  })
</script>
<body>
</body>
</html>

举例印证

最后,我们来尝试做一个简单的例子来印证我们的理解。我们知道,可以得到DOM更新的原因是响应式数据改变的回调队列在nextTick的回调队列之前被调用。那请思考一下两个场景:

假设我们有以下的响应式数据

<template>
	<div>
		<span ref="foo">{{foo}}</span>
		<span ref="bar">{{bar}}</span>
	</div>
</template>
data() {
	return {
		foo: 'foo',
		bar: 'bar'
	}
}
mounted() {
	// 第一种情况
	this.$nextTick(() => {
		console.log(this.$refs.foo.innerHTML) // ??
	})
	this.foo = 'foo更新了'
	// 第二种情况
	this.bar = 'bar 更新了'
	this.$nextTick(() => {
		console.log(this.$refs.foo.innerHTML) // ??
	})
	this.foo = 'foo更新了'
}
  1. 先调用nextTick,后改变响应式数据。可以获取到DOM更新么?
  2. 先改变其他的响应式数据,后调用nextTick,再改变响应式数据bar,nextTick中可以得到DOM更新么?

具体结果可以看这篇博客,这里我就不贴了,留给各位自行思考。

参考

github vue: https://github.com/vuejs/vue
vue 文档: https://vuejs.org/

备注

本文vue源码基于vue 2.6.12


http://www.niftyadmin.cn/n/1382577.html

相关文章

培养自己的核心竞争力

人的核心竞争力超过一半来自重要而不紧急的事情&#xff1a;1、读书。特别读那些非有用性的书添加自己的思维角度和阅读视野&#xff0c;也能够听各种视频节目。比方微信自媒体等等&#xff1b;[转载]《人生的真相》《冷眼看人生》《我不是教你诈》有4本&#xff0c;《爱不厌诈…

使用Spring Request-Reply实现基于Kafka的同步请求响应

2019独角兽企业重金招聘Python工程师标准>>> 大家提到Kafka时第一印象就是它是一个快速的异步消息处理系统&#xff0c;不同于通常tomcat之类应用服务器和前端之间的请求/响应方式请求&#xff0c;客户端发出一个请求&#xff0c;必然会等到一个响应&#xff0c;这种…

C语言-多重背包问题

多重背包问题 问题&#xff1a;有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用&#xff0c;每件费用是c[i]&#xff0c;价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量&#xff0c;且价值总和最大。 分析&#xff1a; 这题目和完全背包问…

JS中获取cookie的最简单方式

const getCookie (name) > document.cookie.match([;\s]?${name}([^;]*))?.pop();// 比如cookie如下&#xff1a; ab; cd // 使用 getCookie(c) // d其中match是字符串的原型方法。 str.match(regexp) 如果传入一个非正则表达式对象&#xff0c;则会隐式地使用 new RegEx…

判断ios或者android

<script type"text/javascript">$(function () {// android和iso下载链接var u navigator.userAgent;var isAndroid u.indexOf(Android) > -1 || u.indexOf(Adr) > -1; //android终端$(.dow).on(click,function () {if (isAndroid) {location.href ht…

24.打印9*9乘法表

/** 打印9*9乘法表*/ public class Multiplication {public static void main(String[] args) {int i, j; // 循环变量for (i 1; i < 9; i){ // 外层循环控制被乘数 for (j 1; j < i; j){ // 内层循环控制乘数 System.out.print(i"*"j&…

JFinal框架学习-------EhCachePlugin

2019独角兽企业重金招聘Python工程师标准>>> 一.关于EhCachePlugin 在之前的文章中&#xff0c;我们已经介绍过了JFinal中Cache的一些简单使用&#xff0c;这篇文章将讲述EhCachePlugin的使用&#xff0c; EhCachePlugin是JFinal集成的缓存插件&#xff0c;通过使用…

jQuery_mobile 按钮学习

2019独角兽企业重金招聘Python工程师标准>>> jQueryMobile1.4之前button的写法&#xff1a; a标签用的是data-role的属性&#xff0c;并且他们默认是圆角的&#xff1b;&#xff08;这样写就可以正常显示一个圆角的button了&#xff09; <a href"#" da…