侧边栏壁纸
博主头像
Hoo - Dev Tools Kit

开发工具箱

  • 累计撰写 5 篇文章
  • 累计创建 0 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vue2 响应式原理

Hoo
Hoo
2025-05-29 / 0 评论 / 0 点赞 / 21 阅读 / 0 字

vue2 数据响应式原理

根据官方文档进行阐述

vue官方阐述:https://cn.vuejs.org/v2/guide/reactivity.html

什么是响应式?就是数据变化过后需要去做一些事情。

首先 vue 通过 Object.defineProperty 遍历对象的每一个属性,把每个属性变为一个 gettersetter 函数,这就把这个数据变成一个响应式数据了。而我们知道,每个组件都有一个 render 函数,这个函数运行完成后会生成一棵虚拟dom树,最终对比新旧树的差异,以最小单位更新真实dom树,从而达到在页面上看到最新数据的效果。但是这里我们就有一个疑问了,这个 render 函数是怎么知道我们的数据发生改变的?

那我们就详细来看看 render 函数的运行过程。

假设:当我们在页面上去创建一个随着 msg 数据变化的 h1 元素时,会在 render 中通过 h("h1", this.msg) 的形式来创建。

这里涉及到一个关键的点:调用了 msggetter 函数。此时,这个 getter 函数就会记录:当前有一个 render 函数,用到了我这个 msg 属性。这就是依赖收集(Collect as Dependency),收集完成之后会交给 Watcher 来进行观察。将来如果这个 msg 属性发生变化的话(此时就用到了 setter 函数),会通知(派发更新) WatcherWatcher 就会根据该属性的依赖关系,去通知 render 重新触发该 render 函数的执行,最终重新生成一棵虚拟dom树。

详细阐述

响应式数据最终的目标,就是当对象或者对象的属性发生变化时,会运行一些函数,最常见的就是 render 函数了,其他的比如 $watch

vue 为了做到这一点,用到了几个核心的部件:

  1. Observer 构造器
  2. Dep
  3. Watcher
  4. Scheduler 调度器

Observer 构造器


这个构造器是 vue 内部提供的,它的作用是将一个普通的对象转换成一个响应式的对象

具体的实现使用到了 Object.defineProperty 的方法。Observer深度遍历对象里的每一个属性,通过该方法将每个属性转换成带有 gettersetter 的属性。这样一来,vue 就能对数据变化做一些额外的操作了。

Observer

在组件生命周期中,这个操作发生在 beforeCreatedcreated 之间。

但是这里有些缺陷。比如说:

  1. 对象:Observer 在深度遍历时,只会遍历到对象的当前属性,所以对于后续需要动态增加或者删除属性,vue 是收不到通知的。

举个例子:

<div id="app" class="container">
      <h1>Obj.a: {{obj.a}}, Obj.b: {{obj.b}}</h1>
      <button @click="delete obj.a">delete a</button>
      <button @click="obj.b = Math.random()">add b</button>
</div>
var vm = new Vue({
	el: "#app",
	data() {
		return {
			obj: {
				a: 1,
			},
		};
	},
});

动态增加:

理想的状态下,点击按钮之后,Obj.b 可以在界面上显示出值,但实际效果:

动态增加

这是因为在最开始的时候,Observer 没有遍历到 obj.b 这个属性,因此就不会将它转换为带有 gettersetter 函数的属性。而当我们去打印 vue 实例时,又能很清楚的看到 obj.b 被创建出来了。

动态删除属性:

动态删除

怎么破局呢?vue 很贴心的考虑到这一点,它提供了 $set$delete 函数。

  • $set(对象,'属性名',属性值):添加响应式属性

  • $delete(对象,'属性名'):删除响应式属性

    <div id="app" class="container">
        <h1>Obj.a: {{obj.a}}, Obj.b: {{obj.b}}</h1>
        <button @click="$delete(obj, 'a')">delete a</button>
        <button @click="$set(obj,'b', Math.random())">add b</button>
    </div>
    

    $set增加响应式属性:

    增加响应式属性

    $delete删除响应式属性:

    删除响应式属性

  1. 数组:对于数组而言,vue 会更改它的原型,原本数组原型应该是这样的 数组.__proto__ === Array.prototype,但是 vue 为了能监听到数组内数据的改动,它选择自己“重做”了一个数组原型,现在 vue 中数组原型的指向变成了这样:

arrinvue

这里我们打印实例里的数组就可以看得很清楚:

vue中数组的原型1
vue中数组的原型2

但是,由于种种原因,数组是监控不到给某一个属性重新赋值的

<div id="app" class="container">
    <ul>
    <li v-for="n in arr">{{n}}</li>
    </ul>
    <button @click="arr[0]=26">change first</button>
</div>
var vm = new Vue({
    el: "#app",
    data() {
        return {
        arr: [1, 2, 3, 4],
        };
    },
});

更改数组的第一项

但是,如果这个数组里有一个对象,给这个对象重新赋值的话,是可以监测到的。这是因为 Observer 会深度遍历到每个对象里的每个属性。

<div id="app" class="container">
    <ul>
    <li v-for="n in arr">{{n}}</li>
    </ul>
    <button @click="arr[0].name='username'">change first</button>
</div>
var vm = new Vue({
    el: "#app",
    data() {
        return {
        arr: [{ name: "monica" }, 2, 3, 4],
        };
    },
});

更改数组第一项(对象)

因此破局的方法同样也是用到 $set$delete 函数。

**$set: **

<div id="app" class="container">
      <ul>
        <li v-for="n in arr">{{n}}</li>
      </ul>
      <button @click="$set(arr, '0', Math.random())">change first</button>
</div>

更改数组的第一项set1
更改数组的第一项set2

Dep


这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠 Dep 来解决。

Dep 的含义是Dependency,表示依赖的意思。

Vue会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep实例,这件事发生在 Observer 将普通对象转换成响应式对象的时候,每个Dep实例都有能力做以下两件事:

  • 记录依赖:是谁在用我。只有在模板中被用到的属性、对象、数组才会被记录,没有用到则不会被记录。

    <h1>Obj.a: {{obj.a}}, Obj.b: {{obj.b}}</h1> 
    <button @click="$delete(obj, 'a')">delete a</button>
    <button @click="$set(obj,'b', Math.random())">add b</button>
    <button @click="k ++">change k</button>
    <ul>
        <li v-for="n in arr">{{n}}</li>
    </ul>
    <button @click="$set(arr, '0', Math.random())">change first</button>
    
    obj: { // dep render(有被用到)
        a: 1, // dep render (有被用到)
    },
    k: 3, // dep (没有被用到)
    arr: [1, 2, 3, 4], // dep render (有被用到)
    
  • 派发更新:我变了,我要通知那些用到我的人

    <button @click="$delete(obj, 'a')">delete a</button> 
    <button @click="$set(obj,'b', Math.random())">add b</button>
    

    这里都触发的是 objrender,所以会 Dep 派发更新给 objrender 函数,让它重新运行。

    <button @click="$set(arr, '0', Math.random())">change first</button>
    

    这里触发的是 arrrender,所以会 Dep 派发更新给 arrrender 函数,让它重新运行。

当读取响应式对象的某个属性时,它会进行依赖收集:有人用到了我

当改变某个属性时,它会派发更新:那些用我的人听好了,我变了

Dep

Watcher


那么 Dep 又是怎么知道是谁在使用我呢?

vue 通过巧妙的方式解决了这个问题,它利用将 render 函数交由一个 watcher 来执行的方式,来达到目的。

具体是怎么做的呢?

首先我们知道,每个组件都有自己的 render 函数,而组件在运行时就会运行自身的 render 函数。这里 vue 将这个运行的权力交给了 watcherwatcher 是一个对象,它会先创建一个全局变量为自己本身,然后再去运行 render 函数,当运行过程中发生依赖记录时 dep.depend()Dep 会去查找当前 watcher 的全局变量所指向的对象并记录下来,从而 Dep 就知道当前时谁在用我了。

Dep 派发更新时,它会通知之前所有记录的 watcher ,告诉它们:我更新了。这些 watcher 就会重新调用 render 函数从而达到页面的更新,进而记录新的依赖。

Watcher

Scheduler 调度器


我们已经知道了,当 Dep 派发更新的时候呢,watcher 会调用 render 重新渲染。但是这就有可能导致函数频繁运行,从而导致效率低下。

因此就出现了 Scheduler 调度器,主要是用在 Dep 派发更新后,watcher 即将调用 render 函数的阶段(第一次使用 render 的时候,watcher 是立即执行的,后面就交由 Scheduler 来执行了)。watcher 会先把自己交给这个 Scheduler 调度器。

调度器维护着一个队列,该队列同一个 watcher 只会存在一次,队列中的 watcher 不会立即执行,它会通过一个 nextTick 的工具方法,把这些需要执行的 watcher 放入到事件循环的微队列里面,这个工具主要是通过 Promise 来实现的。

nextTick 通过 this.$nextTick 暴露给开发者

nextTick 的具体处理方式见:https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97

也就是说,当响应式数据变化时,render函数的执行是异步的,并且在微队列中

总体流程

总体流程

0

评论区