【前端】理解 Vue2.x 响应式原理

前言

前几天在掘金上看到一位网友的面经(大厂面试:别拯救了,拯救不了一点 - 掘金 (juejin.cn)),其中有一道题引起了我的注意:

这道题难在解释清楚原理是什么,这涉及到了Vue渲染页面的原理和nexttick执行顺序。

本文主要内容包括:

  1. 原题答案分析。

    1.1. Vue响应式原理概览。

    1.2. nexttick原理概览。

    1.3. 原题目原理分析。

  2. 修改后的题目分析。

1. 原题答案分析

这里我在Vue2源码中提供的examples来运行这道题

vue2 源码地址:github.com/vuejs/vue

还原题目:

html 复制代码
<meta charset="utf-8" />
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="../../dist/vue.js"></script>
<link rel="stylesheet" href="../../node_modules/todomvc-app-css/index.css" />
<div id="app">
  <div ref="num">
    <h1>a是: {{a}}</h1>
    <h1>b是: {{b}}</h1>
  </div>
</div>

<script>
  const vm = new Vue({
    data() {
      return {
        a: 100,
        b: 200
      }
    },
    created() {
      setTimeout(() => {
        this.$nextTick(() => {
          console.log(this.$refs.num.innerHTML)
        })
        
        this.b = 400
      }, 200)
    }
  })
  vm.$mount('#app')
</script>

可以看到在$nextTick 中拿到的a和b是 300 和 400 ,这很好理解,毕竟nexttick是异步执行嘛,拿到更新后的数据也正常。

把对this.a移动到下面来试试?

html 复制代码
<meta charset="utf-8" />
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="../../dist/vue.js"></script>
<link rel="stylesheet" href="../../node_modules/todomvc-app-css/index.css" />
<div id="app">
  <div ref="num">
    <h1>a是: {{a}}</h1>
    <h1>b是: {{b}}</h1>
  </div>
</div>

<script>
  const vm = new Vue({
    data() {
      return {
        a: 100,
        b: 200
      }
    },
    created() {
      setTimeout(() => {
        this.$nextTick(() => {
          console.log(this.$refs.num.innerHTML)
        })
        this.a = 300 // 把对a的修改移动到下面来试试。
        this.b = 400
      }, 200)
    }
  })
  vm.$mount('#app')
</script>

输出结果:

这回拿到的怎么就是100和200,但是nexttick不是异步执行吗?按理说应该拿到的还是更新后的数据。

想要知道原因,就先要了解vue响应式的原理。

1.1 Vue响应式原理概览

vue的响应式原理大概可以分为如下几个步骤:

被观察者:Dep

一个dep是一个可观察的对象,可以有多个观察者观察它,在data的中,每个对象(包括数组)都有一个Dep,

Watcher观察data中的对象,其实就是观察Dep。那么Dep做了什么事呢?在对象(或者数组)被获取的时候,Dep会去收集Watcher,就拿渲染Watcher来说,当访问data中的数据的时候,Watcher会首先把自己挂载到Dep.target上,然后Dep会去收集Watcher

这样,每个数据就"知道"自己被谁观察了,自己发生改变的时候(即触发set),就会通知观察自己的Watcher,去执行回调(比如刷新页面、重新执行计算属性)。

观察者:Watcher

Watcher是Vue实现响应式的核心部分。我们都知道,Vue的响应式原理基于观察者模式,而Watcher就是这个观察者,它的主要工作是在被观察者发生改变时,接收通知并执行相应的动作(即执行回调函数),类似于JavaScript中的事件监听。在Vue中,有三种Watcher:

1. 渲染Watcher:

渲染Watcher随着组件的创建而创建,每个组件独一份,在创建Watcher的时候,组件会将更新自己的函数updateComponent传入Watcher作为回调,当本组件内的的数据变化的时候,Dep就会通知Watcher触发这个回调,从而更新组件。

updateCompoent内容:

  1. 侦听器Watcher:

侦听器Watcher是Vue的watch核心内容,侦听器Watcher和渲染watcher的在于侦听器Watcher多一个用户传入的回调,并且第一个回调也不再是刷新页面了,而是获取侦听对象

vue2中的侦听器,在被执行后会立马获取一遍侦听目标(vue3 略有不同),之前提到过,获取数据会被Dep记录,当侦听目标发生改变的时候,watcher就会做两件事(即传入的两个回调):1. 重新获取目标值。2.执行用户传入的回调函数。

  1. 计算属性Watcher:

计算属性Watcher和侦听器Watcher很像,

主要的区别就是计算属性有缓存。

evaluate函数:

1.2 nexttick原理概览

nexttick作用其实很简单:

  1. 接受回调函数。
  2. 把回调函数放到任务队列中(其实就是个数组)。
  3. 主线程执行完毕,开始遍历数组,依次执行。

那nexttick可以在下次 DOM 更新循环结束之后执行延迟回调。具体是怎么实现的呢? 这又要回到刚刚提到的Watcher了,其实只需要了解一下页面更新的完整流程你就知道原因了:

再简化一下:

也就是说,nexttick之所以能获取到更新后的页面,是因为用户调用nexttick传入的函数比更新页面的函数后入队列,这就是为什么在修改数据之前调用nexttick是拿不到页面更新后的数据的原因。

原题目原理分析

在大概了解Vue的响应式原理之后,我们就可以来分析这道题目了,

为什么this.a 放到上面去拿到的是更新后的页面也就说得通了:

在理解了原理之后,这道题目就是这么简单。

修改后的题目分析

将题目小小的变动一下:

html 复制代码
<meta charset="utf-8" />
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="../../dist/vue.js"></script>
<link rel="stylesheet" href="../../node_modules/todomvc-app-css/index.css" />
<div id="app">
  <div ref="num">
    <h1>a是: {{a}}</h1>
    <h1>b是: {{b}}</h1>
  </div>
</div>

<script>
  const vm = new Vue({
    data() {
      return {
        a: 100,
        b: 200
      }
    },
    created() {
      // 去除 setTimeout
      this.$nextTick(() => {
        console.log(this.$refs.num.innerHTML)
      })
      this.a = 300
      this.b = 400
    }
  })
  vm.$mount('#app')
</script>

这里我将settimeout去除了,结果还和之前一样吗?

输出:

可以看到,结果和去除settimeout之间不一样了,拿到的居然是更新后的值!

2.1 原因分析

这其实和我们代码执行的位置有关系。

我们知道,Vue的数据响应式是基于观察者模式的,在created中,虽然data已经被劫持,但是由于渲染Watcher还没有被创建,所以页面连一次数据都还没更新,

这个时候我们修改data,不会触发页面的更新逻辑,但是data的值实实在在的已经改了。 当Vue创建渲染Watcher的时候(也就是执行第一次页面更新的时候,而且是同步执行),拿到的data中的a和b就是已经修改后的数据了,所以我们在nexttick中获取到的页面也就是更新后的了。

其实只要我们把逻辑写在mounted中就可以获取到更新之前的值了,因为mounted执行的时候,渲染Watcher已经创建。

输出测试:

相关推荐
森叶5 分钟前
webpack 的打包target讲解 & node环境打包下的文件存储造成不易察觉的坑点
前端·webpack·node.js
亿牛云爬虫专家15 分钟前
Puppeteer的高级用法:如何在Node.js中实现复杂的Web Scraping
前端·javascript·爬虫·node.js·爬虫代理·puppeteer·代理ip
uhan2520 分钟前
2024年9月26日 linux笔记
linux·服务器·前端
Jiaberrr38 分钟前
uniapp视频禁止用户推拽进度条并保留进度条显示的解决方法——方案一
前端·javascript·微信小程序·uni-app·音视频
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:设置 PDF 的查看器首选项和缩放系数
前端·pdf·.net·spire.pdf
OEC小胖胖1 小时前
js中正则表达式中【exec】用法深度解读
开发语言·前端·javascript·正则表达式·web
有一个好名字1 小时前
vue3路由
前端·vue.js
计算机学姐1 小时前
基于php摄影门户网站
开发语言·vue.js·vscode·后端·php·phpstorm·webstorm
马卫斌 前端工程师2 小时前
npm 源切换以及添加 使用工具 nrm 使用方法
前端·npm·node.js
小彭努力中2 小时前
49. 建模软件绘制3D场景(Blender)
前端·3d·blender·webgl