为了账号安全,请及时绑定邮箱和手机立即绑定

【Vue 原理】Vue 是如何代理 data、methods 和 props 的?

前言

Vue 有一个非常有趣的功能,就是我们所有传进去的 datamethods 或者 props,都会挂载到 Vue 实例上, 我们可以通过 this.xxx 的简单做法来进行访问。那么,这到底是怎么实现的呢?

源码实现

首先可以来看看源码部分。相关的源码实现就在 src\core\instance\state.js 文件下。

methods

由于 methodsdata 还有 props 的实现不一致,因此这里简单拉出来单独讲。

首先把目光聚焦在 initMethods 上。

function initMethods (vm: Component, methods: Object) {
    const props = vm.$options.props
    for (const key in methods) {
        if (process.env.NODE_ENV !== 'production') {
            if (typeof methods[key] !== 'function') {
                warn(
                    `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
                    `Did you reference the function correctly?`,
                    vm
                )
            }
            if (props && hasOwn(props, key)) {
                warn(
                    `Method "${key}" has already been defined as a prop.`,
                    vm
                )
            }
            if ((key in vm) && isReserved(key)) {
                warn(
                    `Method "${key}" conflicts with an existing Vue instance method. ` +
                    `Avoid defining component methods that start with _ or $.`
                )
            }
        }
        vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
    }
}

这里主要执行了这么几步骤:

  1. 拿到 vm.$options 上的 props
  2. 遍历 methods 上的属性, 分别判断是否是函数、是否 props上已经有了相同属性名、是否和现有 Vue 实例方法冲突。
  3. 最后,直接赋值给 vm[key] 上进行代理,并且通过 bind 方法 绑定 this

这里主要是最后一个步骤,使得我们能够直接在 Vue 实例上访问 methods 中的方法。

data 和 props

从 data 初始化开始

本章节从 data 的初始化上入手,相关方法为 initData

function initData (vm: Component) {
    let data = vm.$options.data
    data = vm._data = typeof data === 'function'
        ? getData(data, vm)
    : data || {}
    if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
            'data functions should return an object:\n' +
            'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
            vm
        )
    }
    // proxy data on instance
    const keys = Object.keys(data)
    const props = vm.$options.props
    const methods = vm.$options.methods
    let i = keys.length
    while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
            if (methods && hasOwn(methods, key)) {
                warn(
                    `Method "${key}" has already been defined as a data property.`,
                    vm
                )
            }
        }
        if (props && hasOwn(props, key)) {
            process.env.NODE_ENV !== 'production' && warn(
                `The data property "${key}" is already declared as a prop. ` +
                `Use prop default value instead.`,
                vm
            )
        } else if (!isReserved(key)) {
            proxy(vm, `_data`, key)
        }
    }
    // observe data
    observe(data, true /* asRootData */)
}

简单的看一下,initData 主要执行了:

  1. 获取 data
  2. data 赋值到 vm._data 上。
  3. 遍历 data 中的 key,判断是否在 methodsprops 中存在同样的 key, 存在则报个 warnning
  4. 通过 proxy 函数代理key

其中,步骤三是因为 methods 和 props 的属性最终也可以直接通过 Vue 实例进行访问,因此我们需要确保 key 的唯一性。

而步骤四才是本章的核心函数,它实现了 vm.xxxvm._data.xxx 的访问。当然 props 也是同理。

proxy 的庐山真面目

const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
    sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
    }
    sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

可以清楚的看到,首先定义了一个 属性描述符 sharedPropertyDefinitiongetset 初始指定为一个空函数。而proxy 方法 接受三个参数,分别是:

  • target:目标代理对象。
  • sourceKey:代理对象的数据源所对应的key,如果代理的是 _data ,就传入 "_data"
  • key:数据源的 key

proxy 中,sharedPropertyDefinitiongetset方法会被重写,并通过 Object.defineProperty(target, key, sharedPropertyDefinition)来进行属性的代理。

那么结合到代码中的实现,在 initData 阶段,我们执行了 proxy(vm, '_data', key) ,那么,对于这里而言,target 事实上就是 vm, 而 key 则是 vm._data 中的属性 key 。通过重写后的 getset 方法不难看出,假设 adata 中的数据,那么当我们访问 vm.a 的时候,实际上访问的是 vm._data.a;而当我们赋值 vm.a = 1 的时候,实际上会代理到 vm._data.a = 1 上去。

至此,proxy 已经真相了,props 也同样通过 proxy 来代理属性的访问

proxy(vm, `_props`, key)

这里我们不主张直接通过 vm._data.xxx 的方式来进行操作,一方面下划线从规范来讲属于私有属性,是不允许被直接访问的;另一方面, 直接访问 _data 可能会造成一些不可预见的 bug,比方说新增属性不会经过响应式处理。

自己实现个乞丐版

学过一个东西,肯定要自己造个轮子来简单验证一下。直接上代码:

// vue 如何通过 this.xxx 访问 this._data.xxx

const noop = () => {}

const sharedPropertypeDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
}

function proxy (target, sourceKey, key) {
    sharedPropertypeDefinition.get = function proxyGet () {
        return target[sourceKey][key]
    }
    sharedPropertypeDefinition.set = function proxySet (val) {
        target[sourceKey][key] = val
    }
    Object.defineProperty(target, key, sharedPropertypeDefinition)
}

function Vue (data) {
    this._data = data
    Object.keys(this._data).forEach(key => {
        proxy(this, '_data', key)
    })
}

const vueIns = new Vue({a: 1, b: 2})

console.log(vueIns.a)
console.log(vueIns.b)

vueIns.a = 111
vueIns.b = 222

console.log(vueIns.a)
console.log(vueIns.b)

总结

  • Vue 会代理 datamethodsprops 上的属性。我们可以直接通过 this.key 的方式来进行访问和操作。
  • Vue 会 确保 datamethodsprops 上的属性具有唯一性。
  • proxy 通过 Object.defineProperty 的方式来进行属性代理。

线上博客

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
1
获赞与收藏
13

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消