MVVM 双向数据绑定之基础实现

1 概述

原理部分请参考另一篇文章:MVVM 双向数据绑定之核心原理

双向数据绑定的实现需要依赖于数据改动监听,DOM 改动监听,观察者模式规范代码,对象封装,若使用 v-model、{{}} 类的语法,还需要做模板解析。目的是为了实现下面功能:

// Vue 实现
<script src="Vue.js"></script>

// 组件
<div id="app">
  <p>{{ message }}</p>
  <input v-model="message">
</div>

// 初始化组件
<script>
  var app6 = new Vue({
    el: '#app',
    data: {
      message: 'Hello Vue!'
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

例子

开整!

2 构造 Vue 组件

本处我们使用 Vue,是为了向 Vue 致敬,我是从 Vue 开始使用前端的功能框架的。构造器 Vue 用来初始化组件,代码如下:

// Vue 组件构造器
function Vue(options) {
  // 确定 DOM
  this.elem = document.querySelector(options.el)
  // 记录数据
  this.data = options.data || {}
  // 监视数据改动
  this.observe()
  // 编译模板
  this.compile()
}
1
2
3
4
5
6
7
8
9
10
11

构造器中,先确定当前操作的DOM对象,再将模型数据记录的到当前对象上,然后调用 observe() 方法启动模型数据监听,最后调用 compile() 方法编译模板,保证语法 v-model 和 双大括号 {{}} 可用,同时注册观察者对象保证模型数据改动 DOM 随之更新。

3 观察者模式实现模型数据的改动监听

在监视模型数据改动和编译模板前,我们先看观察者的实现。

观察者,用于监听模型数据的变动,并在数据变动时更新对应的 DOM。也就意味着,每当 DOM 上出现了一次对模型数据的使用,就需要一个观察者来监听和更新。由于我们需要同时监听多个模型的属性,因此将多个观察者通过所监听的属性名,分别存储在不同的集合中。

先看观察者对象的代码实现:

// 观察者构造器
function Watcher(attr, update) {
  this.attr = attr
  this.update = update
  // 将当前观察者加入观察者集合
  watcherSet.register(this)
}
1
2
3
4
5
6
7

观察者的结构很简单,两个属性分别是:

  • attr,所监听的模型属性
  • update,当模型属性发生改动时,所执行的更新 DOM 的操作

观察者对象的实例化是发生在我们编译模板的时候,当模板中使用了模型属性,则实例化一个对应的观察者,可以在后边的 compile() 方法实现中看到。

再看观察者对象集合,由于每当 DOM 上出现了一次对模型数据的使用,就需要一个观察者来监听和更新,因此会有很多观察者,需要集中管理,就出现了观察者对象集合,看代码实现:

// 观察者集合构造器
function WatcherSet() {
  this.members = {}
}
// 注册观察者
WatcherSet.prototype.register = function(watcher) {
  // 每个属性使用一个 Set 集合,来存储观察者
  if (this.members[watcher.attr]) {
    this.members[watcher.attr].add(watcher)
  } else {
    this.members[watcher.attr] = new Set([watcher])
  }
}
// 通知某个属性的更新方法执行
WatcherSet.prototype.notify = function(attr) {
  // 调用当前属性上每个观察者的 update 方法,更新 DOM
  this.members[attr].forEach((watcher) => {
    watcher.update()
  })
}

// 观察者集合
var watcherSet = new WatcherSet()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

观察者集合有两个方法,一个是 register(),用于注册某个观察者对象,另一个是 notify(),用于更新 DOM。其中 register() 方法是在实例化 Watcher 的时候注册的,而 notify() 方法会在模型属性发生改动时调动。

有了观察者,我们可以去监听模型数据改动和编译模板了。先去监听模型数据改动。

4 Vue 实例的 observe() 方法监听模型数据

observe() 代码如下:

// 监视模型数据
Vue.prototype.observe = function () {
  // 遍历全部的 data 模型数据
  Object.keys(this.data).forEach(attr => {
    Object.defineProperty(this, attr, {
      get() {
        return this.data[attr]
      },
      set(value) {
        this.data[attr] = value
        // 数据修改的时候,通过监视器模式,监视器改变
        watcherSet.notify(attr)
      }
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

本例的实现,采用了代理模式,就是为了实现 vue.message 来代理访问 vue.data.message 。监听的核心在于属性的 get() 和 set() 方法,我们可以在 set() 方法中来触发观察者的更新操作。

最后就需要模板编译了,就是实现 compile() 方法。

5 Vue 实例的 compile() 方法编译 HTML 模板

所谓的编译模板,就是将 Vue 的模板语法,更新成浏览器可以解析的 DOM 语法。让 v-model, {{}} 语法变得有意义。compile() 的实现如下:

// 编译模板
Vue.prototype.compile = function() {
  let vm = this
  let pattern = /{{\s*(\w+)\s*}}/
  // 遍历全部的 DOM 节点
  Array.from(vm.element.children).forEach(node => {
    // 处理 v-model 属性
    if (node.hasAttribute("v-model")) {
      let attr = node.getAttribute("v-model")
      node.value = vm[attr]
      // DOM 更新修改模型
      node.addEventListener("input", e => {
        vm[attr] = e.target.value
      }) 
      // 增加一个观察者
      new Watcher(attr, () => {
        node.value = vm[attr]
      }) 
    }
    // 处理 {{ }} 
    else if (pattern.test(node.innerHTML)) {
        // 利用正则表达式,找到内容中 {{ }} 的部分,替换为当前模型的值
        node.originInnerHTML = node.innerHTML
        let match = node.originInnerHTML.match(pattern)
        let attr = match[1]
        node.innerHTML = node.originInnerHTML.replace(pattern, vm[attr]);
        // 增加一个观察者
        new Watcher(attr, () => {
          node.innerHTML = node.originInnerHTML.replace(pattern, vm[attr]);
        }) 
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

代码比较多,流程是遍历全部的 DOM 节点,然后分别处理节点的 v-model 属性和内容中的 {{}} 语法。

其中 v-model 的处理,除了设置对应元素的 value 属性外;还需要一个 input 事件,来实现当 input 元素输入新内容时,同时更新模型属性;还需要注册一个观察者,来处理当模型属性改变时对应的 input DOM 的 value 也同时更新。

其中 {{}} 的处理,需要通过正则表达式确定其内部对应的模型属性;还需要注册一个观察者,来处理当模型属性改变时对应的内容也同时更新。但不需要中的 DOM 事件了。

至此,以一个基本的 MVVM 的实现就完成了,可以实现 模型和视图 的双向数据绑定了。

完整代码可以移步 Github 来获取:https://github.com/hanzkering/examples/tree/master/javascript/mvvm

雁过留声