Vue 数据双向响应机制

本文深入探讨Vue.js框架中数据双向绑定的实现机制,包括Complite源码解析器的作用,Observer类如何利用Object.defineProperty劫持数据变化,Dep类收集依赖,以及Watcher类响应数据变化并更新视图的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue 数据双向响应机制

博主wx: -GuanEr-,加博主进前端交流群

参考资料(感谢各位前辈的分享和资料)

尤雨溪讲解 Vue 源码

Vue 源码解析-Vue 中文社区

小马哥 Vue 源码解析

小马哥 Vue 源码解析代码示范

vue-cli 源码

MDN

Vue 的特点是数据驱动视图,也就是说,数据变化时视图随之变化, 所以要先监听到数据的变化,然后再去响应依赖该数据的视图,Vue 使用 Object 的 defineProperty 函数劫持数据的变化,用 Complite 源码解析器解析我们编写的 Vue 代码, 用 Dep 类收集数据依赖,使用 Watcher 类响应数据变化并更新视图。

一、Complite 源码解析器

因为要结合 Watcher,所以 Complite 只在这里做简单的介绍,如果有时间单独写一篇.

我们在 .vue 的文件中编写的代码,或者创建 Vue 实例后编写的代码,部分如指令、函数,直接在 DOM 中渲染调用并计算变量等是不被浏览器直接识别和解析的,所以在代码正常渲染在浏览器并执行业务逻辑之前,Vue 要先将我们的代码进行编译。

编译流程如下(来自 Vue 中文社区 Vue 源码解析,全文链接在文章开头):

Alt
我们编写的 Vue 实例的结构代码其实是一个字符串,这个字符串被 VueComplite 源码解析器编译成抽象语法数AST(DOM 节点及属性以对象嵌套的形式存在),然后根据 AST 为每个节点生成对应的 render 函数,调用某个节点的 render 函数就能形成对应的 VNode

Complite 除了要要编译源码之外,还要捕获数据变化,也就是绑定 Watcher,然后根据 Watcher 类的,更新视图。

二、Observer

1. Object.defineProperty 函数 ( Object.defineProperty(obj, key, desc); )
  • 为一个对象添加属性,或者修改对象已有的属性,并返回这个对象。
  • Object 构造器直接调用,对象的实例不能调用。
  • 接受三个参数,obj 指要操作属性的源对象,key 指要操作的这个属性,desc 指要操作的属性的描述对象。
  • 可以定义 Symbol 类型的数据作为 key
  • 对象属性的描述中,非常重要的两个函数 getset
  • 更具体的内容,MDN 上有非常详细的说明,建议一看。
Object.defineProperty(obj, 'foo', {
  get() {
    // 每当 obj 访问 foo 属性时,get 函数会自动执行
    // 不传入参数,会传入 this,但是 this 不一定指向 obj
    // 该函数的返回值会被当做是 foo 的属性值
  },
  set(newVal) {
    // 当 foo 属性值被修改时,set 函数会自动执行
    // 接受一个参数 newVal,是为 foo 赋的新值
    // 会传入 赋值时的 this 对象
  }
});
2. Object.defineProperty 函数和 vue-cli 的关联

Vue 中,一个组件是一个 VueComponent 实例,每个实例有自己的 $data 对象,其中存储的是当前组件的数据列表,要让每个数据动态响应,就需要为每个数据添加 getset,来劫持数据值的改变。

// 为指定对象的每个键添加 setter 和 getter
function covert(obj) {
  const keyList = Object.keys(obj);
  keyList.forEach(key => {
    // _val 如果该属性已经存在,就是上一次的赋值,不存在为undifined
    // 每个 key 的操作都会形成一个闭包,所以这个闭包就成了单独存储对象某个属性值(_val)的位置
    let _val = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        // 在这里可以捕获到属性的变化,并限制取值操作,如果 return 一个固定值,那么 obj 的某个键就永远是这个值,重新赋值也没用
        return _val;
      },
      set(val) {
        // 这里可以对比属性的新旧值,并限制赋值操作
        _val = val;
      }
    });
  });
}
// -----------------------------------------------
// 调用
const data = {
  visible: true,
  num: 10
};
covert(data); // 为 data 的两个属性绑定 get 和 set
data.visible = false; // 调用 set
console.log(data.visible); // 调用 get
3. vue-cli 源码中的 Observer 类简介(极简)
class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 数组的另一套劫持方式
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    // 为每个键绑定 get 和 set
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    // 数组的特殊操作:由于数组数据类型的特殊性,数组的整体值的变更和劫持依旧在 set 和 get 中
    // 但是 vue 为 Array 这个类的原型函数们添加了劫持,也就是说当数组的值发生改变时,要调用原型函数之前,先处理我们需要的业务操作
  }
}
// ----------------------------------------------------------------
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  ...
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
    },
    set: function reactiveSetter (newVal) {
      ...
    }
  })
}

三、Dep

通过 Observer 类为数据们添加了 gettersetter 之后,就能观察到数据的变化,同时我们要改变跟这个数据有关系的一系列内容,可能是其他数据,也可能是页面渲染的内容。那我们就需要知道使用到这个数据的都有谁,才可以在当前数据变化时通知他们做响应。

1. 依赖、依赖收集和通知依赖
  • 某个数据 A 直接控制的内容,或者通过 A 计算得到的内容,都被称为依赖了 A
  • 都有谁依赖了 A,需要一个准确的计量,才能在 A 每次改变时,执行对应的操作,这种计量方式是一个数组,因为依赖 A 的数据很可能有多个。
  • 确定谁依赖了数据 A,并将这些依赖收集起来:依赖数据 A 一定要获取数据 A,所以在 Agetter 中,做依赖收集。
  • 数据 A 每次发生值的改变时,setter 会执行,所以在 Asetter 中通知依赖。
2. 依赖的操作

依赖可能被添加,也有可能伴随着组件卸载、销毁而被删除,所以依赖除了要被声明之外,还要有其他操作,依赖类 Dep 就是用来为每个数据创建依赖并处理依赖的。

3. vue-cli 中的 Dep
...
let uid = 0
class Dep {
  static target: ?Watcher; // 静态属性,Dep 构造器访问,Dep 的实例不能访问
  id: number; // 每个 Dep 实例唯一的id
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 某个数据的依赖列表
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // 当某个数据的值发生变化时,要循环这个数据的依赖列表,并且让他们相关的所有操作都更新一次
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
...
4. Observer 中调用 Dep(简写)
class Observer {
  constructor() {
    this.dep = new Dep();
  }
  ...
}

function defineReactive() {
  Object.defineProperty(obj, key, {
    get() {
      // get 被调用一次,都代表有一个数据依赖于当前数据,要向当前数据的依赖列表 subs 中添加一个依赖
      dep.depend()
      ...
    },
    set() {
      // set 被调用且值发生改变时,代表当前数据更新,那么依赖于当前数据的所有内容都要发生对应变化
      dep.notify()
      ...
    }
    ...
  });
}

四、Watcher

上面的 Dep 类只是对数据的依赖进行管理的一套方式,从 Dep 的源码中我们可以捕捉到,真正的被添加到 subs 数组中的依赖是 Watcher

1. 什么是 Watcher

某个表达式 Express,用到到了某个数据 A,我们就说 Express 订阅了 A,为了能让这个 ExpressA 每次发生变化时,都能动态的重新计算自己,我们就为它编写一个 update 方法来更新自己并做后续的数据渲染。每次 A 变化,都通知 Express 让它 update

Vue 中,到处都是这样的订阅与响应,所以产生了 Watcher 类,专门处理数据变化之后其依赖们的响应动作。

2. Watcher 需要具备的功能

以下三点来自小马哥源码解析及总结

  • 在自身实例化时往属性订阅器(dep)里面添加自己。
  • 自身必须有一个 update() 方法。
  • 待属性变动 dep.notify() 通知时,能调用自身的 update() 方法,并触发Compile中绑定的回调,则功成身退。
3. 结合 ObserverDep,整个数据双向响应流程如下:
  • 数据初始化,初始化了 dep 属性,继承了 Dep 类的 subs 属性,来承接依赖列表;
  • 为数据绑定了 gettersetter
  • 数据每次被调用都代表被订阅,getter 返回数据值的同时,向 subs 数组中添加了一个 Watcher
  • 数据值发生改变,调用 settersetter 设置数据值的同时,调用 Dep 提供的 notify 函数,来通知该数据的依赖做出响应;
  • notify 函数内部遍历依赖列表(即订阅者 Watcher 列表)subs 数组,调用每个订阅者的 update 函数,让其根绝数据新的值重新计算自己。

扫码拉你进入前端技术交流群

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锋利的二丫

如果对您有帮助,请博主喝杯奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值