美文网首页
双向绑定

双向绑定

作者: 达文西_Huong | 来源:发表于2020-06-17 10:35 被阅读0次

实现双向绑定Proxy比defineproperty优劣如何?

关于双向绑定,其实只要涉及到MVVM框架就不得不提到的问题,双向绑定是Vue三要素之一。

Vue三要素

  • 响应式: 例如如何监听数据变化,其中的实现方法就是我们提高的双向绑定
  • 模板引擎: 如何解析模板
  • 渲染: Vue如何将监听到的数据变化和解析后的HTML进行渲染。

下面介绍的双向数据绑定的方法是基于数据劫持的双向绑定。

常见的基于数据挟持的双向绑定有两种实现

一个是目前Vue2.0的Object.defineProperty
另一个是Vue3.0将要使用的Proxy

通过下面的讲述,会为你揭晓为啥Proxy会替代Object.defineProperty

1. 基于数据劫持实现的双向绑定特点

1.1 什么是数据劫持

数据劫持比较好理解,通常我们利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,从而进一步进行操作。

    const data = {
    name: ''
}

function say(name) {
    if (name === '古天乐') {
        console.log('给大家推荐一款超好玩的游戏');
    } else if (name === '渣渣辉') {
        console.log('戏我演过很多,可游戏我只玩贪玩懒月');
    } else {
        console.log('来做我的兄弟');
    }
}

Object.keys(data).forEach((key) => {
    Object.defineProperty(data, key, {
        enumerable: true,   // 可遍历
        configurable: true, // 属性描述符可改变
        get: function () {
            console.log('get')
        },
        set: function (newValue) {
            console.log(`大家好,我是${newValue}`)
            say(newValue)
        }
    })
})

data.name = 'huong'
// 大家好,我是huong ---- 数据变化的时候可以监听得到
// 来做我的兄弟
1.2数据劫持的优势

目前业界上存在的两种数据劫持,一种是单向劫持,一种是双向劫持。不过在此我们暂且不讨论单向或者双向的优劣,我们需要讨论一下对比其他双向绑定的实现方法,数据劫持的优势所在。

  1. 无需显式调用:例如Vue运用数据劫持 + 发布订阅,直接可以通知变化并驱动视图。就上我们上面所展示的例子,data.name="huong"设置后直接就触发变更,而其他的比如react需要显示调用setStare

不过本人没有了解过其他的框架,但是我看文章这样的表达,似乎优点类似微信小程序的setData?。这部分还有待考究

  1. 可以精确得知变化数据:还是上面的例子,我们劫持了属性的setter,当属性值改变,我们可以精确的获知改变的newValue,因此在这部分不需要额外的diff操作。否则如果我们只知道数据变化了,但是不知道具体哪里发生了变化的时候就需要大量的diff找出变化之,这是需要额外的消耗的。

而小程序的数据变化使用的就是diff来对比变化,然后再更新到视图之上的。

1.3基于数据劫持双向绑定的思路

数据劫持是双向绑定的各种方案中比较流行的一种,最著名的实现就是Vue。

基于数据劫持的双向绑定离不开proxyObject.defineProperty等方法对对象/对象属性的"劫持",我们要实现一个完整的双向绑定需要以下几个要点:

  1. 利用proxyObject.defineProperty生成的Observe针对对象/对象的属性进行劫持,在属性发生改变后通知到订阅者
  2. 解析器Compile解析模板中的Directive(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染。
  3. Watcher属于Observer和Compile桥梁,它将接收到Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化

我们可以看到,虽然Vue运用了数据劫持,但是仍然离不开发布订阅模式。如果不熟悉,需要学习一下。

2.基于Object.defineProperty双向绑定的特点

关于Object.defineProperty的文章在网络上有很多,以下我们主要讲解Object.defineProperty的特点,方便接下来与propxy作对比

2.1 极简版的双向绑定

我们都知道.Object.defineProperty的作用就是劫持一个对象的属性,通常我们对属性的gettersetter方法进行劫持,在对象的属性发生改变时进行特定的操作

我们就对对象objtext属性进行劫持,在获取此属性的值时,打印get val,在更改属性值的时候对DOM进行操作,这就是一个极简的双向绑定

    const obj = {}
    Object.defineProperty(obj,'text',{
        get:function(){
            console.log('get val')
        },
        set:function(newValue){
            console.log('set val' + newValue)
            document.getElementById('input').value = newValue;
            document.getElementById('span').innerHTML = newValue
        }
    })

    const input = document.getElementById('input')
    input.addEventListener('keyup', function(e){
        obj.text = e.target.value
    })

虽然上面实现了双向绑定,但其实这样仅仅只对一个属性的双向绑定,是没有太大用处的,下面将对它进行升级。

2.2升级改造

关于升级原因如下:

  1. 我们只监听了一个属性,一个对象不可能只有一个属性,我们需要对对象每个属性都进行监听
  2. 违反了开放封闭原则,我们每次的修改都需要进入方法内部,这是需要坚决杜绝的。
  3. 代码耦合性严重,可以看到数据和方法还有DOM都是耦合在一起的,这就是所谓的面条代码

下面将解决上面所描述的问题

Vue的操作就是加入了发布订阅者模式,结合Object.defineProperty的劫持能力,实现了可用性很高的双向绑定

发布订阅的角度看我们刚才编写的代码,可以很明显的发现,它的监听,发布和订阅都是写在一起的。所以我们首先需要作的就是解耦。

我们先实现一个订阅发布中心,即消息管理员(Dep),它负责存储订阅者和消息的分发,不管订阅者还是发布者都需要依赖于它

let uid = 0;
// 用于储存订阅者并发布消息
class Dep {
    constructor() {
        // 设置id,用于区分新Watcher和只改变属性值后新产生的Watcher
        this.id = uid++;
        // 储存订阅者的数组
        this.subs = [];
    }
    // 触发target上的Watcher中的addDep方法,参数为dep的实例本身
    depend() {
        Dep.target.addDep(this);
    }
    // 添加订阅者
    addSub(sub) {
        this.subs.push(sub);
    }
    notify() {
        // 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
        this.subs.forEach(sub => sub.update());
    }
}
// 为Dep类设置一个静态属性,默认为null,工作时指向当前的Watcher
Dep.target = null;

现在我们需要实现监听者(Observer),用于监听属性的变化

// 监听者,监听对象属性值的变化
class Observer {
    constructor(value) {
        this.value = value;
        this.walk(value);
    }
    // 遍历属性值并监听(每一个值)
    walk(value) {
        Object.keys(value).forEach(key => this.convert(key, value[key]));
    }
    // 执行监听的具体方法
    convert(key, val) {
        defineReactive(this.value, key, val);
    }
}

function defineReactive(obj, key, val) {
    const dep = new Dep();
    // 给当前属性的值添加监听
    let chlidOb = observe(val);   //  进一步监听到obj的子节点上的obj(递归,直到子元素不再是一个对象)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            // 如果Dep类存在target属性,将其添加到dep实例的subs数组中
            // target指向一个Watcher实例,每个Watcher都是一个订阅者
            // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set: newVal => {
            if (val === newVal) return;
            val = newVal;
            // 对新值进行监听
            chlidOb = observe(newVal);
            // 通知所有订阅者,数值被改变了
            dep.notify();
        },
    });
}

function observe(value) {
    // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
}

接下来我们再实现一个订阅者(Watcher)

    class Watcher {
    constructor(vm, expOrFn, cb) {
        this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者
        this.vm = vm; // 被订阅的数据一定来自于当前Vue实例
        this.cb = cb; // 当数据更新时想要做的事情
        this.expOrFn = expOrFn; // 被订阅的数据
        this.val = this.get(); // 维护更新之前的数据
    }
    // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
    update() {
        this.run();
    }
    addDep(dep) {
        // 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存
        // 此判断是避免同id的Watcher被多次储存
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }
    run() {
        const val = this.get();
        console.log(val);
        if (val !== this.val) {
            this.val = val;
            this.cb.call(this.vm, val);
        }
    }
    get() {
        // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
        Dep.target = this;
        const val = this.vm._data[this.expOrFn];
        // 置空,用于下一个Watcher使用
        Dep.target = null;
        return val;
    }
}

我们最后完成Vue,将上述方法挂在再Vue上

    class Vue {
    constructor(options = {}) {
        // 简化了$options的处理
        this.$options = options;
        // 简化了对data的处理
        let data = (this._data = this.$options.data);
        // 将所有data最外层属性代理到Vue实例上
        Object.keys(data).forEach(key => this._proxy(key));
        // 监听数据
        observe(data);
    }
    // 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
    $watch(expOrFn, cb) {
        new Watcher(this, expOrFn, cb);
    }
    _proxy(key) {
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key],
            set: val => {
                this._data[key] = val;
            },
        });
    }
}

至此,一个简单的双向绑定算是实现了。

2.3Object.defineProperty的缺陷

其实上面的版本还是存在缺陷的,如果将值改为数组,则无法监听其变化。但是Vue文档中的检测方法是用以下几种方式

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

Object.defineProperty的第二个缺陷是,只能挟持对象的属性,因此,我们需要对每个对象进行遍历,甚至如果属性的值是对象,那就需要深度遍历

Object.keys(value).forEach(key => this.convert(key, value[key]));

3.Proxy实现的双向绑定的特点

Proxy 在ES2015规范中被正式发布,它就像一层拦截。外界对该对象的访问,都必须先通过这层拦截。因此它提供了一种机制,可以对外界的访问进行过滤和改写

3.1 Proxy可以直接监听对象而非属性

我们把上面极简版的双向绑定改写成Proxy的版本

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup', function(e) {
  newObj.text = e.target.value;
});

可以看到,Proxy是直接挟持的是整个对象,并且返回的是一个新对象。比起Object.defineProperty显然是更方便的。

3.2 Proxy可以直接监听数组的变化

当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和length的变化,我们可以借此进行操作,以上文中Object.defineProperty无法生效的列表渲染为例。

const list = document.getElementById('list');
const btn = document.getElementById('btn');

// 渲染列表
const Render = {
    // 初始化
    init: function (arr) {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < arr.length; i++) {
            const li = document.createElement('li');
            li.textContent = arr[i];
            fragment.appendChild(li);
        }
        list.appendChild(fragment);
    },
    // 我们只考虑了增加的情况,仅作为示例
    change: function (val) {
        const li = document.createElement('li');
        li.textContent = val;
        list.appendChild(li);
    },
};

// 初始数组
const arr = [1, 2, 3, 4];

// 监听数组
const newArr = new Proxy(arr, {
    get: function (target, key, receiver) {
        console.log(key);
        return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key !== 'length') {
            Render.change(value);
        }
        return Reflect.set(target, key, value, receiver);
    },
});

// 初始化
window.onload = function () {
    Render.init(arr);
}

// push数字
btn.addEventListener('click', function () {
    newArr.push(6);
});

很显然,Proxy不需要那么多hack(即使hack也无法完美实现监听)就可以无压力监听数组的变化,我们都知道,标准永远优先于hack。

3.3 Proxy的其他优势

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。

Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。

当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。

原文:https://www.jianshu.com/p/2df6dcddb0d7

相关文章

  • Vue 中的双向数据绑定

    双向绑定 单向数据流 双向绑定 or 单向数据流 Vue 是单向数据流,不是双向绑定 Vue 的双向绑定是语法糖 ...

  • vue2.0双向绑定的使用及简单实现

    v-model双向绑定用法input的双向绑定 chechbox(利用value值) v-model双向绑定简单实...

  • Vue之表单双向数据绑定和组件

    三、表单双向数据绑定和组件 目录:双向数据绑定、组件 1.双向数据绑定 1)什么是双向数据绑定Vue.js是一个M...

  • vue 面试汇总(更新中...)

    1.说说对双向绑定的理解 1.1、双向绑定的原理是什么 我们都知道Vue是数据双向绑定的框架,双向绑定由三个重要部...

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • [转] DataBinding 数据绑定

    数据绑定分为单项绑定和双向绑定两种。单向绑定上,数据的流向是单方面的,只能从代码流向 UI;双向绑定的数据是双向的...

  • Vue双向数据绑定v-model

    v-model 数据双向绑定用作双向数据绑定 一、组件内部双向数据绑定 1、在实例的data中,设置content...

  • 「1分钟--前端01」vue双向绑定

    目录 ⊙常见双向绑定的实现方法 ⊙基于数据劫持双向绑定的优点 ⊙基于Object.defineProperty双向...

  • 02Vue.js的数据绑定

    理解Vue的双向数据绑定 Vue有一个显著的地方就是它拥有双向数据绑定功能,那么何为双向数据绑定呢?双向是指:HT...

  • Vue入门(二)——数据绑定

    一、什么是双向数据绑定 双向数据绑定是Vue的核心功能之一。所谓双向数据绑定是指:HTML标签上的数据绑定到Vue...

网友评论

      本文标题:双向绑定

      本文链接:https://www.haomeiwen.com/subject/tkvjxktx.html