Vue源码解析系列(四) -- 来实现一个双向绑定吧(吊打面试官)

904 阅读3分钟

通过学习了Vue源码解析系列(三) -- 响应式系统的依赖收集与视图更新Vue源码解析系列(二) -- 响应式系统内部是怎么运行的,我们对Vue响应式系统有一定的了解,并且知道它是如何实现数据更新视图视图改变数据的,那么有这样的基础,我们来手写一个MVVM,以便面试的时候,吊打面试官(此为笑谈,不足论,嘿嘿)。 那么先抛出一张在座的各位再也熟悉不过的图:

1、当我们new MVVM之后有两步操作,Observer,Compile,我们知道Observer是做数据劫持,Compile是解析指令,那么问题来了:

  • Observer为什么要做数据劫持?
  • Compile为什么要做解析指令? 带着这两个问题,我们回顾一下往期内容:
  • 什么是数据响应式
  • 数据响应式原理是什么?
  • 数据响应式是如何实现的?

数据响应式就是数据双向绑定,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新;如果用户更新了View,那么Model数据也被自动更新了,这种情况就是双向绑定。

数据响应式原理

  • Vue实现数据响应式原理就是通过Object.defineProperty()这个方法重新定义了对象获取属性值get设置属性值set的操作来实现的
  • Vue3.0中是通过ECMAScript6中的proxy对象代理来实现的。 那么本章节就是来实现数据响应式的。

那么回答前面的两个问题,为什么要劫持数据?为什么要解析指令

  • 只有劫持到数据,才能对数据做到监听,以便于数据更改能够及时做到更新视图。
  • Vue中自定义了N多指令,只有解析它,我们JavaScript才能认识它,并运行它。 诸如此类问题我们不再复述,下面开始实现数据响应式。

写一个demo之前,我们应当整理好思路:

1. 首先实现整体的一个架构(包括MVVM类或者VUE类、Watcher类),   /这里用到一个订阅发布者设计模式。
2. 然后实现MVVM中的由M到V,把模型里面的数据绑定到视图。
3. 最后实现V-M, 当文本框输入文本的时候,由文本事件触发更新模型中的数据
4. 同时也更新相对应的视图。
//html代码
<div id="app">
      <h1>MVVM双向绑定</h1>
      <div>
        <div v-text="myText"></div>
        <div v-text="myBox"></div>
        <input type="text" v-model="myText" />
        <input type="text" v-model="myBox" />
      </div>
</div>

我们创建了两个divinput实现input框数据关联,说白了也就是相同的数据源,那我们的数据源在哪呢?

//数据源data
const app = new Vue({
        el: "#app",
        data: {
          myText: "大吉大利!今晚吃鸡!",
          myBox: "我是一个盒子!",
        },
});

可见我们需要一个Vue类,也就是一个发布者,那么直接上代码:

//Vue类(发布者)
class Vue{

}

发布者有了,我们还需要有订阅者:

//Watcher类(订阅者)
class Watcher{

}

可见两者都有了,那么我们该怎么实现呢?

  • 获取data数据
  • 获取元素对象
  • 构造一个存放订阅者的对象
 class Vue {
        constructor(optios) {
          this.$data = optios.data; //获取数据
          this.$el = document.querySelector(optios.el); //获取元素对象
          this._directive = {}; // 存放订阅者
        }
 }       

那么我们说了,我们需要劫持数据解析指令,那么我们得构造两个方法。

   class Vue {
       constructor(optios) {
         this.$data = optios.data; //获取数据
         this.$el = document.querySelector(optios.el); //获取元素对象
         this._directive = {}; // 存放订阅者
         this.Observer(this.$data);
         this.Compile(this.$el);
       }
       //劫持数据
       Observer(data) {
           Object.defineProperty(this.$data, key, {
             get: function(){},
             set: function(){}
             },
           });
       }
       //解析指令   //视图 --- >对象 -- >指令
       Compile(el) {
       
       }
   }

一个是劫持数据,一个是解析元素指令,劫持到的属性要根据属性分配容器,当当前容器不存在该属性的时候,我们便需要把他添加到订阅器对象里面,等待通知更新。

  for (let key in data) {
          this._directive[key] = [];
          let val =data[key];
          let watch = this._directive[key];
  }

那么解析指令,首先必须要递归当前节点,是否还有子节点,是否有v-text指令,v-model指令。

 	  let nodes = el.children;
          for (let i = 0; i < nodes.length; i++) {
            let node = nodes[i];
            //递归 查询所有当前对象子类是否再含有子类
            if (node.children.length) {
              this.Compile(nodes[i]);
            }
            //判断是否含有V-text指令
            if (node.hasAttribute("v-text")) {
              let attrVal = node.getAttribute("v-text");

              this._directive[attrVal].push(
                new Watcher(node, this, "innerHTML", attrVal)
              );
            }

            //判断是否含有V-model指令
            if (node.hasAttribute("v-model")) {
              let attrVal = node.getAttribute("v-model");

              this._directive[attrVal].push(
                new Watcher(node, this, "value", attrVal)
              );
              node.addEventListener("input", () => {
                //赋值到模型
                this.$data[attrVal] = node.value;
                // console.log(this.$data);
              });
            }
          }

那么我们触发更新时候需要收集依赖,我们直接吧收集到的依赖return出去

 Object.defineProperty(this.$data, key, {
              get: function(){
                    return val;
              }
 }

那么我们订阅者长什么样呢?我们订阅者,接收当前元素信息,MVVM对象,标识,属性。并且需要构造一个更新方法update

	class Watcher {
        constructor(el, vm, exp, attr) {
          this.el = el;
          this.vm = vm;
          this.exp = exp;
          this.attr = attr;
          this.update();
        }
        //更新视图
        update() {
          this.el[this.exp] = this.vm.$data[this.attr];
          //div.innerHTML/value = this.Vue.$data["myText/myBox"]
        }
	}

到这里已经快完成了,那么我们收集了依赖就要去,通知watcher去更新视图啊,那么来了:

	Object.defineProperty(this.$data, key, {
              get: function(){
                    return val;
              },
              set: function(newVal){
                  if(newVal !== val){
                    val = newVal;
                    watch.forEach(element => {
                        element.update();  
                    });
                  }
              },
	});

做到这里,你就可以实现一个数据响应式了。

我们已经掌握了响应式原理,那我们开始着手Vue的另一个核心概念组件系统了。那我们下一章讲解一个遗留的问题:Vue源码解析系列(五) -- $mount是如何实现挂载的