代码实现vue的双向绑定(一):订阅者和发布者

参看相关文章以及 vue 源码,自己去实现 vue 的双向绑定,希望如果还对双向绑定比较懵懂的同学也可以照着代码自己打一遍,加深对 vue 双向绑定的理解。动手之前需要了解以下知识点:

  • 要有 Vue 项目的实践经历,清楚 Vue 的使用方法。
  • es6 中 Class 的使用方法。


参考文档

剖析Vue原理&实现双向绑定MVVM

友情提示:本文可以伴着vue源码一起看,对源码的理解会更加深入


实现思路

vue.js 实现双向绑定就是使用数据劫持结合发布者-订阅者,通过 **Object.defineProperty()**来劫持每个属性的 setter getter ,在数据变动的时候发布消息给订阅者,触发相应的监听回调。

双向绑定实现的核心就是通过 **Object.defineProperty()**,对 data 每个属性进行了 get ,set 的拦截。
只用 Object.defineProperty()已经可以实现双向绑定,但是效率非常低,需要结合观察者模式(提升双向绑定的效率),观察者模式是一对多的一种模式,就是修改某一个 data 的值,页面上只要用了这个 data 的地方都进行更新。

主要的实现思路就是

  1. 实现一个数据监听器 Observer,对 data 的所有属性进行监听,如果有变动就拿到最新的值,然后通知订阅者。数据监听器主要的方法就是Object.defineProperty监听值的变化,再去维护一个数组收集订阅者,数据变动的时候去调用这些订阅者的update()方法。
  2. 实现一个指令解析器 Compile,这个就是对元素节点进行扫描解析,去替换数据(本文不实现,只进行模拟)
  3. 实现一个订阅者 Watcher,订阅并且收到每个属性变动的通知,执行相应回调函数,从而去更新视图。



代码实现

本文主要参考 vue2.x 源码手把手实现观察者 Observer 和订阅者 Watcher,暂不实现 vue 的指令解析,用 JS 代码模拟初始化和数据改变。模拟 vue 实例如下,声明一个data模拟 vue 中的data对象,对data建立观察者,新建Watcher去订阅data.msg的变化去看 vue 的双向绑定是如何运作的。

<!-- index.html -->

<script type="module">
  import observe from "./observer.js";
  import Watcher from "./watcher.js";

  // 模拟 vue 中的 data 对象以及改变
  var data = {
    msg: "Hello",
  };
  console.log('begin', data);

  // 模拟初始化 Data
  observe(data);

  // 模拟根据某些规则新建 Wacther
  // 比如 v-bind、{{ xxx }} 等等
  new Watcher(data, "msg", () => {
    console.log("更新视图");
  });

  // 模拟数据更改
  data.msg = "Hi";
</script>



实现一个观察者 Observer

首先是熟悉Object.defineProperty(obj, prop, descriptor),三个参数对象、属性、改变方法(get/set)。当我们用这个方法定义一个值的时候,调用时就使用了里面的 get 方法,赋值的时候就用了里面的 set 方法。具体的使用查看 MDN 文档,这里不详细说明。

<!-- index.html -->

<script type="module">
  import observe from "./observer.js";

  // 模拟 vue 中的 data 对象以及改变
  var data = {
    msg: "Hello",
  };
  console.log('begin', data);

  // 模拟初始化 Data
  observe(data);

  // 模拟数据更改
  data.msg = "Hi";
</script>


定义observe函数对data中所有属性进行遍历,并且使用Object.defineProperty对每个属性的get方法和 set方法进行劫持(这里就是所谓的数据劫持)。

// observe.js

export default function observe(data) {
  // 这里先不考虑 vue 中 data 为函数的情况
  if (!data || typeof data !== "object") {
    return;
  }

  // 对data中所有数据遍历
  Object.keys(data).forEach((key) => {
    defindReactive(data, key, data[key]);
  });
}

function defindReactive(data, key, val) {
  observe(val); // 递归遍历子属性
  // Object.defineProperty
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: false,
    get: function () {
      console.log("触发 getter", val);
      return val;
    },
    set: function (newVal) {
      console.log("触发 setter", val, "->", newVal);
      val = newVal;
    },
  });
}


执行之后看到效果,我们对 data.msg 进行修改的时候会触发自己定义的set方法

在这里插入图片描述

从上面的思路中可以知道,我们需要在set方法执行(也就是值发生改变的)的时候,去通知所有订阅者去执行一些更新动作。那么如何去管理订阅者,我们可以通过一个数组,将所有订阅者都放在这个数组里,当需要更新的时候,遍历这个数组里面的订阅者,依次去执行自己的更新动作。

// dep.js
// 维护一个数组来收集订阅者,数据变动的时候触发 notify() 通知各个订阅者,再调用订阅者的 update() 方法去更新数据
export default class Dep {
  constructor() {
    this.subs = []; // 此数组用来收集订阅者
  }

  // 将 Watcher 添加到订阅者列表中
  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}


var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在getter里面将订阅者添加到数组里面,用Dep.target来指向当前正在执行操作的Watcher

// observe.js
import Dep from "./dep.js"; // 用数组维护订阅者

// ...

function defindReactive(data, key, val) {
  var dep = new Dep(); // 用数组维护订阅者

  observe(val); // 递归遍历子属性

  // Object.defineProperty
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
  Object.defineProperty(data, key, {
    enumerable: true, // 可枚举
    configurable: false, // 不能再 define
    get: function () {
      console.log("触发 getter", val);
      Dep.target && dep.addSub(Dep.target) 
      // console.log(dep)
      return val;
    },
    set: function (newVal) {
      console.log("触发 setter", val, "->", newVal);
      val = newVal;
      dep.notify(); // 通知所有订阅者
    },
  });
}


数据变化的时候通知订阅者更新这一步已经完成了,但是订阅者是什么?怎么添加订阅者?这两个问题我们还没有实现。接下来就要先实现我们一直说的订阅者


实现订阅者 Watcher

index.html中直接去创建一个Watcher去模拟 vue 中初始化的时候根据某些规则去构造不同的Watcher(比如 v-bind{{ xxx }}等等,这里不进行实现)。

// index.html

<script type="module">
  import observe from "./observer.js";
  import Watcher from "./watcher.js";

  // 模拟 vue 中的 data 对象以及改变
  var data = {
    msg: "Hello",
  };
  console.log('begin', data);

  // 模拟初始化 Data
  observe(data);

  // 模拟根据某些规则新建 Wacther
  // 比如 v-bind、{{ xxx }} 等等
  new Watcher(data, "msg", () => {
    console.log("更新视图");
  });

  // 模拟数据更改
  data.msg = "Hi";
</script>


实现 Observer 的时候我们知道,订阅者需要满足的条件:

  • 要有一个 update方法去做更新动作,参数有一个回调函数,不同的Watcher可以在更新的时候做不同的动作(比如更新视图、用户自己定义的 watch 的回调函数等)
  • 需要记录当前的值
  • 需要将自己添加到dep里面
    • 初始化的时候要触发getter
    • Dep.target指向自己,触犯完getter之后把Dep.target清空
// watcher.js
import Dep from "./dep.js";

// 在自身实例化的时候往 dep 中添加自己;当属性变动 dep.notice() 通知时,调用自身的 update 方法,触发更新视图
export default class Watcher {
  constructor(vm, exp, cb) {
    this.cb = cb // 构建 Wacher 的时候可以传入不同回调函数,在更新时执行不同的操作
    this.vm = vm // 实例(在这里就是 index.html 的 data 对象)
    this.exp = exp // 相当于 key 值

    this.value = this.get() // 实例化的时候触发 Observer 的 get,往 dep 添加自己
  }

  update() {
    this.run() 
  }

  run(){
    const value = this.get() // 取得最新值
    const oldValue = this.value
    if(value !== oldValue) {
      this.value = value
      console.log('获得最新值:', value, ',去更新视图啦')
      this.cb()
    }
  }

  get() {
    Dep.target = this // 将 Dep.target 指向自己
    const value = this.vm[this.exp] // 触发数据的 getter,使得可以往数据里添加订阅者
    Dep.target = null // 添加到数组后将指向清空

    return value
  }

}


执行一下代码就能看到在new Watcher的时候触发了一次 getter,改变数据执行 run()的时候又调用了一次getter,但是每一次调用getter的时候都将同一个Watcher放到了订阅者数组dep中,明显这是可以进行优化的地方。
在这里插入图片描述

所以我们需要给每一个dep标注一个唯一索引id,每个Watcher都记录下自己所在depid,并且判断我要加入的dep是否重复。代码优化后如下

// watcher.js

export default class Watcher { 
	constructor(vm, exp, cb) {
    // ...
    this.depIds = {} // 记录 depId
    this.value = this.get() // 实例化的时候触发 Observer 的 get,往 dep 添加自己
  }
  
  // 需要判断 dep.id 是否已经在当前 Watcher 的 depIds 里面
  // 如果在 oberser 的 getter 里面直接调用 addSub,会将 Watcher 重复添加到 dep 中
  addDep(dep) {
    if(!this.depIds.hasOwnProperty(dep.id)) {
      dep.addSub(this)
      this.depIds[dep.id] = dep
    }
  }
}
// dep.js

let uid = 0;
export default class Dep {
	constructor() {
    this.id = uid++
    // ...
  }
  
  // ...
  
  // 调用目标 Watcher 的 addDep 方法
  depend() {
    Dep.target.addDep(this)
  }
}
// observe.js

function defindReactive(data, key, val) {
  var dep = new Dep(); // S2 用数组维护订阅者
  observe(val); // 递归遍历子属性

  Object.defineProperty(data, key, {
		// ...
    get: function () {
      console.log("触发 getter", val);
      // 不能直接调用 addSub,不然每次触发 getter 都会重复添加 Watcher
      // Dep.target && dep.addSub(Dep.target) 

      if(Dep.target) {
        dep.depend()
      }
      return val;
    },
		// ...
  });
}


执行代码可以看到dep下面只有一个 Watcher
![image.png](https://img-blog.csdnimg.cn/img_convert/10276ff6fe4d711338eeaf6ad8517e75.png#clientId=u1d57928f-c2c6-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=277&id=ub86e16e5&margin=[object Object]&name=image.png&originHeight=277&originWidth=328&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13106&status=done&style=none&taskId=u0632802e-d10c-431c-9ceb-6e558abeb5a&title=&width=328)​

到这里的双向绑定原理中的订阅者-发布者以及数据劫持的部分已经实现,当然还需要一个解析器,去解析代码中需要双向绑定的地方。


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue双向原理主要是通过数据劫持和发布订阅模式来实现的。 当创建Vue实例时,Vue会遍历data选项中的所有属性,使用Object.defineProperty方法将这些属性转换成getter和setter,并且在getter中收集依赖,在setter中触发依赖,从而实现对数据的监听。 当数据发生变化时,setter会通知订阅者更新视图,订阅者会重新渲染视图,从而实现数据的双向。 以下是Vue双向原理代码的简单实现: // 数据劫持 function observe(obj) { if (!obj || typeof obj !== 'object') { return; } Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } function defineReactive(obj, key, val) { observe(val); // 递归子属性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { console.log(`get ${key}: ${val}`); return val; }, set: function reactiveSetter(newVal) { console.log(`set ${key}: ${newVal}`); val = newVal; }, }); } // 发布订阅模式 class Dep { constructor() { this.subs = []; // 存储所有的订阅者 } addSub(sub) { this.subs.push(sub); } notify() { this.subs.forEach((sub) => { sub.update(); }); } } // 订阅者 class Watcher { constructor(obj, key, cb) { this.obj = obj; this.key = key; this.cb = cb; Dep.target = this; // 将当前订阅者指向Dep静态属性target this.value = obj[key]; // 触发getter,收集依赖 Dep.target = null; } update() { const newVal = this.obj[this.key]; if (newVal !== this.value) { this.value = newVal; this.cb && this.cb(newVal); } } } // 测试代码 const vue = { data: { message: 'Hello, Vue!', }, }; observe(vue.data); new Watcher(vue.data, 'message', (newVal) => { console.log(`watcher1: ${newVal}`); }); new Watcher(vue.data, 'message', (newVal) => { console.log(`watcher2: ${newVal}`); }); vue.data.message = 'Hello, World!'; // 输出: // get message: Hello, Vue! // set message: Hello, World! // watcher1: Hello, World! // watcher2: Hello, World!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值