AnnVoV / blog

24 stars 2 forks source link

vue 中使用Object.assign遇到的问题 #10

Open AnnVoV opened 6 years ago

AnnVoV commented 6 years ago

前言

之前一直没有很注意vue源码中的proxy方法,最近遇到一个问题,发现了一些忽略的细节。背景如下:

背景

一般我们都会在mounted钩子里面去写一些通过异步接口获取数据的方法,比如下面,发现Object.assign 下面两种写法结果不一样

  <el-form :model="form" label-width="130px">
    <el-form-item>
      <el-input :model="form.title">
    </el-form-item>
  </el-form>
export default {
  data() {
    return {
      form: {}
    }
  },
  mounted() {
    Ajax.get(url)
      .then((data) => {
        // 这样title是双向绑定的
        this.form = data;
        // 这样title也是双向绑定的
        this.form = Object.assign({}, this.form, data)
        // 这样title并不是双向绑定的
        this.form = Object.assign(this.form, data)
      })
  }
}

原因

因为在set里面有这样一个判断

Object.defineProperty(obj, key, {
  enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      // 重点注意这里
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
})

  因为Object.assign(this.form, newData)是在原对象上修改的新的值,所以this.form 在被修改值的时候,会进入其setter,且此时newVal === value 所以会return 也就是新的值没有进入observe(newVal)方法   而Object.assign({}, this.form, newData) 相当于创建了一个新对象,此时newVal === value 就不成立了,所以会进入observe(newVal)的方法

fxxjdedd commented 6 years ago

有个疑问, Object.assign(this.form, newData) 的情况下,此时newVal是新值,而value是在getter.call(obj)得到的,应该是旧值,因为set还没完成。此时,如果新值不等于旧值,那就不会return啊?

fxxjdedd commented 6 years ago

而且,根据assgin的实现 Object.assign({}, this.form, newData) 其实是分两步的,第一步合并{}和this.form,第二部合并第一步的结果和newData,按照这个思路的话,其实和 Object.assign(this.form, newData) 是没区别的啊,但事实是有区别的,我到底哪里想错了呢?

fxxjdedd commented 6 years ago

我懂了,您说的是form这个属性,而不是form中的title属性。对于Object.assign(this.form, newData),this.form的地址并没有变,所以newVal === value不会触发更新,对于Object.assign({}, this.form, newData),产生了新的对象赋值给this.form, 着改变了form的地址,所以newVal !== value可以触发更新。

fxxjdedd commented 6 years ago

http://jsrun.net/YcgKp/edit 总结了一下

AnnVoV commented 6 years ago

是的 是那个意思

ZSkycat commented 6 years ago

=_= 所以即使只是想要修改子属性,就必须创建一个新的对象吗?但是我在自定义的class中,使用 Object.assign 是可以正确调用属性的 set 的

ZSkycat commented 6 years ago
t1 = {
    _a:1,
    set a(value){
        console.log('set a')
        this._a = value;
    },
    get a(){
        console.log('set a')
        return this._a;
    }
}
Object.assign(t1, { a: 1 })

控制台使用,可以看到属性 set 是可以被成功调用的,但是对 vue 响应对象却无效,无法理解。

fxxjdedd commented 6 years ago

vue初始化的时候会对t1进行Observe,把里面现有的属性都变成响应式的,现在你Object.assign(t1, { a: 1 }) 这个a属性是一个新的属性,之前没有被Observe过,所以不会生效。

简化一下问题:

new Vue({
  data() {
    return {
       _a:1
    }
  }
})

解决方案有:

  1. this.t1 = Object.assign({}, t1, {a:1}) assign此时会返回一个新对象,把它赋值给t1,实际上是this.data.t1=Object.assign({}, t1, {a:1}), 因为data同样在初始化的时候Observe过t1属性,而此时t1的引用变了,所以会触发更新

  2. this.$set(this.t1, 'a', 1) 这一步不仅给t1添加了一个a属性,同时还Observe了这个属性

  3. 一开始就让vue去Observe这个a属性

    t1 = {
    _a:1,
        a: 0
    }
ZSkycat commented 6 years ago

😅 发现我看错了,原来工作是正常。我的使用场景正是已经初始化过了,通过 Object.assign 同时修改多个属性而已。