Vue双向数据绑定原理


原理

Vue 采用数据劫持结合发布者-订阅者模式的方法,通过 Object.defineProperty()来劫持各个属性的 setter,getter 属性,在数据变动话,通知订阅者,触发更新回调函数,重新渲染视图

关键要素

  • observer 实现对 vue 各个属性进行监听
function observer(obj, vm) {
  Object.keys(obj).forEach(function (key) {
    defineReactive(vm, key, obj[key])
  })
}
// Object.defineProperty改写各个属性
function defineReactive(obj, key, val) {
  // 每个属性建立个依赖收集对象,get中收集依赖,set中触发依赖,调用更新函数
  var dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖  Dep.target标志
      Dep.target && dep.addSub(Dep.target)
      return val
    },
    set: function (newVal) {
      if (newVal === val) return
      // 触发依赖
      dep.notify()
      val = newVal
    },
  })
}
  • Dep 实现
function Dep() {
  this.subs = []
}
Dep.prototype = {
  constructor: Dep,
  addSub: function (sub) {
    this.subs.push(sub)
  },
  notify: function () {
    this.subs.forEach(function (sub) {
      sub.update() // 调用的Watcher的update方法
    })
  },
}
  • compiler 实现对 vue 各个指令模板的解析器,生成 AST 抽象语法树,编译成 Virtual Dom,渲染视图
// 编译器
function compiler(node, vm) {
  var reg = /\{\{(.*)\}\}/
  // 节点类型为元素
  if (node.nodeType === 1) {
    var attr = node.attributes
    // 解析属性
    for (var i = 0; i < attr.length; i++) {
      if (attr[i].nodeName == 'v-model') {
        var _value = attr[i].nodeValue
        node.addEventListener('input', function (e) {
          //给相应的data属性赋值,触发修改属性的setter
          vm[_value] = e.target.value
        })
        node.value = vm[_value] // 将data的值赋值给node
        node.removeAttribute('v-model')
      }
    }
    new Watcher(vm, node, _value, 'input')
  }
  // 节点类型为text
  if (node.nodeType === 3) {
    if (reg.test(node.nodeValue)) {
      var name = RegExp.$1
      name = name.trim()
      new Watcher(vm, node, name, 'input')
    }
  }
}
  • Watcher 连接 observer 和 compiler,接受每个属性变动的通知,绑定更新函数,更新视图
function Watcher(vm, node, name, nodeType) {
  Dep.target = this // this为watcher实例
  this.name = name
  this.node = node
  this.vm = vm
  this.nodeType = nodeType
  this.update() // 绑定更新函数
  Dep.target = null //绑定完后注销 标志
}
Watcher.prototype = {
  get: function () {
    this.value = this.vm[this.name] //触发observer中的getter监听
  },
  update: function () {
    this.get()
    if (this.nodeType == 'text') {
      this.node.nodeValue = this.value
    }
    if (this.nodeType == 'input') {
      this.node.value = this.value
    }
  },
}

完整实现

function Vue(options) {
  this.date = options.data
  var data = this.data
  observer(data, this) // 监测
  var id = options.el
  var dom = nodeToFragment(document.getElmentById(id), this) //生成Virtual Dom
  // 编译完成后,生成视图
  document.getElementById(id).appendChild(dom)
}
function nodeToFragment(node, vm) {
  var flag = document.createDocumentFragment()
  var child
  while ((child = node.firstChild)) {
    compiler(cild, vm)
    flag.appendChild(child)
  }
  return flag
}

// 调用
var vm = new Vue({
  el: 'app',
  data: {
    msg: 'hello word',
  },
})

Author: Eric
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Eric !
  TOC