Post

Vue Implementation Principles - Implementing Bidirectional Binding mvvm Study Notes

Vue Implementation Principles - Implementing Bidirectional Binding mvvm Study Notes

Several ways to implement data binding

  • Publisher-subscriber pattern
  • Dirty value checking: as angular.js, when a specified event is triggered, it will decide whether or not to update the view by comparing whether or not there is a change in the data by means of dirty value checking. These events are: DOM event (input text, click button), XHR response event, browser location change event, Timer event ($timeout, $interval), execute $digest() or apply().
  • Data hijacking: such as vue.js, using data hijacking combined with the publisher-subscriber model, through the Object.defineProperty() to hijack the setter, getter of the individual properties, in the event of a change in the data publish a message to the subscriber, triggered by the corresponding listener callbacks.

Implementing two-way binding for vue

This is achieved primarily through the defineProperty property of Object, overriding the set and get functions of data. two-way binding Here’s an example implementation of the v-model, v-bind and v-click commands.

Final realization

1
2
3
4
5
<div id="app">
  <input type="text" v-model="number" />
  <button type="button" v-click="increment">增加</button>
  <h3 v-bind="number"></h3>
</div>
1
2
3
4
5
6
7
8
9
10
11
var app = new myVue({
  el: '#app',
  data: {
    number: 0
  },
  methods: {
    increment: function() {
      this.number++
    }
  }
})

mvvm example

The myVue constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
function myVue(options) {
  this._init(options)
}
myVue.prototype._init = function (options) {
  this.$options = options
  this.$el = document.querySelector(options.el) // 根节点dom
  this.$data = options.data
  this.$methods = options.methods

  this._binding = {} // 绑定的订阅者
  this._observe(this.$data) // 监听属性
  this._compile(this.$el)
}

Observer listener

Listens for changes in each piece of data and notifies the subscriber when it listens for a change.

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
myVue.prototype._observe = function(obj) {
  var value
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      value = obj[key]
      if (typeof value === 'object') {
        this._observe(value)
      }
      this._binding[key] = {
        _watchers: []
      }
      var binding = this._binding[key]
      Object.defineProperty(this.$data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          console.log('获取属性' + key + ': ' + value)
          return value
        },
        set: function(newVal) {
          console.log('设置属性' + key + ': ' + newVal)
          value = newVal
          // 通知该属性的所有订阅者更新数据
          binding._watchers.forEach(function(item) {
            item.update()
          })
        }
      })
    }
  }
}

Subscribers

Used to bind update functions to update DOM elements.

1
2
3
4
5
6
7
8
9
10
11
12
function Watcher(el, vm, exp, attr) {
  this.el = el  // 指令对应的DOM元素
  this.vm = vm // 指令所属的myVue实例
  this.exp = exp // myVue实例更新的属性
  this.attr = attr // DOM元素绑定的属性值

  this.update()
}
// 更新视图
Watcher.prototype.update = function() {
  this.el[this.attr] = this.vm.$data[this.exp]
}

Compile parsing

Parses v-bind, v-model, and v-click directives and binds the view to the model.

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
myVue.prototype._compile = function(app) {
  var nodes = app.children
  var _this = this
  for (let i = 0, len = nodes.length; i < len; i++) {
    var node = nodes[i]
    if (node.children.length) {
      this._compile(node)
    }
    // v-click
    if (node.hasAttribute('v-click')) {
      var methodName = node.getAttribute('v-click')
      node.onclick = _this.$methods[methodName].bind(_this.$data)
    }
    // v-model
    if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
      var dataName = node.getAttribute('v-model')
      // 添加订阅者
      _this._binding[dataName]._watchers.push(new Watcher(node, _this, dataName, 'value'))
      node.addEventListener('input', function() {
        _this.$data[dataName] = this.value // 使data与dom的值保持一致
      })
    }
    // v-bind
    if (node.hasAttribute('v-bind')) {
      var dataName = node.getAttribute('v-bind')
      // 添加订阅者
      _this._binding[dataName]._watchers.push(new Watcher(node, _this, dataName, 'innerHTML'))
    }
  }
}

Source code

mvvm-demo

Reference

This post is licensed under CC BY 4.0 by the author.