由浅入深详解vue数据双向绑定原理

什么是数据双向绑定

数据双向绑定(Two-Way Data Binding) 是一种机制,用于实现视图(UI)和数据模型(Model)之间的双向同步更新。在双向绑定中,当数据发生变化时,视图会自动更新;同样,当视图(例如用户输入)发生变化时,数据模型也会自动更新。

Vue数据双向绑定原理

  1. 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter这样的 话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
  2. compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对 应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  3. Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:在自身实例化时往属性订阅器(dep)里面添加自己自身必须有一个 update()方法待属性变动 dep.notify()通知时,能调用自身的update()方法,并触发 Compile 中绑定的回调,则功成身退。
  4. MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听 自己的 model 数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新;视图交互变化(input)->数据model变更的双向绑定效果。
js 复制代码
class Vue {
  constructor(options) {
    this.$data = options.data;
    // 调用数据劫持的方法
    Observe(this.$data);
    // 属性代理,将 data 属性代理到 Vue 实例上
    ······
    // 调用模板编译的函数
    Compile(options.el, this);
  }
}
// 定义数据劫持的方法
function Observe(obj) {
    Object.defineProperty(obj, key, {
      get() {
        // Watcher 实例添加到dep.subs中,
        Dep.target && dep.addSub(Dep.target);
      },
      set(newVal) {
        Observe(value);
        // 通知每一个订阅者更新自己的文本
        dep.notify();
      },
    });
}
// 对 HTML 结构进行模板编译
function Compile(el, vm) {
    // 初始化视图
    .......
     new Watcher(vm, execResult[1], (newValue) => {
      // 更新视图
    });

}
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 所有的 watcher 都要存到这个数组中
    this.subs = [];
  }
  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher);
  }
  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }
}
// 订阅者的类
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    // 把创建的 Watcher 实例存到 Dep 实例的 subs 数组中
    Dep.target = this;
    // 进行一次取值操作出发getter函数,在getter函数中将watcher实例添加到dep收集依赖数组中
    key.split(".").reduce((newObj, k) => newObj[k], vm);
    Dep.target = null;
  }
  update() {
      ......
  }
}

Vue数据双向绑定原理代码拆解

1.框架入口(new MVVM())

  1. 接收 options 配置对象,包含 data 和 el 属性
  2. 对 data 对象进行数据劫持(Observe)
  3. 将 data 属性代理到 Vue 实例上,可以直接通过 vm.name 访问而不是 vm.$data.name
  4. 编译模板(Compile)
js 复制代码
class Vue {
  constructor(options) {
    this.$data = options.data;
    // 调用数据劫持的方法
    Observe(this.$data);
    // 属性代理
    Object.keys(this.$data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key];
        },
        set(newValue) {
          this.$data[key] = newValue;
        },
      });
    });
    // 调用模板编译的函数
    Compile(options.el, this);
  }
}

2.数据劫持(Observer)

核心作用:

1.使用 Object.defineProperty 为每个属性添加 getter/setter

2.getter 中进行依赖收集(Dep.target && dep.addSub

3.setter 中触发更新(dep.notify

4.递归处理嵌套对象
缺点:

1.无法劫持新增属性(需用 Vue.set)。

2.对数组需重写方法(如 push)。

js 复制代码
function Observe(obj) {
  // 这是递归的终止条件
  if (!obj || typeof obj !== "object") return;
  const dep = new Dep();

  // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key];
    // 把 value 这个子节点,进行递归
    Observe(value);
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newVal) {
        value = newVal;
        Observe(value);
        // 通知每一个订阅者更新自己的文本
        dep.notify();
      },
    });
  });
}

3.模板编译(Compile)

  1. 模板编译和节点替换
    将模板中的文本节点(比如 {{ name }})提取出来,并替换成对应的动态数据(如 vm.name)。这部分用到了正则表达式来查找模板中的插值表达式并替换它们。
  2. 响应式绑定
    对于文本节点和 input 元素,它通过 Watcher 类来实现响应式更新。Watcher 会监听数据变化,当数据发生变化时,自动更新对应的 DOM 元素。
  3. v-model 双向绑定
    对于 input 元素,代码实现了 v-model 的基本功能。通过获取 v-model 属性值(例如 v-model="name"),将 input 元素的值与 vm.name 绑定,并在用户输入时更新 vm.name
  4. 性能优化
    使用 document.createDocumentFragment() 提高 DOM 操作的性能,因为 fragment 是内存中的一个轻量级 DOM 对象,不会触发浏览器的重排和重绘。
js 复制代码
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el);
  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment();
  // 将根元素的子节点全部移入文档片段(避免频繁操作真实 DOM)
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode);
  }
  // 进行模板编译
  replace(fragment);
  vm.$el.appendChild(fragment);
  // 负责对 DOM 模板进行编译的方法
  function replace(node) {
    // 用正则 /\{\{\s*(\S+)\s*\}\}/ 提取表达式(如 {{ name }} 中的 name)
    const regMustache = /\{\{\s*(\S+)\s*\}\}/;
    // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
    if (node.nodeType === 3) {
     // 文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      const text = node.textContent;
      // 进行字符串的正则匹配与提取
      const execResult = regMustache.exec(text);
      if (execResult) {
        // 通过 reduce 链式访问嵌套属性(如 info.address)
        const value = execResult[1]
          .split(".")
          .reduce((newObj, k) => newObj[k], vm);
        node.textContent = text.replace(regMustache, value);
        // 在这个时候,创建 Watcher 类的实例
        new Watcher(vm, execResult[1], (newValue) => {
          node.textContent = text.replace(regMustache, newValue);
        });
      }
      // 终止递归的条件
      return;
    }
    // 判断当前的 node 节点是否为 input 输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === "INPUT") {
      // 得到当前元素的所有属性节点
      const attrs = Array.from(node.attributes);
      const findResult = attrs.find((x) => x.name === "v-model");
      if (findResult) {
        // 获取到当前 v-model 属性的值   v-model="name"    v-model="info.a"
        const expStr = findResult.value;
        const value = expStr.split(".").reduce((newObj, k) => newObj[k], vm);
        node.value = value;
        // 创建 Watcher 的实例
        new Watcher(vm, expStr, (newValue) => {
          node.value = newValue;
        });
        // 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
        node.addEventListener("input", (e) => {
          const keyArr = expStr.split(".");
          const obj = keyArr
            .slice(0, keyArr.length - 1)
            .reduce((newObj, k) => newObj[k], vm);
          const leafKey = keyArr[keyArr.length - 1];
          obj[leafKey] = e.target.value;
        });
      }
    }
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child));
  }
}

4.发布者-订阅者模式(Dep、Watcher)

  1. Dep(依赖)类 负责管理订阅者(Watcher 实例),通过 addSub 添加订阅者,并通过 notify 通知所有订阅者更新。
  2. Watcher(观察者)类 存储一个回调函数,当被 Dep 通知时,执行该回调。
  3. Dep 作为调度中心,统一管理和触发依赖更新
js 复制代码
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 今后,所有的 watcher 都要存到这个数组中
    this.subs = [];
  }

  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher);
  }

  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }
}

// 订阅者的类
class Watcher {
  // cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
  // 但是,只知道如何更新自己还不行,还必须拿到最新的数据,
  // 因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
  // 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
  // 因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
    // Dep.target作用是一个静态属性,表示当前正在执行的 watcher 实例,确保在执行 getter 函数时,能 
    够把 watcher 实例添加到 dep 的 subs 数组中
    Dep.target = this;
    // 这段代码的意义就是进行一次取值操作出发getter函数,在getter函数中将watcher实例添加到dep收集依
    // 赖数组中
    key.split(".").reduce((newObj, k) => newObj[k], vm);
    Dep.target = null;
  }
  // watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
  update() {
    const value = this.key.split(".").reduce((newObj, k) => newObj[k], this.vm);
    this.cb(value);
  }
}
相关推荐
kymjs张涛2 小时前
零一开源|前沿技术周刊 #11
前端·javascript·vue.js
anyup2 小时前
🚀 2025 最推荐的 uni-app 技术栈:unibest + uView Pro 高效开发全攻略
前端·vue.js·uni-app
掘金012 小时前
🚀 Vue 中使用 `@vueuse/core` 终极指南:从入门到精通
vue.js
掘金012 小时前
🔥 Vue 开发者的“外挂”库: 让你秒变超级赛亚人!🔥
javascript·vue.js·前端框架
北辰浮光3 小时前
[Element-plus]动态设置组件的语言
javascript·vue.js·elementui
李大玄3 小时前
一套通用的 JS 复制功能(保留/去掉换行,兼容 PC/移动端/微信)
前端·javascript·vue.js
russo_zhang3 小时前
【Nuxt】一行代码实现网站流量的实施监控与统计
vue.js·nuxt.js
泉城老铁3 小时前
vue如何实现行编辑
前端·vue.js
好好好明天会更好3 小时前
vue项目中pdfjs-dist实现在线浏览PDF文件
前端·vue.js