原理

Vue 的响应依赖于Object.defineProperty,这也是Vue不支持IE8的原因。 Vue通过设定对象属性的setter/getter方法来监听数据的变化。 通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

实现过程

一段简单的demo

<template>
  <input type="text" id="a"/>
  <span id="b"></span>
</template>

<script>
  const obj = {};
  Object.defineProperty(obj,'hello', {
    get() {
      console.log("啦啦啦,方法被调用了");
    },
    set(newVal) {
      document.getElementById('a').value = newVal
      document.getElementById('b').innerHTML = newVal
    }
  })
  document.addEventListener('keyup', function(e) {
    obj.hello = e.target.value
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

通过Object.defineProperty这个方法,input中的keyup事件触发的时候obj中的hello 值被赋值input的value,当对obj.hello赋值触发了set方法,在set方法中改变了inpt的值。

vue中的双向绑定

  1. 实现一个数据监听器Observer(),能够对数据对象的所有属性进行监听,如有变动可以拿到最新值并通知订阅者
  2. 实现一个指令解析器Compile(),对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  3. .实现一个Watcher(),作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

定义一个Vue对象

function Vue(options) {
  this._init(options)
}

Vue.prototype._init = function (options) {
  this.$options = options
  this.$el = document.querySelector(options.el)
  this.$data = options.data
  this.$methods = options.methods
  // 定义一个绑定对象,用于存放对象
  this._binding = {}
  // 观察者
  this._obverse(this.$data)
  // 解析器
  this._compile(this.$el)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

观察者(observable)

function _obverse () {
  let value
  // 遍历对象,收集绑定的数据
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      this._binding[key] = {
        _directives = []
      }
      value = obj[key]
      // 做个迭代
      if (typeof value === 'object') this._obverse(value)

      let binding = this._binding[key]
      Object.defineProperty(
        this.$data,
        key,
        {
          enumerable: true,
          configurable: true,
          get: function () {
            // 更新
            if (value !== newVal) {
              value = newVal
              binding._directives.forEach(function (item) {
                item.update()
              })
            }
          }
        }
      )
    }
  }
}
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

TIP

  • 利用Obeject.defineProperty()来监听属性变动
  • 需要将observer的数据进行递归遍历,包括子属性对象的属性,都加上setter和getter
  • 给某个对象赋值,就会触发setter,那么就能监听到数据变化,通过notify()发布出去

解析器(compile)

Vue.prototype._compile = function (root) {
  let _this = this
  let nodes = root.children
  for (let i = 0; i < nodes.length; i++) {
    let node = nodes[i]
    if (node.children.length) {
      this._compile(node)
    }
    if (node.hasOwnProperty('v-click')) {
      node.click = (function () {
        let attrVal = node[i].getAttribute('v-click')
        return this.$methods[attrVal].bind(_this.$data)
      })();
    }
    if (
      node.hasOwnProperty('v-model') &&
      (
        node.tagName = 'INPUT' ||
        node.tagName = 'TEXTAREA'
      )
    ) {
      node.addEventListener(
        'input',
        (function (key) {
          let attrVal = node.getAttribute('v-model')
          _this._binding[attrVal]._directives.push(
            new Watch(
              'input',
              node,
              _this,
              attrVal,
              'value'
            )
          )
          return function () {
            _this.$data[attrVal] = node[key].value
          }
        })(i)
      )
    }

    if (node.hasOwnProperty('v-bind')) {
      let attrVal = node.getAttribute('v-bind')
      _this._binding[attrVal]._directives.push(
        new Watch(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        )
      )
    }
  }
}
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

TIP

  • compile主要是解析模板指令,将模板的变量替换成数据,然后初始化渲染页面视图
  • 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变化,收到通知,更新视图

Watcher

Watcher订阅者为Observer和Compile之间通信的桥梁

function Watcher (
  name,
  el,
  vm,
  attr,
  exp
) {
  this.name = name
  this.el = el
  this.vm = vm
  this.attr = attr
  this.exp = exp
  this.update()
}

Watch.prototype.update = function () {
  this.$el[this.attr] = this.vm.$data[this.exp]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

TIP

  • 在Compile中实例化时
  • 往属性订阅器(dep)里添加在自己
  • 自身必须有一个update()方法
  • 当数据变动是接到dep属性订阅器的notify发布通知时,能够调用自身的update()方法, 从而触发get方法去更新数据