Vue3.0中的双向数据绑定原理实现

作者: 贺鹏飞 分类: Vue,数据可视化 发布时间: 2021-01-31 10:50

前言

Vue3.0是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。Vue3.0与Vue2.0的区别仅是数据劫持的方式由Object.defineProperty更改为Proxy代理,其他代码不变。

Object.defineProperty缺点

1、在Vue中,Object.defineProperty没法监控到数组下标的变化,致使直接经由过程数组的下标给数组设置值,不能及时相应。为了处置惩罚这个题目,经由vue内部处置惩罚后可以运用以下几种要领来监听数组,分别是push() 、pop() 、shift()、 unshift() 、splice() 、sort()、 reverse(),Vue.set()对于数组的处理其实就是调用了splice方法

2、Object.defineProperty只能挟制对象的属性,因而我们须要对每一个对象的每一个属性举行遍历。Vue里,是经由过程递归以及遍历data对象来完成对数据的监控的,假如属性值也是对象那末须要深度遍历,明显假如能挟制一个完全的对象,不管是对操纵性照样机能都邑有一个很大的提拔。

Proxy

Proxy 也就是代理,可以帮助我们完成很多事情,例如对数据的处理,对构造函数的处理,对数据的验证,说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义,因此提供了一种机制,可以对外界的访问进行过滤和改写。

语法:

let p = new Proxy(target, handler);

target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。具体的handler相关函数请查阅官网。

  let w3cjs = {
     name: "w3cjs",
     age: 99
  };
  w3cjs = new Proxy(w3cjs, {
    get(target, key) {
         let result = target[key];
         //如果是获取 年龄 属性,则添加 岁字
         if (key === "age") result += "岁";
         return result;
    },
    set(target, key, value) {
           if (key === "age" && typeof value !== "number") {
           throw Error("age字段必须为Number类型");
        }
        return Reflect.set(target, key, value);
    }
  });
  console.log(`我叫${w3cjs.name}  我今年${w3cjs.age}了`);
  w3cjs.age = 100;

上方案例中定义了 w3cjs对象,其中有 agename 两个字段,我们在Proxy中的 get 拦截函数中添加了一个判断,如果是取 age 属性的值,则在后面添加 。在 set 拦截函数中判断了如果是更改 age 属性时,类型不是 Number则抛出错误。最后输出正确结果:我叫w3cjs  我今年99岁了。

Proxy支持拦截的操作,一共有13种:

  • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

Vue 3.0双向绑定原理的实现

1. 定义构造函数

function Vue(option){
    this.$el = document.querySelector(option.el);   //获取挂载节点
    this.$data = option.data;
    this.$methods = option.methods;
    this.deps = {};     //所有订阅者集合 目标格式(一对多的关系):{msg: [订阅者1, 订阅者2, 订阅者3], info: [订阅者1, 订阅者2]}
    this.observer(this.$data);  //调用观察者
    this.compile(this.$el);     //调用指令解析器
}

2. 定义指令解析器

Vue.prototype.compile = function (el) {
    let nodes = el.children; //获取挂载节点的子节点
    for (var i = 0; i < nodes.length; i++) {
        var node = nodes[i];
        if (node.children.length) {
            this.compile(node) //递归获取子节点
        }
        if (node.hasAttribute('l-model')) { //当子节点存在l-model指令
            let attrVal = node.getAttribute('l-model'); //获取属性值
            node.addEventListener('input', (() => {
                this.deps[attrVal].push(new Watcher(node, "value", this, attrVal)); //添加一个订阅者
                let thisNode = node;
                return () => {
                    this.$data[attrVal] = thisNode.value //更新数据层的数据
                }
            })())
        }
        if (node.hasAttribute('l-html')) {
            let attrVal = node.getAttribute('l-html'); //获取属性值
            this.deps[attrVal].push(new Watcher(node, "innerHTML", this, attrVal)); //添加一个订阅者
        }
        if (node.innerHTML.match(/{{([^\{|\}]+)}}/)) {
            let attrVal = node.innerHTML.replace(/[{{|}}]/g, '');   //获取插值表达式内容
            this.deps[attrVal].push(new Watcher(node, "innerHTML", this, attrVal)); //添加一个订阅者
        }
        if (node.hasAttribute('l-on:click')) {
            let attrVal = node.getAttribute('l-on:click'); //获取事件触发的方法名
            node.addEventListener('click', this.$methods[attrVal].bind(this.$data)); //将this指向this.$data
        }
    }
}

3. 定义观察者(区别在这一块代码)

Vue.prototype.observer = function (data) {
    const that = this;
    for(var key in data){
        that.deps[key] = [];    //初始化所有订阅者对象{msg: [订阅者], info: []}
    }
    let handler = {
        get(target, property) {
            return target[property];
        },
        set(target, key, value) {
            let res = Reflect.set(target, key, value);
            var watchers = that.deps[key];
            watchers.map(item => {
                item.update();
            });
            return res;
        }
    }
    this.$data = new Proxy(data, handler);
}

4. 定义订阅者

function Watcher(el, attr, vm, attrVal) {
    this.el = el;
    this.attr = attr;
    this.vm = vm;
    this.val = attrVal;
    this.update(); //更新视图
}

5. 更新视图

Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.val]
}

6. 使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="./vue.js"></script>
</head>
<body>
    <!--
        实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。就必须要实现以下几点:
            1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
            2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
            3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
            4、mvvm入口函数,整合以上三者
    -->
    <div id="app">
        <input type="text" l-model="msg" >
        <p l-html="msg"></p>
        <input type="text" l-model="info" >
        <p l-html="info"></p>
        <button l-on:click="clickMe">点我</button>
        <p>{{msg}}</p>
    </div>

    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                msg: "W3CJS",
                info: "实现mvvm的双向绑定"
            },
            methods: {
                clickMe(){
                    this.msg = "Vue3.0双向数据绑定原理的实现";
                }
            }
        })
    </script>
</body>
</html>

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注