欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

从Proxy到Vue3数据绑定

程序员文章站 2022-06-19 10:01:52
Proxy...

导言: 本菜鸡在Vue2没多久,结果就Vue3发布了。赶紧学习和体验了一番Vue3,发现和Vue2有较大不同。其中最让我印象深刻的是他们有一个叫ref和reactive的用来绑定和更新数据。然后再略微调查原理之后发现Vue3是用的Proxy来实现数据绑定。我之前对JS语法的Proxy基本完全不懂,正好趁这个机会,深入理解一下Proxy和Vue3的数据绑定。希望对于同样是初学者的你,看完我的文章,能对这两部分知识有更深入的认识。

数据绑定

我们进行网页设计,除了需要设计各个页面组件之外,另外最重要的部分应该就属于数据交互了。经常我们要面对的需求是获取用户的数据或者将一些处理过的数据发送给用户。比如我们用原生代码展示一下上将数据发送给用户:

<p></p>
const data = { value: 'hello' }
document.querySelector('p').innerText = data.value;

等到我们使用Vue之后,Vue引入了一种设计模式称为MVVM,我们不再和DOM直接进行交互。

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

从Proxy到Vue3数据绑定
上图是一个经验的MVVM的示意图,我们在使用Vue的时候,只设计UI以及数据处理的Model。如何将数据渲染在页面上,我们不再关心了。交给Vue来处理。

可以再看一下,我们最开始举得简单代码的例子。之前我们是View和Model直接进行通信的,现在多了一个ViewModel层。就像假设你之前出入小区可以随便出入。现在因为疫情,门口多了一个站岗的保安。你能不能出入不再是由你决定了,而是由保安决定。这样的保安或者ViewModel,可以称之为一种代理。

类似于交易不是双方直接达成,而是要经过中间商才能达成交易。类似于你们之前是直接现金交易,现在都需要用支付宝转到对方账户才行。支付宝就是这样的中间商或者称为代理。有了代理就可以设置一些处理规则,或者拦截等。比如支付宝可以定义一种代理规则:只要是通过我转账到对方账户,我就要收取1%的手续费。

Proxy

我们理解了代理这个概念之间,下面就可以开始讲Proxy。首先看一下MDN关于Proxy的定义:

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

总之,Proxy就是干代理的事。就是前文讲的那个保安或者支付宝。

关于Proxy最简单的讲解,我就不去贴MDN的内容了,如果对Proxy具体语法完全不了解请移步Proxy,Proxy的handler对象方法虽然很多,但最最常用的还是get和set。如果完全不了解Proxy的,别的对象方法可以暂时不看,一定要把这两个方法看一下。

下面我举一些Proxy的通俗例子,并逐渐过渡到Vue3的Proxy中。

首先举一个简单的例子:

let o = {
  name: 'xiaoming',
  age: 20
}

let handler = {
  get(obj, key) {
      return key in obj ? obj[key] : ''
  }
}

let p = new Proxy(o, handler)

console.log(p.from)

这个代码是想表达如果 o 对象有这个 key-value 则直接返回,如果没有一律返回空。这里的p的结果就相当于是一个代理过后的结果了,o的数据不是直接传给p,而是经过经过handler代理逻辑之后才传过去的。对于对象o而言,如果直接访问o.from会报错。对于p而言,因为中间有一个代理程序帮我们处理了一下,所以不会报错,而是返回空。

通过上面的例子,我们初步了解了Proxy的代理特性,下面我们来看一下更复杂一点的例子。

let user = {
    name: 'xiaoming',
    age: 20,
    _password: '***'
}
user = new Proxy(user, {
    get(target, prop) {
        if (prop.startsWith('_')) {
            throw new Error('不可访问')
        } else {
            return target[prop]
        }
    },
    set(target, prop, val) {
        if (prop.startsWith('_')) {
            throw new Error('不可访问')
        } else {
            target[prop] = val
            return true
        }
    },
    deleteProperty(target, prop) { // 拦截删除
        if (prop.startsWith('_')) {
            throw new Error('不可删除')
        } else {
            delete target[prop]
            return true
        }
    },
    ownKeys(target) {
        return Object.keys(target).filter(key => !key.startsWith('_'))
    }
})
console.log(user.age)
console.log(user._password)
user.age = 18
console.log(user.age)
try {
    user._password = 'xxx'
} catch (e) {
    console.log(e.message)
}

try {
    // delete user.age
    delete user._password
} catch (e) {
    console.log(e.message)
}
console.log(user.age)

for (let key in user) {
    console.log(key)
}

大家可以执行一下代码看看输出效果。关于Proxy的基本介绍就先到这里,下面我们来看看Vue3中是如何使用Proxy实现数据绑定的。

Vue3中的Proxy

在Vue3中我们使用ref和reactive来实现数据绑定。更改ref或者reactive实例化对象的值,会导致页面中的数据也发生对应更改。之所以能够实现更改ref或者reactive对应的值使得页面的值发生对应变化是因为我们实际操作的数据是经过Proxy代理过后的,而在Proxy代理中,我们有具体定义页面数据刷新的代码。我用代码举一个很简单的例子:

let obj = {name: 'xiaoming', age: 20}
let state = new Proxy(obj, {
  get (obj, key) {
    return obj[key]
  },
  set (obj, key, value) {
    obj[key] = value
    console.log('更新UI界面')
    // set方法必须通过返回值告诉Proxy此次操作是否成功
    return true
  }
})

console.log(state.name)
state.age = 22

你可以执行一下上述代码看看执行效果。当我们去设置一个值的时候,就会具体执行更能UI界面的操作。通过Proxy就可以实现ref和reactive的页面更新和数据绑定,这也是ref和reactive背后的基本原理。

我们下面来谈谈reactive和ref的Proxy具体实现。

我们在使用Vue3的时候,通常使用ref来监听一些简单数据类型如数字、字符串、布尔等。你可能注意到一种现象,是更改ref的值的时候,需要使用ref.value的形式,(在template中使用ref时,Vue自动帮我们添加.value了,所以不需要我们手动添加),看起来很神奇,为什么会有一个.value呢?是因为ref本质上是一个包装过后的reactive。在你去定义ref的时候,Vue会内部帮我们包装一下。

let age = ref(18)
// 等价于
let age = reactive({value: 18})

对于reactive,使用Proxy来完成对象监听要更复杂一些,因为对象里面可能还嵌套对象,比如下面这种形式的对象:

let arr = [{id:1, name: '鲁班'}, {id:2, name: '虞姬'}]
let obj = {a:{id:1, name: '鲁班'}, b:{id:2, name: '虞姬'}}

所以我们需要对每一层的对象都监听到,才能做到当数据发生变化时,实现对应的更新。要实现一层一层的监听,往往就会想到使用递归来实现。下面是代码演示:

function reactive(obj) {
  if (typeof obj === 'object') {
    if (obj instanceof Array) {
      // 如果是一个数组,则取出数组中每一个元素,
      // 判断每一个元素是否是一个对象,如果又是一个对象,则也需要包装成Proxy
      obj.forEach((item, index) => {
        if (typeof item === 'object') {
          obj[index] = reactive(item)
        }
      })

    } else {
      // 如果是一个对象,则取出对象中的每一个属性,
      // 判断属性是否又是一个对象,如果又是一个对象,则也需要包装成Proxy
      for (let key in obj) {
        let item = obj[key]
        if (typeof item === 'object') {
          obj[key] = reactive(item)
        }
      }
    }

    return new Proxy(obj, {
      get (obj, key) {
        return obj[key]
      },
      set (obj, key, val) {
        obj[key] = val
        console.log('更新UI界面')
        return true
      }
    })

  } else {
    console.warn(`${obj} is not object`)
  }
}

你可以执行一些测试看看效果,看看会输出什么。

let state = reactive(arr)
state[0].name = 'zhangsan'
state[1].id = 3

对于ref而言,因为ref是包装过的reactive,所以使用reactive就可以定义ref:

function ref(val) {
  return reactive({value: val})
}
参考资料

[1] Vue双向数据绑定
[2] Vue面试题总结
[3] Proxy
[4] Vue3 的 Proxy 和 defineProperty 的比较
[5] Vue3.0教程

本文地址:https://blog.csdn.net/Einstellung/article/details/110477861