重温Vue2双向绑定的原理

81 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第一天,点击查看活动详情

背景

为什么会写这个文章呢?当然是因为现在处在一个特殊的环境中

双向绑定原理定义

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

数据劫持 Object.defineProperty()

我觉得vue2出了这么久大家对这个应该不陌生,在这我就简单的举个例子

Object.defineProperty()会接收三个参数,第一个参数是对象名,第二个参数是对象的属性,第三个是属性的相关操作包括属性值。 意思就是给属性增加一个监听,当去更改属性或者获取属性的时候我们都可以知道

const person = {
      name: "小明",
    };
    
function updateObj(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log(val, "get获取name值");
          return val;
        },
        set(newVal) {
          console.log(newVal, "set设置name值");
          val = newVal;
          return val;
        },
      });
    }
updateObj(person, "name", (val = "小明"));

image.png

核心内容搞明白了哪我们开始下一步,这里我先贴一段代码 这块代码可以解释为当我们创建了一个Vue实例时,其实就是执行Vue构造函数(这块不理解的可以看我其他文章)构造函数里面增加一个data属性,属性值是我们传进来的data,接着去调用observe方法,接收两个参数一个是data 一个是当前实例的this,接下来我们看observe方法

    <div id="app">
      双向绑定原理
      <input type="name" v-model="name" /> {{name}}
     </div>
 
   <script type="text/javascript">
    function Vue(options) {
      this.data = options.data;
      observe(this.data, this);
      // 模版解析对应的操作
      var id = options.el;
      var dom = nodeToFragment(document.getElementById(id), this);
      document.getElementById(id).appendChild(dom);
    }
    var vm = new Vue({
      el: "app",
      data: {
        name: "小明",
      },
    });
    </script>

observe

这个方法暂时只看我们想看的,就是循环我们传进来的data拿到对应的key,去调用defineReactive(vm, key, obj[key]); 第一个参数this指向实例对象、key、key的value, 这里面其实主要就使用了Object.defineProperty给属性添加get和set其他操作先不看

 //实现一个观察者,对于一个实例 每一个属性值都进行观察。
    function observe(obj, vm) {
      for (let key of Object.keys(obj)) {
        defineReactive(vm, key, obj[key]);
      }
    }
    //实现一个响应式监听属性的函数。一旦有赋新值就发生变化
    function defineReactive(obj, key, val) {
      var dep = new Dep(); //观察者实例
      Object.defineProperty(obj, key, {
        get: function () {
          if (Dep.target) {
            //每一个观察着都是唯一的
            dep.addSub(Dep.target);
            console.log(dep, "dep");
          }
          return val;
        },
        set: function (newVal) {
          if (newVal === val) {
            return;
          }
          val = newVal;
          console.log("新值" + val);
          //一旦更新立马通知
          dep.notify();
        },
      });
    }

   

模版解析compile

nodeToFragment函数主要获取我们的节点信息每个节点调用compile方法,然后判断节点类型,如果是元素节点此处input,先获取input上绑定的属性 v-model type=‘text’ 之类的得到一个数组对象,然后就判断那个是v-model,获取到绑定的属性name然后添加input事件

 if (node.nodeType === 1) {
        var attr = node.attributes;
        //解析元素节点的所有属性
        for (let i = 0; i < attr.length; i++) {
          if (attr[i].nodeName == "v-model") {
          //获取v-model的值 此处是name
            var name = attr[i].nodeValue; //看看是与哪一个数据相关
            node.addEventListener("input", function (e) {
              //将与其相关的数据改为最新值
              vm[name] = e.target.value;
            });
            node.value = vm.data[name]; //将data中的值赋予给该node
            node.removeAttribute("v-model");
          }
        }
      }

如果是文本节点 就判断是不是模版{{}} 如果是获取到里面绑定属性,然后node.nodeValue = vm[name]; 然后获取实例上对应属性的value赋值给node,

      //如果是文本节点
      if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
          var name = RegExp.$1; //获取到匹配的字符串
          name = name.trim();
          node.nodeValue = vm[name]; //将data中的值赋予给该node
          new Watcher(vm, node, name); //绑定一个订阅者
        }
      }

注意此时已经触发了该属性绑定的get方法,由于里面加了if (Dep.target) 判断所以只返回了value,没做其他操作

var dep = new Dep(); //观察者实例
      Object.defineProperty(obj, key, {
        get: function () {
          if (Dep.target) {
            //每一个观察着都是唯一的
            dep.addSub(Dep.target);
            console.log(dep, "dep");
          }
          return val;
        },
        set: function (newVal) {
          if (newVal === val) {
            return;
          }
          val = newVal;
          console.log("新值" + val);
          //一旦更新立马通知
          dep.notify();
        },
      });

接着到到 new Watcher(vm, node, name); 此处我认为是在进行依赖收集,在Dep构造函数上增加Dep.target = thisthis指向当前watcher实例,然后就是一些初始化属性的操作,

接着调用自身update方法,然后执行get方法获取当前属性的value, this.value = this.vm[this.name];注意此时又触发了此属性的get 此时if (Dep.target)成立进入内部 把Dep.target放进sub数组中,接着将获取到的值赋值,然后Dep.target=null,防止重复添加

    function Watcher(vm, node, name) {
      Dep.target = this;
      this.vm = vm;
      this.node = node;
      this.name = name;
      this.update();
      Dep.target = null;
    }

    Watcher.prototype = {
      update() {
        this.get();
        this.node.nodeValue = this.value; //更改节点内容的关键
      },
      get() {
        this.value = this.vm[this.name]; //触发相应的get
      },
    };

此时其实整个的双向绑定原理就结束了,当我们改变name的时候触发set,如果值相等则直接return不相等在则去调用 dep.notify();,循环当前节点所有的依赖(Watcher实例) 执行实例自身update方法

        set: function (newVal) {
          if (newVal === val) {
            return;
          }
          val = newVal;
          console.log("新值" + val);
          //一旦更新立马通知
          dep.notify();
        },

End

这样写个文章理解下来面试的时候被问到就不慌了,之前面试官问我原理我只会说出文章开头双向绑定原理的定义,这次我可以多说点了也不知道有没有用。