Vue: 关于data与computed名称冲突合并的策略调整

Created on 1 Apr 2017  ·  5Comments  ·  Source: vuejs/vue

What problem does this feature solve?

抱歉

首先非常抱歉我的英文不是很好, 我使用了尽量多的代码来描述我的需求

例1

在使用Vue拓展(Vue.extend)时,datacomputed对于使用者来说并没有什么区别
目前的处理方式是,data拥有较高的优先级,他会覆盖computed
但我觉得,这样的设计容易产生歧义,使用者往往会认为程序没有达到预期效果,

如下面这个例子, 在定义extend时

var vExt = Vue.extend({
    data: {
        fields: {
            ext_computed1: "ext_computed1"
        }
    },
    computed: {
        ext_computed1: {
            get: function () { return this.fields.ext_computed1; },
            set: function (value) {
                this.fields.ext_computed1 = "ext_computed1:" + value;
            }
        }
    }
});

对于使用者来说,data和computed在使用上没有任何区别
目前的处理方式,容易产生歧义,认为程序没有达到预期效果,如以下代码

var xx = new vExt({
    el: "####x",
    data: {
        ext_computed1: "123"
    }
});

var result = xx.ext_computed1;  //result = "123", but I thought 'result' = "ext_computed1:123"

这时xx.ext_computed1 返回的是 "123", 但使用者预期的可能是 "ext_computed1:123"

例2

另外一种情况正好相反:
如: 拓展定义

var vExt = Vue.extend({
    data: function () {
        return {
            ext_data1: null
        };
    }
});

使用者:

var xx = new vExt({
    el: "####x",
    data: {
        fields: {
            data1: "my:data1"
        }
    },
    computed: {
        ext_data1: {
            get: function () { return this.fields.data1; },
            set: function (value) {
                this.fields.data1 = "my:" + value;
            }
        }
    }
});
var result = xx.ext_computed1;  //result = "ext_data1", but I thought 'result' = "my:ext_data1"

xx.ext_data1 返回的是 "ext_data1", 但预期是 "my:ext_data1"

解释

以上两种情况的预期并不是一个特殊的需求
在大多数服务器语言中他们是非常正常的一个行为
例如C#中的继承

class Parent
{
    public virtual string Name { get; set; } = "xxxx";
}


class Child: Parent
{
    private string _name;

    public override string Name
    {
        get { return _name; }
        set { _name = "my:" + value; }
    }
}

What does the proposed API look like?

修改思路

initState函数中,是先执行initData再执行initComputed
所以我的思路是在 initData 中判断,如果存在 同名的computed 则不生成代理

function initData(vm) {
  var data = vm.$options.data;
  var computed = vm.$options.computed;
  data = vm._data = typeof data === 'function'
    ? data.call(vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    "development" !== '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
  var keys = Object.keys(data);
  var computed = vm.$options.computed;
  var cKeys = computed && Object.keys(computed); //得到 computed 枚举
  var props = vm.$options.props;
  var i = keys.length;
  while (i--) {
    if (props && hasOwn(props, keys[i])) {
      "development" !== 'production' && warn(
        "The data property \"" + (keys[i]) + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(keys[i])) {
      if (cKeys || cKeys.indexOf(keys[i]) === -1) { //当 computed 存在时,不生成代理 
        proxy(vm, "_data", keys[i]);
      }
    }
  }
  // observe data
  observe(data, true /* asRootData */);
  return data;  //返回data值后面用, 这里也可以只返回与 computed 同名的属性
}

上面的处理可以让data不将computed覆盖

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  var initValues = null;
  if (opts.data) {
    initValues = initData(vm); //获取初始化的data
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch) { initWatch(vm, opts.watch); }
  if (!initValues) return;
  var keys = Object.keys(initValues);
  for (var i = 0; i < keys.length; i++) {
      vm[keys[i]] = initValues[keys[i]];  //将data赋值给computed
  }
}

上面的处理,可以让computed拥有初始值,并触发setter(如果有)

Most helpful comment

你的原始例子对 computed 和 options 的理解有偏差。data 就是 data,computed 就是 computed,在 options 里面是完全分开的,只有在创建出来的实例上面共用一个 namespace。

原则上来说,data 和 computed 有重复定义是错误的使用方式,e7e86fd 对这种情况添加了警告。

All 5 comments

在我印象中,尤大的中文也是非常厉害的(知乎刷得溜溜的) 😄

我想你应该精炼一下你的问题:

  var vm = new Vue({
    data: {
      prop1: 'this is data prop'
    },
    computed: {
      prop1: function () {
        return 'this is computed prop'
      }
    }
  });

  console.log(vm.prop1); // 'this is data prop'

你的问题是prop1始终是data的属性,而非计算属性。

因为Vue在初始化computed的时候做了限制:
source: https://github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L166

从注释上来看,这是为了防止覆盖组件原型链上的计算属性
但是同时也阻止了computed覆盖data的代理属性,是吗?

你那个精简后的虽然也是同一个现象,但问题的表现还是有区别的,同一个对象中data和computed重名是完全可以避免的。
但我想说的是,写拓展的和使用拓展的可能不是同一个人。
那么data和computed的冲突就会是个问题了,就好像服务器语言中的继承和重写一样。
当一个拓展的实例希望江拓展的data改为computed时,他会发现完全没有任何办法。
另一种情况是,当一个拓展的实例希望在初始化时给某个属性(使用者并不关心这个属性是data还是computed)赋值时,他却只能通过vm.xxx=yyy的方式

源码中确实是做了判断,不覆盖data,但是我不明白这样做的用意,在我的理解中,computed和data在使用上没有任何区别,他们本质上应该是相同的,data会被代理为类似computed的存在。
所以这样看来computed的优先级应该高于data。
另外,如果已经在存在一个与data同名的computed,也说明使用者希望由自己控制这个属性的行为,更应该优先使用computed。
以上是我个人在使用中的一些不便和疑惑的地方。

@blqw 还没睡吗 : ) 我想你对计算属性的理解有点模糊

中文文档: https://cn.vuejs.org/v2/guide/computed.html#计算属性

computed存在的意义是为了简化模板的语法,将复杂的计算逻辑留在代码中。

他和data属性的代理意义是不同的

至于为什么data的代理属性优先级高于计算属性。我想作者考虑的是两者的使用频率。

另外,下次提交的时候麻烦使用英语,想清楚问题后用翻译软件翻译过来就是了。会有更多的人解答你的问题

你的原始例子对 computed 和 options 的理解有偏差。data 就是 data,computed 就是 computed,在 options 里面是完全分开的,只有在创建出来的实例上面共用一个 namespace。

原则上来说,data 和 computed 有重复定义是错误的使用方式,e7e86fd 对这种情况添加了警告。

Was this page helpful?
0 / 5 - 0 ratings