vue2.x双向绑定

264 阅读3分钟

vue2.x双向绑定原理

vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。

一、Object.defineProperty

用于在对象上定义一个新属性,或者修改对象现有属性,并返回此对象。

语法:

Object.defineProperty(obj, prop, descriptor)

参数:

  • obj:必需。目标对象
  • prop:必需。需定义或修改的属性的名字
  • descriptor:必需。目标属性所拥有的特性

返回值:

传入函数的对象,即第一个参数obj

属性:

  • get:获取值时调用的方法
  • set:设置值时调用的方法

举例:

let number = 18
let person = {
  name:'张三',
  sex:'男'
}

Object.defineProperty(person,'age',{
  value:18,
  enumerable:true, //控制属性是否可以枚举,默认值是false
  writable:true, //控制属性是否可以被修改,默认值是false
  configurable:true //控制属性是否可以被删除,默认值是false

  //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
  get(){
    console.log('将要读取person.age属性')
    return number
  },

  //当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
  set(value){
    console.log('修改person.age属性,新值为',value)
    number = value
  }

})
console.log(Object.keys(person))
console.log(person)

二、JS的双向绑定

每当值改变的时候都会调用set方法,可以重写set方法,来实现js的双向绑定

<body>
  <div id="app">
    <input id="a" type="text">
    <p id="b"></p>
  </div>
  <script>
    const domA = document.getElementById('a')
    const domB = document.getElementById('b')
    let obj = {}
    let val = 'new'
    Object.defineProperty(obj, 'name', {
      get: function() {
        console.log('get val:' + val)
        return val
      },
      set: function(newVal) {
        val = newVal
        console.log('set val:' + val)
        domA.value = val
        domB.innerHTML = val
      }
    })
    domA.addEventListener('keyup', function(e) {
      obj.name = e.target.value
    })
  </script>
</body>

修改输入框内容,或手动更改obj.name的值时,p标签的内容也会随即显示相同的内容。

三、DocuemntFragment

文档片段,一个没有父对象的最小文档对象。

当我们需要批量的向目标 DOM 中插入大量的节点或内容时,使用传统方式会多次触发回流和重绘(每次插入数据后,页面会立即反映出这个变化),影响页面性能,此时就可以考虑使用 DocumentFragment ,把所有的新节点附加其上,然后把文档碎片的内容一次性添加到 document 中。相比传统操作,这个操作仅发生一个重渲染。

vue进行编译时,就是将挂载目标的所有子节点劫持到DocumentFragment中,经过一番处理之后,再将DocumentFragment整体返回插入挂载目标。

实现变量绑定到input和文本节点上

  1. 处理每一个节点的编译方法,如果有input绑定v-model属性或者有{{ xxx }}的文本节点出现,就进行内容替换,替换为vm实例中的data中的内容
  2. 在向DocumentFragmen中添加节点时,每个节点都要按照上述方法处理一下
  3. 创建Vue的构造函数

四、view => model

通过改写set方法,使页面上的输入框改变值,vm中的data需要获取最新的value

inputinput、keyup、change事件中获取输入框的新值,利用Object.defineProperty将新值赋值给vm.text,把vm实例中的data下的text通过Object.defineProperty设置为访问器属性,这样给vm.text赋值,就触发了set。

并且实现一个观察者,对于一个实例每一个属性值都进行观察。

五、model=> view

通过修改vm实例的属性,该改变输入框的内容与文本节点的内容。

页面中可能多处用到 data中的属性,这是1对多的。也就是说,改变1个model的值可以改变多个view中的值。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作

完整代码

<body>
  <div id="app">
    <input v-model="text" type="text">
    {{text}}
  </div>
  <script>
    // 编译方法
    function compile(node, vm) {
      let reg = /\{\{(.*)\}\}/ // 匹配{{ xxx }}中的xxx
      // 如果是元素节点
      if (node.nodeType === 1) {
        // attributes包含了元素的所有属性
        let attr = node.attributes
        // 解析元素节点的所有属性
        for (let i = 0;i < attr.length;i++) {
          if (attr[i].nodeName == 'v-model') {
            let name = attr[i].nodeValue // 看看是与哪一个数据绑定
            node.addEventListener('input', function(e) {
              vm[name] = e.target.value // 将实例的text修改为最新值
              // 实时更新的是vm的访问器属性text,vm.data[text]并不会更新
              console.log(vm)
            })
            // node.value = vm[name] // 将data的值赋给该节点
            new Watcher(vm, node, name) // 不直接赋值,而是通过绑定一个订阅者。vm变,输入框数据跟着变
            node.removeAttribute('v-model')
          }
        }
      }
      // 如果是文本节点
      if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
          // 获取到匹配的字符串xxx
          let name = RegExp.$1
          name = name.trim()
          // node.nodeValue = vm[name] // 将vm[text]的值赋给该节点
          new Watcher(vm, node, name) // 不直接赋值,而是通过绑定一个订阅者
          // console.log(new Watcher(vm, node, name))
        }
      }
    }
    // 把app的所有子节点都劫持过来,在向碎片化文档中添加节点时,每个节点都处理一下
    function nodeToFragment(node, vm) {
      let fragment = document.createDocumentFragment()
      let child
      // 每次循环都是先把node.firstChild赋值给child,然后当child非空时,才会进入循环,并不是根据两者是否相等
      while (child = node.firstChild) {
        compile(child, vm)
        fragment.appendChild(child)
      }
      return fragment
    }
    // 响应式监听属性的方法
    function defindeReactive(obj, key, val) {
      let dep = new Dep()
      Object.defineProperty(obj, key, {
        get: function() {
          if (Dep.target) {
            dep.addSub(Dep.target)
          }
          return val
        },
        set: function(newVal) {
          if (newVal === val) return
          val = newVal
          console.log(`新值:${val}`)
          // 一旦更新马上发布消息
          dep.notify()
        }
      })
    }
    // 观察者方法
    function observe(obj, vm) {
      for (let key of Object.keys(obj)) {
        defindeReactive(vm, key, obj[key])
      }
    }
    // Watcher构造函数
    function Watcher(vm, node, name) {
      Dep.target = this // Dep.target是一个全局变量
      this.vm = vm
      this.node = node
      this.name = name
      this.update()
      Dep.target = null
    }
    Watcher.prototype = {
      update() {
        this.get()
        this.node.nodeValue = this.value // 注意,这是更新节点内容的关键
        this.node.value = this.value // 更新输入框数据的关键。vm变,输入框数据跟着变
      },
      get() {
        this.value = this.vm[this.name] // 触发相应的get
      }
    }
    // Dep构造函数
    function Dep() {
      this.subs = []
    }
    Dep.prototype = {
      addSub(sub) {
        this.subs.push(sub)
      },
      notify() {
        // 执行所有订阅者的回调函数update
        this.subs.map(item => {
          item.update()
        })
      }
    }
    // Vue构造函数
    function Vue(options) {
      this.data = options.data
      let data = this.data
      // console.log(this) // this指向vm,vm = {data: {text: 'lvxiaobu'}},仅有data一个属性,vm != options
      // vm还有额外的隐式属性值为options.data里面的所有属性值text: 'lvxiaobu',方便get和set方法进行双向绑定
      observe(data, this)
      let id = options.el
      // 这个this也就是实例化对象本身
      let dom = nodeToFragment(document.getElementById(id), this)
      // 处理完所有DOM节点后,重新将内容添加回去
      document.getElementById(id).appendChild(dom)
    }
    // 实例化Vue
    let vm = new Vue({
      el: 'app',
      data: {
        text: 'lvxiaobu'
      }
    })
  </script>
</body>