关于javascript:Vue数据双向绑定的简易实现

4次阅读

共计 5835 个字符,预计需要花费 15 分钟才能阅读完成。

1. 前言

本文次要参考了_潭州课堂 Remi 老师_的上课录制视频。

本文适宜于学习 Vue 源码的高级学者,浏览后,你将对 Vue 的数据双向绑定原理有一个大抵的理解,意识 ObserverCompileWathcer 三大角色(如下图所示)以及它们所施展的性能。

本文将一步步带你实现简易版的数据双向绑定,每一步都会详细分析这一步要解决的问题以及代码为何如此写,因而,在浏览完本文后,心愿你能本人入手实现一个简易版数据双向绑定。

2. 代码实现

2.1 目标剖析

本文要实现的成果如下图所示:

本文用到的 HTML 和 JS 主体代码如下:

<div id="app">
    <h1 v-text="msg"></h1>
    <input type="text" v-model="msg">
    <div>
        <h1 v-text="msg2"></h1>
        <input type="text" v-model="msg2">
    </div>
</div>
let vm = new Vue({
        el: "#app",
        data: {
            msg: "hello world",
            msg2: "hello xiaofei"
        }
    })

咱们将依照上面三个步骤来实现:

  • 第一步:将 data 中的数据同步到页面上,实现 M ==> V 的初始化;
  • 第二步:当 input 框中输出值时,将新值同步到 data 中,实现 V ==> M 的绑定;
  • 第三步:当 data 数据产生更新的时候,触发页面发生变化,实现 M ==> V 的绑定。

2.2 实现过程

2.2.1 入口代码

首先,咱们要发明一个 Vue 类,这个类接管一个 options 对象,同时,咱们要对 options 对象中的无效信息进行保留;

而后,咱们有三个次要模块:ObserverCompileWathcer,其中,Observer用来数据劫持的,Compile用来解析元素,Wathcer是观察者。能够写出如下代码:(ObserverCompileWathcer这三个概念,不必细究,前面会详解解说)。

class Vue {
        // 接管传进来的对象
        constructor(options) {
            // 保留无效信息
            this.$el = document.querySelector(options.el);
            this.$data = options.data;

            // 容器: {属性 1: [wathcer1, wathcer2...], 属性 2: [...]},用来寄存每个属性观察者
            this.$watcher = {};

            // 解析元素: 实现 Compile
            this.compile(this.$el); // 要解析元素, 就得把元素传进去

            // 劫持数据: 实现 Observer
            this.observe(this.$data);  // 要劫持数据, 就得把数据传入
        }
        compile() {}
        observe() {}
    }

2.2.2 页面初始化

在这一步,咱们要实现页面的初始化,即解析出 v-textv-model指令,并将 data 中的数据渲染到页面中。

这一步的关键在于实现 compile 办法,那么该如何解析 el 元素呢?思路如下:

  • 首先要获取到 el 上面的所有子节点,而后遍历这些子节点,如果子节点还有子节点,那咱们就须要用到递归的思维;
  • 遍历子节点找到所有有指令的元素,并将对应的数据渲染到页面中。

代码如下:(次要看 compile 那局部)

class Vue {
        // 接管传进来的对象
        constructor(options) {
            // 获取有用信息
            this.$el = document.querySelector(options.el);
            this.$data = options.data;

            // 容器: {属性 1: [wathcer1, wathcer2...], 属性 2: [...]}
            this.$watcher = {};

            // 2. 解析元素: 实现 Compile
            this.compile(this.$el); // 要解析元素, 就得把元素传进去

            // 3. 劫持数据: 实现 Observer
            this.observe(this.$data);  // 要劫持数据, 就得把数据传入
        }
        compile(el) {
            // 解析元素下的每一个子节点, 所以要获取 el.children
            // 备注: children 返回元素汇合, childNodes 返回节点汇合
            let nodes = el.children;

            // 解析每个子节点的指令
            for (var i = 0, length = nodes.length; i < length; i++) {let node = nodes[i];
                // 如果以后节点还有子元素, 递归解析该节点
                if(node.children){this.compile(node);
                }
                // 解析带有 v -text 指令的元素
                if (node.hasAttribute("v-text")) {let attrVal = node.getAttribute("v-text");
                    node.textContent = this.$data[attrVal]; // 渲染页面
                }
                // 解析带有 v -model 指令的元素
                if (node.hasAttribute("v-model")) {let attrVal = node.getAttribute("v-model");
                    node.value = this.$data[attrVal];
                }
            }
        }
        observe(data) {}}

这样,咱们就实现页面的初始化了。

2.2.3 视图影响数据

因为 input 带有 v-model 指令,因而咱们要实现这样一个性能:在 input 框中输出字符,data中绑定的数据产生相应的扭转。

咱们能够在 input 这个元素上绑定一个 input 事件,事件的成果就是:将 data 中的相应数据批改为 input 中的值。

这一部分的实现代码比较简单,只有看标注那个中央就明确了,代码如下:

class Vue {constructor(options) {this.$el = document.querySelector(options.el);
            this.$data = options.data;
            
            this.$watcher = {};    

            this.compile(this.$el);

            this.observe(this.$data);
        }
        compile(el) {
            let nodes = el.children;

            for (var i = 0, length = nodes.length; i < length; i++) {let node = nodes[i];
                if(node.children){this.compile(node);
                }
                if (node.hasAttribute("v-text")) {let attrVal = node.getAttribute("v-text");
                    node.textContent = this.$data[attrVal];
                }
                if (node.hasAttribute("v-model")) {let attrVal = node.getAttribute("v-model");
                    node.value = this.$data[attrVal];
                    // 看这里!!只多了三行代码!!node.addEventListener("input", (ev)=>{this.$data[attrVal] = ev.target.value;
                        // 能够试着在这里执行:console.log(this.$data),
                        // 就能够看到每次在输入框输出文字的时候,data 中的 msg 值也产生了变动
                    })
                }
            }
        }
        observe(data) {}}

2.2.4 数据影响视图

至此,咱们曾经实现了:当咱们在 input 框中输出字符的时候,data中的数据会主动产生更新;

本大节的次要工作是:当 data 中的数据产生更新的时候,绑定了该数据的元素会在页面上自动更新视图。具体思路如下:

1) 咱们将要实现一个 Wathcer 类,它有一个 update 办法,用来更新页面。观察者的代码如下:

class Watcher{constructor(node, updatedAttr, vm, expression){
            // 将传进来的值保存起来,这些数据都是渲染页面时要用到的数据
            this.node = node;
            this.updatedAttr = updatedAttr;
            this.vm = vm;
            this.expression = expression;
            this.update();}
        update(){this.node[this.updatedAttr] = this.vm.$data[this.expression];
        }
    }

2) 试想,咱们该给哪些数据增加观察者?何时给数据增加观察者?

在解析元素的时候,当解析到 v-textv-model指令的时候,阐明这个元素是须要和数据双向绑定的,因而咱们在这时往容器中增加观察者。咱们需用到这样一个数据结构:{属性 1: [wathcer1, wathcer2...], 属性 2: [...]},如果不是很清晰,能够看下图:

能够看到:vue实例中有一个 $wathcer 对象,$wathcer的每个属性对应每个须要绑定的数据,值是一个数组,用来寄存察看了该数据的观察者。(备注:Vue源码中专门发明了 Dep 这么一个类,对应这里所说的数组,本文属于繁难版本,就不过多介绍了)

3) 劫持数据:利用对象的拜访器属性 gettersetter做到当数据更新的时候,触发一个动作,这个动作的次要目标就是让所有察看了该数据的观察者执行 update 办法。

总结一下,在本大节咱们须要做的工作:

  1. 实现一个 Wathcer 类;
  2. 在解析指令的时候(即在 compile 办法中)增加观察者;
  3. 实现数据劫持(实现 observe 办法)。

残缺代码如下:

    class Vue {
        // 接管传进来的对象
        constructor(options) {
            // 获取有用信息
            this.$el = document.querySelector(options.el);
            this.$data = options.data;

            // 容器: {属性 1: [wathcer1, wathcer2...], 属性 2: [...]}
            this.$watcher = {};

            // 解析元素: 实现 Compile
            this.compile(this.$el); // 要解析元素, 就得把元素传进去

            // 劫持数据: 实现 Observer
            this.observe(this.$data);  // 要劫持数据, 就得把数据传入
        }
        compile(el) {
            // 解析元素下的每一个子节点, 所以要获取 el.children
            // 拓展: children 返回元素汇合, childNodes 返回节点汇合
            let nodes = el.children;

            // 解析每个子节点的指令
            for (var i = 0, length = nodes.length; i < length; i++) {let node = nodes[i];
                // 如果以后节点还有子元素, 递归解析该节点
                if (node.children) {this.compile(node);
                }
                if (node.hasAttribute("v-text")) {let attrVal = node.getAttribute("v-text");
                    // node.textContent = this.$data[attrVal]; 
                    // Watcher 在实例化时调用 update, 代替了这行代码

                    /**
                     * 试想 Wathcer 要更新节点数据的时候要用到哪些数据? 
                     * e.g.     p.innerHTML = vm.$data[msg]
                     * 所以要传入的参数顺次是: 以后节点 node, 须要更新的节点属性, vue 实例, 绑定的数据属性
                    */
                    // 往容器中增加观察者:  {msg1: [Watcher, Watcher...], msg2: [...]}
                    if (!this.$watcher[attrVal]) {this.$watcher[attrVal] = [];}
                    this.$watcher[attrVal].push(new Watcher(node, "innerHTML", this, attrVal))
                }
                if (node.hasAttribute("v-model")) {let attrVal = node.getAttribute("v-model");
                    node.value = this.$data[attrVal];

                    node.addEventListener("input", (ev) => {this.$data[attrVal] = ev.target.value;
                    })

                    if (!this.$watcher[attrVal]) {this.$watcher[attrVal] = [];}
                    // 不同于上处用的 innerHTML, 这里 input 用的是 vaule 属性
                    this.$watcher[attrVal].push(new Watcher(node, "value", this, attrVal))
                }
            }
        }
        observe(data) {Object.keys(data).forEach((key) => {let val = data[key];    // 这个 val 将始终保留在内存中, 每次拜访 data[key], 都是在拜访这个 val
                Object.defineProperty(data, key, {get() {return val;    // 这里不能间接返回 data[key], 不然会陷入有限死循环
                    },
                    set(newVal) {if (val !== newVal) {val = newVal;// 同理, 这里不能间接对 data[key]进行设置, 会陷入死循环
                            this.$watcher[key].forEach((w) => {w.update();
                            })
                        }
                    }
                })
            })
        }
    }

    class Watcher {constructor(node, updatedAttr, vm, expression) {
            // 将传进来的值保存起来
            this.node = node;
            this.updatedAttr = updatedAttr;
            this.vm = vm;
            this.expression = expression;
            this.update();}
        update() {this.node[this.updatedAttr] = this.vm.$data[this.expression];
        }
    }

    let vm = new Vue({
        el: "#app",
        data: {
            msg: "hello world",
            msg2: "hello xiaofei"
        }
    })

至此,代码就实现了。

3. 将来的打算

用设计模式的常识,剖析下面这份源码存在的问题,并和 Vue 源码进行比对,算是对 Vue 源码的解析

正文完
 0