基于MVVM模式手写实现Vue响应式~再也不怕面试考了

前言

现在的面试中,只要技术栈中写到了Vue,面试官基本都会问一个点:“请说一下Vue实现数据相应式的原理~”,一些同学知道解决的核心步骤,一些同学知道数据响应式结合的设计模式,可如果要你直接思路清晰的手写一波,在某些方面可能还是会有些问题,所以今天不妨跟着我一起,看看我这版手写vue响应式的思路。

如果对您有所帮助,阔以 点波小赞 鼓励一下咯~ ~,当然,有写得不对的地方,请直接提出来,咱们共同进步!

1.什么是Vue的数据响应式

当一个 Vue 实例被创建时,data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。

要理解Vue的响应式,一定要结合MVVM这种设计模式,Vue在基于MVVM的设计模式下,构建了一套响应式系统,如图所示:
mvvm.png

model层(数据管理层)一般负责处理修改数据,并将发送数据变化发送至viewmodel层;<br>
viewmode层(之后简写为vm层) 一般用于
1. 接收viewmode层传递的数据,并渲染到视图层view中;
2. 监听view层用户操作后,改变的数据,并将model层中的数据修改,同时,触发view层有修改数据的视图更新

view层(视图层)一般负责渲染数据,提供用户交互,当用户操作后通知viewmode层,触发相应事件
复制代码

以上粗略的介绍了mvvm设计模式的三个模块,接下来将结合mvvm设计模式,为大家介绍手写实现Vue数据响应式的思路以及具体方法。

2.通过一个具体的需求实现Vue响应式

具体需求:

带者具体问题(非抽象)去开发,一般会有跟清晰的目击感,那我们先模拟一个初级的Vue的具体需求:

1.支持script标签中 vue的初始化(实例化),在传入根节点元素id和指定的data后,在body中通过v-model和v-text指令绑定的元素要有初始化的值显示出来。

2.见图二,区域二中通过v-model绑定的输入框修改值以后,对应区域一v-text指令绑定的对应值显示也要相应的改变。

响应式1.png

响应式需求2.png

实现思路及代码

针对上面的需求,我拆分成5个步骤来实现:

第1步 支持vue对象的实例化,包括绑定根节点元素;将创建vue实例时传入的data对象初始化成vue实例的上的data属性,并实现针对有v-textv-model的元素进行依赖收集,获取到这些元素绑定的data属性。

1.1 思路分析:

第一步主要是构建vm层,也就是实现vue的实例化;先将model层的数据转化到vm层,即将传入的data作为vue实例的参数$data
之后是对含v-textv-model的元素进行依赖收集并渲染,这一步在设计模式上属于vm层-view层的初次渲染;同时要构建发布者-订阅模式,为之后vm-view的动态发布更新做准备。

1.2 代码实现:

  class Vue {
            constructor(options) {
                    //绑定根节点元素
                    this.el = options.el;
                      if (this.el !== undefined || this.el !== "" || this.el !== null) {
                        this.rootnode = document.querySelector(this.el);
                    }
                    //将数据存入vm层
                    this.$data = options.data;
                    //依赖搜集(解析出v-model和v-text对应元素,并将data中对应绑定的那个元素值关联上)
                    //为了可以让根节点的各级子节点都能进行依赖搜集,采用递归传祖节点方式进行依赖搜集
                    this.complie(this.rootnode)
            }
            
            
            //对指令进行依赖收集
            complie(el) {
                let node = el;
                if (node.children.length > 0) {
                 for (let i = 0; i < node.children.length; i++) {
                        let childNode = node.children[i];
                        if (childNode.children.length > 0) {
                         //传入含有子节点的子节点元素
                            this.complie(childNode)
                        } else {
                            if (childNode.hasAttribute('v-text')) {
                                 console.log(childNode, "通过v-text绑定了:", childNode.getAttribute('v-text'))
                            }
                            if (childNode.hasAttribute('v-model')) {
                                 console.log(childNode, "通过v-model绑定了:", childNode.getAttribute('v-model'))
                            }
                        }
                    }
                }
            }
        }
复制代码

1.3 步骤1执行结果:

实现了 modelvm,并完成了v-指令的依赖搜集

步骤1执行结果.png

第2步 通过观察者-订阅模式将$data中的数据渲染到对应元素上

2.1 思路分析:这一步主要是将 vm层中的数据渲染到view层,其中需要建立观察者-订阅模式,可以把Vue看成发布者,Watcher看成订阅者(负责view层视图渲染)。

2.2 代码实现:

//发布者
        class Vue {
            constructor(options) {
                    //绑定根节点元素
                    this.el = options.el;
                     if (this.el !== undefined || this.el !== "" || this.el !== null) {
                        this.rootnode = document.querySelector(this.el);
                    }
                    //将数据存入vm层
                     this.$data = options.data;

                    //创建一个订阅者组成的容器,只要有元素用到了$data中的属性,
                    //那么就给容器中$data对应属性的那一项数组,添加一个订阅者,
                    //这样,整个页面只要用到了那个属性的,都能建立发布订阅,
                    //之后的步骤中只要vm中data一变,都能触发视图更新
                    this.$directive = {}
                    console.log(this.$data)
                    this.observer(this.$data)
                    console.log(this.$directive)
                    //依赖搜集(解析出v-model和v-text对应元素,并将data中对应绑定的那个元素值关联上)
                    //为了可以让根节点的各级子节点都能进行依赖搜集,采用递归传祖节点方式进行依赖搜集
                    this.complie(this.rootnode)
                    
                }
                //主要用于数据劫持,并发布更新
            observer(data) {
                    //初始化订阅者容器
                      console.log(data)
                    for (let key in data) {
                        this.$directive[key] = [];
                    }
                      }
                //对指令进行依赖收集
            complie(el) {
                let node = el;
                 if (node.children.length > 0) {
                    for (let i = 0; i < node.children.length; i++) {
                        let childNode = node.children[i];
                         if (childNode.children.length > 0) {
                            //传入含有子节点的子节点元素
                            this.complie(childNode)
                        } else {
                          if (childNode.hasAttribute('v-text')) {
                                let _this = this
                                  this.$directive[childNode.getAttribute('v-text')].push(new Watcher({
                                    el: childNode,
                                    vm: _this,
                                    attr: childNode.getAttribute('v-text'),
                                    v_type: 'innerHTML'
                                }))
                            }
                             if (childNode.hasAttribute('v-model')) {
                              // console.log(childNode, "通过v-model绑定了:", childNode.getAttribute('v-model'))
                                    let _this = this;
                                   this.$directive[childNode.getAttribute('v-model')].push(new Watcher({
                                        el: childNode,
                                        vm: _this,
                                         attr: childNode.getAttribute('v-model'),
                                        v_type: 'value'
                                    }))
                              }
                            }
                        }
                    }
                }
        }
    //订阅者(主要用于更新view视图层)
        class Watcher {
            constructor(options) {
                    this.el = options.el;
                    this.vm = options.vm;
                      this.attr = options.attr;
                    this.v_type = options.v_type;
                    this.update();
            }
          //依据传入的元素,元素对应绑定的值,渲染到视图层
          //订阅者不负责判断数据是否改变等,只负责渲染数据到视图层
           update() {
                // this.vm.$data[this.attr]
                let {
                    el,
                    vm,
                    attr,
                    v_type
                } = this;
                 console.log(vm, attr, el, v_type, vm.$data[attr])
                el[v_type] = vm.$data[attr];
           }
        }
                            
复制代码

2.3 步骤2 执行结果:

使页面能够在vue实例创建的时候将绑定的数据初始化渲染到视图层上。

步骤2执行结果.png

第3步 在用户通过交互改变输入框数据之后,使视图层数据同步发生改变

3.1 思路分析:

这一步从设计模式的角度应该这样看:

1.首先监听用户在`view`视图层的交互,触发的钩子函数会直接改变`vm`实例中的$data值;
2.在vm层中通过数据劫持,监听到$data值改变时,触发订阅者的update方法,实现视图层对应数据的更新。
复制代码

3.2 代码实现:

 //发布者
        class Vue {
            constructor(options) {
                    //绑定根节点元素
                    this.el = options.el;
                      if (this.el !== undefined || this.el !== "" || this.el !== null) {
                        this.rootnode = document.querySelector(this.el);
                    }
                    //将数据存入vm层
                     this.$data = options.data;
                    //创建一个订阅者组成的容器,只要有元素用到了$data中的属性,那么就给容器中$data对应属性的那一项数组,添加一个订阅者,这样,整个页面只要用到了那个属性的,都能建立发布订阅,之后的步骤中只要vm中data一变,都能触发视图更新
                    this.$directive = {}
                    this.observer(this.$data)
                        //依赖搜集(解析出v-model和v-text对应元素,并将data中对应绑定的那个元素值关联上)
                         //为了可以让根节点的各级子节点都能进行依赖搜集,采用递归传祖节点方式进行依赖搜集
                    this.complie(this.rootnode)
        }
                //主要用于数据劫持,并发布更新
            observer(data) {
        //初始化订阅者容器
         for (let key in data) {
                        this.$directive[key] = [];
                        let value = data[key];
                        let _this = this;
                        Object.defineProperty(this.$data, key, {
                            get: function() {
                              return value
                            },
                             set: function(newValue) {
                                if (newValue !== value) {
                                    value = newValue;
                                     //触发跟这个数据绑定的所有订阅者的视图更新方法
                                    _this.$directive[key].forEach((item) => {
                                        item.update()
                                          })
                                }
                            }
                        })
                    }
                }
                  //对指令进行依赖收集
            complie(el) {
                let node = el;
                if (node.children.length > 0) {
                 for (let i = 0; i < node.children.length; i++) {
                        let childNode = node.children[i];
                        if (childNode.children.length > 0) {
                        //传入含有子节点的子节点元素
                            this.complie(childNode)
                        } else {
                          if (childNode.hasAttribute('v-text')) {
                                 // console.log(childNode, "通过v-text绑定了:", childNode.getAttribute('v-text'))
                                let _this = this;
                                 this.$directive[childNode.getAttribute('v-text')].push(new Watcher({
                                    el: childNode,
                                    vm: _this,
                                    attr: childNode.getAttribute('v-text'),
                                    v_type: 'innerHTML'
                                 }))
                            }
                            if (childNode.hasAttribute('v-model')) {
                               this.$directive[childNode.getAttribute('v-model')].push(new Watcher({
                                        el: childNode,
                                        vm: this,
                                        attr: childNode.getAttribute('v-model'),
                                        v_type: 'value'
                                    }))
                                     //监听视图层的交互,让vm层中绑定的数据改变
                                childNode.addEventListener('input', (function() {
                                    return function(val) {
                                    console.log(val)
                                        this.$data[childNode.getAttribute('v-model')] = val.target.value;

                                    }
                                })().bind(this))
                                                                  }
                        }
                    }
                }
            }
        }
           //订阅者(主要用于更新view视图层)
        class Watcher {
            constructor(options) {
                    this.el = options.el;
                     this.vm = options.vm;
                    this.attr = options.attr;
                    this.v_type = options.v_type;
                     this.update();

                }
                 //依据传入的元素,元素对应绑定的值,渲染到视图层(订阅者不负责判断数据是否改变等,只负责渲染数据到视图层)
            update() {
                let {
                    el,
                    vm,
                    attr,
                    v_type
                } = this;
                 console.log(vm, attr, el, v_type, vm.$data[attr])
                el[v_type] = vm.$data[attr];
            }
          }
复制代码

3.3 代码实现结果

如下图所示,经过了第三步的开发后,已经实现通过监听view层交互,修改vm层中$data的值,并在$data修改后自动渲染到view视图层中。在区域2输入框输入值时,区域1绑定的这个值也会同步改变。

步骤3执行结果.png

3.实现vue响应式思路总结

这次实现主要是结合MVVM的设计模式,总的说来分为以下几步;

1.构建vm层,实例化时将model层的数据转化到vm层,即将传入的data作为vue实例的参数$data。
2.实现`vm层-view层`中的渲染(初次时通过对含v-text、v-model的元素进行依赖收集并渲染); 
3.vm层中的数据渲染到view层,其中需要建立观察者-订阅模式,可以把Vue看成发布者,Watcher看成订阅者(负责view层视图渲染)。
4.监听用户在`view`视图层的交互,触发的钩子函数会直接改变`vm`实例中的$data值;
5.在vm层中通过数据劫持,监听到$data值改变时,触发订阅者的update方法,实现视图层对应数据的更新。
进而最终实现了实例化Vue对象,根节点子元素中v-model、v-text指令以及model-vm-view的数据双向绑定,即数据响应式
复制代码

以上就是本文的全部内容,如果觉得写得还阔以,请高抬贵手,点波小赞咯~ ~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享