认识双向绑定在框架中的作用

因为 Vue 是数据双向绑定的框架,而整个框架的由三个部分组成:

  • 数据层(Model):应用的数据及业务逻辑,为开发者编写的业务代码;
  • 视图层(View):应用的展示效果,各类UI组件,由template 和 css 组成的代码;
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来;

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM。(详细的MVVM知识不在讲解范围内,只需要清楚双向绑定在此架构中作用和位置即可)
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它 是什么,便可以进一步了解数据绑定的原理。

理解ViewModel

它的主要职责就是:

  1. 数据变化后更新视图;
  2. 视图变化后更新数据;

那么,就可以得出它主要由两个部分组成:

  1. 监听器(Observer):观察数据,做到时刻清楚数据的任何变化,然后通知视图更新;
  2. 解析器(Compiler):观察UI,做到时刻清楚视图发生的一切交互,然后更新数据;

然后把二者组合起来,一个具有数据双向绑定的框架就诞生了。

双向绑定如何实现的?

实现监听器

确保它是一个独立的功能,它的任务就是监听数据的变化,并提供通知功能。
监听数据变化常见的方式有三种:

  • 观察者模式(发布+订阅);
  • 数据劫持;
  • 脏检查;

第三种,是 AngularJS 最早所提供的监听数据变化方式,而 Vue 是采用前两者的组合,那么让我们来看看它们是怎么被运用的。

观察者模式

观察者模式是一种对象行为模式。它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
在观察者模式中,主导的是起通知作用的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅并接收通知
举例代码如下:

/**
 发布者
 */
function Subject() {
  
  // 单个发布者的所有订阅者
  this.observers = [];
  
  // 添加一个订阅者
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  
  // 通过所有的订阅者
  this.notify = function(value) {
    this.observers.forEach(callback => callback(value));
  };
}

/**
 * 订阅者
 */
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

// ====

// 手动更新数据
function setData(data, key, value) {
    data[key] = value;

    // 通知此值的所有订阅者,数据发生了更新
    messageQueue[key].notify(value);
}

// ====

// 消息队列
const messageQueue = {};

// 数据
const myData = { value: "" };

// 将每个数据属性添加可订阅的入口
for (let key in myData) {
  messageQueue[key] = new Subject();
}

// 订阅 value 值的变化
Observer(messageQueue, "value", value => {
  console.warn("value updated:", value);
});

// 更新数据
setData(myData, "value", "hello world.");
setData(myData, "value", 100);
setData(myData, "value", true);

消息队列(queue)

我们可以看到,单纯的订阅和发布功能,它们彼此是独立存在的,因此还需要一个消息队列来关联他们。
上面的例子中,消息队列是作为一个全局存储变量,而在框架中则是封装起来的,每个 new Vue() 都有独立的一个队列,在下面我们将具体演示。

数据劫持

其实,就数据监听来说,观察者就已经满足了需求。但是,为什么和Vue不一样呢?因为 Vue 进行了优化,添加了数据劫持。
2009 年发布的 ECMAScript 5 中新增了一个 Object.definePropotype 的特性(具体使用不在此文章讲解范围内,请自行了解 developer.mozilla.org/zh-CN/docs/… ),能够定义对象属性的 getter 和 setter ,这可就厉害了,要知道 JavaScript 中一切皆对象。
那么我们的 setData(myData, ‘value’, 100); 就可以替换成 myData.value = 100; 的编写方式。从语法和使用上都变的更简单。
以下是变动后的代码:

// 发布者
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback => callback(value));
  };
}

// 订阅者
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

// ====

// 数据拦截器
function Watcher(data, queue) {
  for (let key in data) {
    let value = data[key];
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: () => value,
      set: newValue => {
        value = newValue;

        // 通知此值的所有订阅者,数据发生了更新
        queue[key].notify(value);
      }
    });
  }
  return data;
}

// ====

// 消息队列
const messageQueue = {};

// 数据
const myData = Watcher({ value: "" }, messageQueue);

// 将每个数据属性都添加到观察者的消息队列中
for (let key in myData) {
  messageQueue[key] = new Subject();
}

// 订阅 value 值的变化
Observer(messageQueue, "value", value => {
  console.warn("value updated:", value);
});

// 更新数据
myData.value = "hello world.";
myData.value = 100;
myData.value = true;

当然,ES2015 已经出来有点时间了,所以应尽用新的解决方案 Proxy(具体的使用请自行了解哦 developer.mozilla.org/zh-CN/docs/… ),讲到它主要是因为 Vue3 就采用了此方案。至于性能和具体的差异,之后有机会再单独分享,这里就不细说,让我们看看新的语法怎么写呢?

代码:

// 发布者
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback => callback(value));
  };
}

// 订阅者
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

// ====

// 数据拦截器 - 代理方式
function ProxyWatcher(data, queue) {
  return new Proxy(data, {
    get: (target, key) => target[key],
    set(target, key, value) {
      target[key] = value;

      // 通知此值的所有订阅者,数据发生了更新
      queue[key].notify(value);
    }
  });
}

// ====

// 消息队列
const messageQueue = {};

// 数据
const myData = ProxyWatcher({ value: "" }, messageQueue);

// 将每个数据属性都添加到观察者的消息队列中
for (let key in myData) {
  messageQueue[key] = new Subject();
}

// 订阅 value 值的变化
Observer(messageQueue, "value", value => {
  console.warn("value updated:", value);
});

// 更新数据
myData.value = "hello world.";
myData.value = 100;
myData.value = true;

以上,我们已经完成双向绑定中的数据层的功能,现在任何的数据变化,我们都可以及时的知道,并关联任何想要做的事情,比如更新视图。

接下来就是模板的操作了,也就是 Compile 模板解析功能。

模板解析(实现视图到数据的绑定)

对于DOM的操作,目前常见的方式有:

  • 原生或者基于库的DOM操作;
  • 将DOM转换为Virtual DOM,然后进行对比与更新;
  • 使用原生的Web Component技术;

以上三种的差异与性能等,之后有机会再单独分享。在Vue中使用的是 Virtual DOM 的方式,因为它比直接操作 DOM 所消耗的性能要少很多,也不存在 Web Component 的兼容性。
这三中方式的本质都是更新 DOM 的展示效果,只是方式不同而已,为了更简单的说明双向绑定的原理,我们就采用第一种方式。虚拟DOM是有很多独立的第三方库,如果有兴趣同学可以去研究哦。

解析器与DOM操作

这里的主要任务是:

  • 解析模板中所有的特定特性,例如:v-model、v-text、{{ }}语法等;
  • 关联数据展示到DOM;
  • 关联事件绑定到DOM;
  • 因此,下面的功能只能需要满足:展示数据到模板上。

代码如下:

<div id="app">
  <input v-model="value" />
  <p v-text="value"></p>
</div>
// 模板解析
function Compile(el, data) {

  // 关联自定义特性
  if (el.attributes) {
    [].forEach.call(el.attributes, attribute => {
      if (attribute.name.includes('v-')) {
        Update[attribute.name](el, data, attribute.value);
      }
    });
  }

  // 递归解析所有DOM
  [].forEach.call(el.childNodes, child => Compile(child, data));
}

// 自定义特性对应的事件
const Update = {
  "v-text"(el, data, key) {

    // 初始化DOM内容
    el.innerText = data[key];
  },
  "v-model"(input, data, key) {

    // 初始化Input默认值
    input.value = data[key];

    // 监听控件的输入事件,并更新数据
    input.addEventListener("keyup", e => {
      data[key] = e.target.value;
    });
  }
};

// ====

// 数据
const myData = { value: "hello world." };

// 解析
Compile(document.querySelector("#app"), myData);

目前我们定义的 DOM 解析,并没有关联数据监听,让我们来完成它!

完整的双向绑定

代码如下:

// 发布者
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback => callback(value));
  };
}

// 订阅者
function Observer(queue, key, callback) {
  queue[key].attach(callback);
}

// ====

// 数据拦截器 - 代理方式
function ProxyWatcher(data, queue) {
  return new Proxy(data, {
    get: (target, key) => target[key],
    set(target, key, value) {
      target[key] = value;

      // 通知此值的所有订阅者,数据发生了更新
      queue[key].notify(value);
    }
  });
}

// ====

// 模板解析
function Compile(el, data) {

  // 关联自定义特性
  if (el.attributes) {
    [].forEach.call(el.attributes, attribute => {
      if (attribute.name.includes('v-')) {
        Update[attribute.name](el, data, attribute.value);
      }
    });
  }

  // 递归解析所有DOM
  [].forEach.call(el.childNodes, child => Compile(child, data));
}

// 自定义特性对应的事件
const Update = {
  "v-text"(el, data, key) {

    // 初始化DOM内容
    el.innerText = data[key];

    // 创建一个数据的订阅,数据变化后更新展示内容
    Observer(messageQueue, key, value => {
        el.innerText = value;
    });
  },
  "v-model"(input, data, key) {

    // 初始化Input默认值
    input.value = data[key];

    // 监听控件的输入事件,并更新数据
    input.addEventListener("keyup", e => {
      data[key] = e.target.value;
    });

    // 创建一个订阅
    Observer(messageQueue, key, value => {
      input.value = value;
    });
  }
};

// ====

// 消息队列
const messageQueue = {};

// 数据
const myData = ProxyWatcher({ value: "hello world." }, messageQueue);

// 将每个数据属性都添加到观察者的消息队列中
for (let key in myData) {
    messageQueue[key] = new Subject();
}

// ====

// 解析+关联
Compile(document.querySelector("#app"), myData);

如此,一个非常简单的 MVVM 功能就完成了。当然,它仅仅是为了讲解原理非编写的,如果要做成向 Vue 这样的成熟框架,就需要将各个核心封装成模块,进行更扩展性的定义。
如果再简单的封装一下,看上去就是一个极简的Vue了。
代码如下:

// 观察者功能
// 发布者
function Subject() {
  this.observers = [];
  this.attach = function(callback) {
    this.observers.push(callback);
  };
  this.notify = function(value) {
    this.observers.forEach(callback => callback(value));
  };
}
// 订阅者
function Observer(queue) {
  this.queue = queue
  this.add = function(key, callback) {
    this.queue[key].attach(callback);
  }
}

// ====

// 数据拦截器
// 监听数据更新 - 代理方式
function ProxyWatcher(data, queue) {
  return new Proxy(data, {
    get: (target, key) => target[key],
    set(target, key, value) {
      target[key] = value;

      // 通知此值的所有订阅者,数据发生了更新
      queue[key].notify(value);
    }
  });
}

// ====

// 模板解析
function Compile(el, vm) {

  // 关联自定义特性
  if (el.attributes) {
    [].forEach.call(el.attributes, attribute => {
      if (attribute.name.includes('v-')) {
        Update[attribute.name](el, vm.data, attribute.value, vm);
      }
    });
  }

  // 递归解析所有DOM
  [].forEach.call(el.childNodes, child => Compile(child, vm));

  return el
}

// 自定义特性对应的事件
const Update = {
  "v-text"(el, data, key, vm) {

    // 初始化DOM内容
    el.innerText = data[key];

    // 创建一个数据的订阅,数据变化后更新展示内容
    vm.observer.add(key, value => {
      el.innerText = value;
    });
  },
  "v-model"(input, data, key, vm) {

    // 初始化Input默认值
    input.value = data[key];

    // 创建一个订阅
    vm.observer.add(key, value => {
      input.value = value;
    });

    // 监听控件的输入事件,并更新数据
    input.addEventListener("keyup", e => {
      data[key] = e.target.value;
    });
  }
};

// ====

// 封装
function Vue({ el, data }) {

  // initProxy
  this.messageQueue = {};
  this.observer = new Observer(this.messageQueue)
  this.data = ProxyWatcher(data, this.messageQueue);

  // initState
  for (let key in myData) {
    this.messageQueue[key] = new Subject();
  }

  // initRender
  // initEvents
  this.el = Compile(el, this);
}

// ====

// 数据
const myData = { value: "hello world." };

// 实例
const vm = new Vue({
  el: document.querySelector("#app"),
  data: myData
});
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值