美文网首页
双向绑定

双向绑定

作者: pixels | 来源:发表于2017-11-30 16:22 被阅读18次

    Vue的双向绑定是通过访问器属性来实现(Object.defineProperty中的get、set,具体使用方法请google),具体通过代码示例解释

    html元素

     <div id="app">
      <input type="text" v-model="text"></input>
      <span>{{text}}</span>
      <div>
        <p> {{food1}} </p>
        <p> {{food2}} </p>
        <p> {{favourite}} </p>
        <div>
          <p> <span> {{note}} </span> </p>
          <p> <span> {{text}} </span> </p> 
        </div>
      </div>
    </div>
    

    初始化

    初始化Vue时将vue中的data绑定到view层面

    1. 创建一个Vue对象
    class Vue {
      constructor (option) {
        Object.assign(this, option)
        var dom = nodeToFragment(document.getElementById('app'), this)
      }
    }
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'Hello World!',
        food1: 'fish',
        food2: 'meat',
        favourite: 'you guess!',
        note: '注意不能与禁忌食物同食'
      }
    })
    

    示例中定义了一个Vue对象,根节点为id为app的元素

    1. 通过Object.assign把option相应的配置(data等)都分配给vm
    2. nodeToFragment方法实现从根节点开始遍历html元素节点,将data元素绑定至view层
      nodeToFragment方法的实现如下
    function nodeToFragment(node, vm) {
      var child
      var vmDataName
      var fragment = document.createDocumentFragment()
      while (child = node.firstChild) {
        // 节点为元素
        if (child.nodeType === 1) {
          vmDataName = child.getAttribute('v-model')
          if (vmDataName) {
            child.value = vm.data[vmDataName]
          }
          nodeToFragment(child, vm)
        } else if (child.nodeType === 3){ // 文本节点
          if(/\{\{(.*)\}\}/.test(child.nodeValue)) {
            vmDataName = RegExp.$1
            child.nodeValue = vm.data[vmDataName]
          }
        }
        fragment.append(child)
      }
      node.append(fragment)
    }
    

    通过while (child = node.firstChild)循环遍历node的所有子节点
    a. 如果是元素节点,如果获取到v-model属性,更新元素的value值,然后继续遍历child的子节点
    b. 如果是文本节点,匹配‘{{}}’中的内容,替换成对应的data值
    c. 将更新后的节点append至fragment中
    d. 将最后生成的节点全部append至node元素下

    View至Model层的更新

    对上面代码进行修改,使得对input元素的修改能够反映到Vue中

    function nodeToFragment(node, vm) {
      while (child = node.firstChild) {
        // 节点为元素
        if (child.nodeType === 1) {
          vmDataName = child.getAttribute('v-model')
          if (vmDataName) {
            child.value = vm.data[vmDataName]
            bindViewToModel(child, vm.data, vmDataName) // View层的变化反应到model层
          }
          nodeToFragment(child, vm)
        }
        ……
      }
      node.append(fragment)
    }
    
    function bindViewToModel(node, obj, key) {
      node.addEventListener('input', e => {
        obj[key] = e.target.value
      })
    }
    

    添加了一个bindViewToModel方法,通过监听input事件,更新model

    Model至Vue的更新

    class Dep {
      constructor() {
        this.subs = []
      }
      addSubs(sub) {
        this.subs.push(sub)
      }
      notify() {
        this.subs.forEach(sub => {
          sub.bindModelToView()
        });
      }
    }
    function definePropertyOfData(obj) {
      for(var key in obj) {
        (function(dep) {
          var value = obj[key]
          Object.defineProperty(obj, key, {
            get: () => {
              if (Dep.target) {
                dep.addSubs(Dep.target) //订阅
              }
              return value
            },
            set: val => {
              value = val
              dep.notify() 
            }
          })
        })(new Dep())
      }
    }
    
    

    对于obj的每一个属性,每次有对象访问这个属性时,如果这个对象订阅了这个属性(Dep.target为true),就将对象添加至订阅列表(dep.addSubs)。在属性值发生改变时,对所有订阅列表中的对象发生一个通知(notify)。对象接受到通知以后,就将更新后的属性值反应到View中

    现在对html中每一个访问了data元素值的元素,都订阅它绑定的属性值

    class Watcher {
      constructor (node, obj, key) {
        this.node = node
        this.obj = obj
        this.key = key
        Dep.target = this
        node.nodeValue = obj[key]
        Dep.target = null
      }
      bindModelToView () {
        this.node.nodeValue = this.obj[this.key] 
      }
    }
    function nodeToFragment(node, vm) {
      ……
      while (child = node.firstChild) {
       ……
       else if (child.nodeType === 3){ // 文本节点
          if(/\{\{(.*)\}\}/.test(child.nodeValue)) {
            vmDataName = RegExp.$1
            new Watcher(child, vm.data, vmDataName)
          }
        }
        fragment.append(child)
      }
      node.append(fragment)
    }
    

    通过Dep.target = this获取订阅者, node.nodeValue = obj[key]会调用对象的get方法,在get方法中,发现订阅者(Dep.target)存在,就会通过dep.addSubs(Dep.target)将该订阅者添加至订阅列表,之后将Dep.target 设置为空。obj属性值发送改变时(调用set方法),通过dep.notify()发送通知。接收到通知后,Dep.target调用bindModelToView方法将model改变反应至View中

    完整代码

    
    var dom = `
        <div id="app">
            <input type="text" v-model="text"></input>
            <span>{{text}}</span>
        <div>
          <p> {{food1}} </p>
          <p> {{food2}} </p>
          <p> {{favourite}} </p>
          <div>
            <p> <span> {{note}} </span> </p>
            <p> <span> {{text}} </span> </p> 
          </div>
        </div>
        </div>`
    document.body.innerHTML = dom
    
    // View层的变化反应到model层:通过addEventListener监视vue层事件,更新model
    
    class Vue {
      constructor (option) {
        Object.assign(this, option)
        definePropertyOfData(this.data)
        var dom = nodeToFragment(document.getElementById('app'), this)
      }
    }
    function definePropertyOfData(obj) {
      for(var key in obj) {
        (function(dep) {
          var value = obj[key]
          Object.defineProperty(obj, key, {
            get: () => {
              if (Dep.target) {
                dep.addSubs(Dep.target) //订阅
              }
              return value
            },
            set: val => {
              value = val
              dep.notify() 
            }
          })
        })(new Dep())
      }
    }
    class Watcher {
      constructor (node, obj, key) {
        this.node = node
        this.obj = obj
        this.key = key
        Dep.target = this
        node.nodeValue = obj[key]
        Dep.target = null
      }
      bindModelToView () {
        this.node.nodeValue = this.obj[this.key] 
      }
    }
    class Dep {
      constructor() {
        this.subs = []
      }
      addSubs(sub) {
        this.subs.push(sub)
      }
      notify() {
        this.subs.forEach(sub => {
          sub.bindModelToView()
        });
      }
    }
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'Hello World!',
        food1: 'fish',
        food2: 'meat',
        favourite: 'you guess!',
        note: '注意不能与禁忌食物同食'
      }
    })
    function nodeToFragment(node, vm) {
      var child
      var vmDataName
      var fragment = document.createDocumentFragment()
      while (child = node.firstChild) {
        // 节点为元素
        if (child.nodeType === 1) {
          vmDataName = child.getAttribute('v-model')
          if (vmDataName) {
            child.value = vm.data[vmDataName]
            bindViewToModel(child, vm.data, vmDataName) // View层的变化反应到model层
          }
          nodeToFragment(child, vm)
        } else if (child.nodeType === 3){ // 文本节点
          if(/\{\{(.*)\}\}/.test(child.nodeValue)) {
            vmDataName = RegExp.$1
            new Watcher(child, vm.data, vmDataName)
          }
        }
        fragment.append(child)
      }
      node.append(fragment)
    }
    function bindViewToModel(node, obj, key) {
      node.addEventListener('input', e => {
        obj[key] = e.target.value
      })
    }
    window.setTimeout(() => {
      vm.data.favourite = 'rice'
      console.log(vm.data)
    }, 5000)
    

    在谷歌中,将代码黏贴纸console中,即可尝试看到效果。

    相关文章

      网友评论

          本文标题:双向绑定

          本文链接:https://www.haomeiwen.com/subject/mtvvbxtx.html