vue双向数据绑定原理

几种实现双向绑定的做法

目前几种主流的mvc框架都实现了单向数据绑定,而双向绑定无非就是在单项绑定的基础上给可输入元素添加了change事件,来动态修改model和view。

实现数据绑定的做法有大致如下几种

     发布者-订阅者模式

     脏值检查

     数据劫持

 

发布者-订阅者模式

    一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法时vm.set('property', value).

脏值检查

    angular.js 时通过脏值检测的的方式比较数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval() 定时轮询检测数据变动,angular的做法时,只有在指定的事件触发进入脏值检测

    DOM事件

    XHR响应事件

    浏览器Location变更事件

    Timer事件

    执行digest() 或 apply()

数据劫持

    vue.js 则时采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter, 在数据变动时发布消息给订阅者,触发相应的监听回调。

    思路整理

    1、实现一个数据监听器Observer, 能够对数据对象的所有属性进行监听,如果变动可拿到最新值并通知订阅者

    2、实现一个指令解析器Compile, 对每个元素节点的指令进行扫描和解析, 根据指令模板替换数据,以及绑定相应的更新函数

    3、实现一个Watcher,作为连接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

    4、入口函数,整合以上三者

 

流程图

 

 

数据监听器

function observe(obj, vm) {
    // 对传入的对象  遍历  并分别添加 object.defineProperty
    Object.keys(obj).forEach((key) => {
           defineReactive(vm, key, obj[key])
    })
}

function defineReactive(vm, key, val){
    var dep = new Dep();
    Object.defineProperty(vm, key, val){
          get:  function () {
               if(Dep.target)  dep.addSub(Dep.target)
               return val;
          },

          set: function(newval) {
              if(newval === val) return
              val = newval;
              // 通知订阅者
              dep.notify();
          }
    }
}

// 需要实现一个消息订阅器
function Dep() {
    // 消息订阅的容器是一个数组  数组的每一项都是指代一个view 和 model的中间者
    this.subs = [];
}

Dep.prototype = {
      addSub: function (sub){
         this.subs.push(sub)
      },

      notify: function () {
          this.subs.forEach((sub) => {
                 //在这里,需要配合watcher进行更新
                 sub.update()
          }) 
      }
}

 

实现Compile

function nodeToFragment(node, vm) {
      var flag = document.createDocumentFragment();
      var child;
      while(child = node.firstChild) {
           compile(child, vm);
           // 将子节点劫持到文本节点中
           flag.appendChild(child)
      }

      return flag;
}

function compile(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") {
                     // 此时 name为text
                     var name = attr[i].nodeValue;
                     // 增加数据的变化监听
                     node.addEventListener('input', (e)=>{
                          vm[name] = e.target.value;
                     })
                     // 在这里 因为 我们的数据监听器 已经封装了vm[name]
                     node.value = vm[name];
                     node.removeAttribute('v-model')
               }
          }
          new Watcher(vm, node, name, 'input')
     }

      if(node.nodeType === 3){
            if(reg.test(node.nodeValue)){
                   var name + RegExp.$1;
                   name = name.trim();
                   new Watcher(vm, node, name, 'text')
            }
      }
}

 

watcher观察函数

//订阅者 搭建数据监听变化和变异模板的桥梁
    function Watcher(vm, node, name, nodeType) {
        Dep.target = this;
        this.vm = vm;
        this.node = node;
        this.name = name;
        this.nodeType = nodeType
        this.update()
        Dep.target = null;
    }

    Watcher.prototype = {
        update: function () {
            this.get()
            if (this.nodeType === 'text') {
                this.node.nodeValue = this.value
            }
            if (this.nodeType === 'input') {
                this.node.value = this.value
            }
        },
        get: function () {
            this.value = this.vm[this.name];
        }
    }

 

入口函数

function Vue(options) {
        // 将options里面的data属性 放入数据监听器
        this.data = options.data;
        var data = this.data;
        observe(data, this); // this指代vm
        // 对指定id的dom 进行页面的渲染
        this.$el = options.el;
        var id = this.$el;
        var Dom = nodeToFragment(document.getElementById(id), this);
        // 编译完成之后 将dom 添加到节点中
        document.getElementById(id).appendChild(Dom)
    }

    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world',
            name: '你好,全世界'
        }
    });
    vm.data.text = 'majunchang'


    document.getElementsByClassName('btn')[0].onclick = function () {
        vm.text = 'majunchang'
        vm.name = '又疑瑶台镜,飞在青云端'
    }

 

posted on 2022-02-22 10:07  zhishiyv  阅读(48)  评论(0编辑  收藏  举报

导航