前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue 随记(1):数据劫持

vue 随记(1):数据劫持

作者头像
一粒小麦
发布2020-07-13 15:17:02
4680
发布2020-07-13 15:17:02
举报
文章被收录于专栏:一Li小麦一Li小麦

引子

开发中可能时常遇到这种问题:

开发一个计算器界面如下,

html 结构为:

代码语言:javascript
复制
<div>
    <button id="minus">-</button> <input type="number" id="r"> <button id="plus">+</button>
    <p>r<sup>2</sup> = <span id="square"></span></p>
    <p>r<sup>3</sup> = <span id="cube"></span></p>
</div>

要求:点击加减号,输入框内容自动增加或减去1,输入时允许数字,当数字变动时,自动更新下面的r平方和r立方的内容。

按照一般的开发节奏,应该这么做

代码语言:javascript
复制
const calc = r => {
    document.querySelector(`#square`).innerHTML = Math.pow(r, 2);
    document.querySelector(`#cube`).innerHTML = Math.pow(r, 3);
}


document.querySelector(`#r`).addEventListener('change', e => {
    const r = e.target.value;
    calc(r);
})

document.querySelector(`#plus`).addEventListener('click', e => {
    const r = document.querySelector(`#r`).value;
    const newR = Number(r) + 1;
    document.querySelector(`#r`).value = newR;
    calc(newR);
})

document.querySelector(`#minus`).addEventListener('click', e => {
    const r = document.querySelector(`#r`).value;
    const newR = Number(r) - 1;
    document.querySelector(`#r`).value = newR;
    calc(newR);;
})

触发数据改变的来源不止一个,意味着每个来源都需要调用代码中的calc方法。如果我要加多一个按钮,点击后输入框内容+10,不但要从视图层取数据修改(e.targer.value),input的内容,还得把calc方法带上,十分痛苦。

这时就可以考虑用双向绑定处理r的数据变化。我预想通过一个对象来管理这个案例中的关键数据——r值,为了防止轻易被篡改,通过仿react的getStatesetState api 来获取和设置。而不再通过视图层取。

其实已经是一个老掉牙的问题了。数据劫持是双向绑定各种方案中比较流行的一种,最著名的实现就是Vue。而vue在2.x版本中使用的是Object.defineProperty,将在今年8月发布的3.0中,将正式使用Proxy

Object.defineProperty

语法:

代码语言:javascript
复制
Object.defineProperty(obj, prop, descriptor)

我们可以通过Object.defineProperty这个方法,直接在一个对象上定义一个新的属性,或者是修改已存在的属性。最终这个方法会返回该对象。

MDN 文档地址:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

看上去很不错,来写一下:

代码语言:javascript
复制
class Responsive {
    constructor(opts) {
        this.opts = opts;

        const { data } = this.opts;

        Object.keys(data).forEach(key => {
            Object.defineProperty(data, key, {
                get() {
                   data[key]
                },

                set(nick) {
                    console.log(data[key], nick)
                    data[key] = nick;
                }
            })
        })

    }

    getState(prop) {
        const data = this.opts.data;
        return data[prop];
    }

    setState(prop, value) {
        const data = this.opts.data;
        data[prop] = value;
    }
}



const res = new Responsive({
    data:{
        r:1
    }
})

res.getState('r')

结果浏览器爆栈了。

原因在于:给r定义了setter,然后在setter里面又给r赋值,就是又调用了setter,循环调用了。

处理循环调用可以考虑深拷贝克隆一个data。那么,每次获取,或者是设定,都会从克隆体去取值或者是更新。

代码语言:javascript
复制
// ...
Object.keys(data).forEach(key => {
    _data[key] = data[key];
    Object.defineProperty(data, key, {
        get() {
            return _data[key];
        },

        set(nick) {
            console.log(data[key], nick)
            _data[key] = nick;
        }
    })
})
//...

这段代码就是笔者工作中重要的一段处理逻辑了。

现在在setState的时候,我们都可以去劫持数据的变化,那么我们模仿vue,可以加上watch切面:

代码语言:javascript
复制
class Responsive {
    constructor(opts) {
        this.opts = opts;
        const { data } = this.opts;
        const _data = {};

        Object.keys(data).forEach(key => {
            _data[key] = data[key];
            Object.defineProperty(data, key, {
                get() {
                    return _data[key];
                },

                set(nick) {
                    const old = _data[key];
                    _data[key] = nick;
                    opts.watch[key] && opts.watch[key](old,nick);
                }
            })
        })

    }

    getState(prop) {
        const data = this.opts.data;
        return data[prop];
    }

    setState(prop, value) {
        const data = this.opts.data;
        data[prop] = value;
    }
}

接下来去写文章开头的代码:

代码语言:javascript
复制
const calc = r => {
    document.querySelector(`#r`).value = r;
    document.querySelector(`#square`).innerHTML = Math.pow(r, 2);
    document.querySelector(`#cube`).innerHTML = Math.pow(r, 3);
}

const res = new Responsive({
    data: {
        r: 0
    },
    watch: {
        r: function (oldVal, newVal) {
            console.log(`r被修改:${oldVal}->${newVal}`);
            calc(newVal)
        }
    }
});


document.querySelector(`#r`).addEventListener('change', e => {
    res.setState('r',Number(e.target.value));
})

document.querySelector(`#minus`).addEventListener('click', e => {
    const r = res.getState('r');
    res.setState('r',r - 1);
})

document.querySelector(`#plus`).addEventListener('click', e => {
    const r = res.getState('r');
    res.setState('r',r + 1);
})

上面的代码通过watch来注入calc的副作用。实现了r对#r/#square/#cube几个视图层的绑定。对于三个触发r值改变的操作——加,减,输入,关注点就可以集中于setState了。比起原来的代码可算优雅了不少。

defineProperty是es5方法。因此可在任何ie >= 8的浏览器上正常运行。

Proxy

最早传出 vue 3.0 说要出,已经过了2年有余了。vue 3其中一项重要的改变就是在响应式方面,使用Proxy来替代原有的defineProperty。

Proxy是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。

•Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。•Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。•使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。

语法:

代码语言:javascript
复制
const p = new Proxy(target, handler);

说明:

•target 是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。•handler 是一个对象,其声明了代理target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。•p 是代理后的对象。当外界每次对 p 进行操作时,就会执行 handler 对象上的一些方法。Proxy共有13种劫持操作,handler代理的一些常用的方法有如下几个:•重要的方法有get(读取)、set(修改)、has(判断是否有属性)和constructor(构造函数)等。

MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

实现和上面代码一样的api,需要定义一个handler生成器:

代码语言:javascript
复制
class Responsive {
    // 根据配置生成hadler
    static createHandler = (opts) => {
      const { watch } = opts;
      const handler = {
        get: function (target, prop) {
          return target[prop];
        },

        set: function (target, prop, newVal) {
          let old = target[prop];
          target[prop] = newVal;
          // 在设置对象的属性时
          if (watch && watch[prop]) {
            watch[prop](old, newVal);
          }

          return true;
        }
      }
      return handler;
    };

    // ...
}

然后在构造函数中调用。

代码语言:javascript
复制

    constructor(opts) {
      const { data } = opts;
      const handler = Responsive.createHandler(opts);
      this.proxy = new Proxy(data, handler);
    }

    setState(key, value) {
      if (key) {
        this.proxy[key] = value;
      }
    }

    getState(key, value) {
      if (key) {
        return this.proxy[key];
      }
    }

那么就实现了一样的功能。写法较defineProperty更为优雅些。在笔者的正在进行项目的代码中,也使用了该代码段。

小结

如果是满足日常的封装需要,那么本文就是时候结束于此。

但是写到这里的我,应该有了更多问号。

•目前的封装很不完善。只能监听对象第一层的内容。对于复杂数据类型,不支持。所谓的数据绑定,依然是在watchapi上手工把calc方法写进去的。能不能进一步提供类似vue一样的体验呢?•Proxy对于defineProperty的优势是哪里?•Vue 3 的新变化 ?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引子
  • Object.defineProperty
  • Proxy
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档