Skip to content

第 51 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty? #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
zeroone001 opened this issue Apr 9, 2019 · 11 comments
Labels

Comments

@zeroone001
Copy link

  1. Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  2. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  3. Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
@Moriarty02
Copy link

Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

而要取代它的Proxy有以下两个优点;

可以劫持整个对象,并返回一个新对象
有13种劫持操作

摒弃Object.defineProperty,基于 Proxy 的观察者机制探索

@noctiomg
Copy link

楼上讲的很细致啦!有一点我补充一下:

Object.defineProperty本身有一定的监控到数组下标变化的能力:
Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。具体我们可以参考 《记一次思否问答的问题思考:Vue为什么不能检测数组变动》这篇文章,文章底部配图中有尤大大的严肃回复截图; 下方的讨论区也很值得大家下去看一看,有对于 for / forEach / for .. in .. 几个循环方式的讨论。

关于 Vue 3.0 的其他信息我们可以参考 尤大大发布的 Vue 3.0 新特性预览PPT

另外补充一些其他资料给大家:

@yft
Copy link

yft commented Aug 10, 2019

感觉 Proxy 行为有些复杂,一个操作会引起很多中间操作出现,这部分有哪位盆友讲解一下或者指个路,介绍一下Vue是怎样处理这些干扰信息的吗?

let a = [1];

let newA = new Proxy(a, {
    get(target, p, receiver) {
        console.log('get', p);
        return Reflect.get(target, p, receiver);
    },
    set(target, p, value, receiver) {
        console.log('set', p);
        return Reflect.set(target, p, value, receiver);
    }
});

newA.push(1);
console.log('===');
newA[2] = 2;
newA.length;
console.log('===');
newA.length = 100;
console.log('===');
newA.shift();

输出

get push
get length
set 1
set length
===
set 2
get length
===
set length
===
get shift
get length
get 0
get 1
set 0
get 2
set 1
set length

@aeolusheath
Copy link

1, 数据的变化是通过getter/setter来追踪的。因为这种追踪方式,有些语法中,即便是数据发生了变化,vue也检查不到。比如 向Object添加属性/ 删除Object的属性。

2,检测数组的变化,因为只是拦截了unshift shift push pop splice sort reverse 这几个方法,所以像

list[0] = 4
list.length = 0

检测不到

@zhl1232
Copy link

zhl1232 commented Oct 11, 2019

之前正好写过个类似的东西,大概就是 Object.defineProperty 的监听只会根据第二个参数的 prop 来判断,甚至内存指针指向同一数据都不会监听,而且不会往下递归。

let a = {
  b:{c1:1,c2:2},
}

Object.defineProperty(a, 'b', {
  get: function() {
    console.log(a.b);
  }
})
// Maximum call stack size exceeded
// 循环调用,肯定是爆栈了
let a = {
  b:{c1:1,c2:2},
}

let temp = a.b

Object.defineProperty(a, 'b', {
  get: function() {
    return temp
  }
})
temp.c1 = 99
console.log(a.b, temp);  // {c1: 99, c2: 2} {c1: 99, c2: 2}
// 两个对象指针还是指向同一个数据,但是存取描述符只会根据 obj 和 prop 来判断
let a = {
  b:{c1:1,c2:2},
}

let temp = a.b

Object.defineProperty(a, 'b', {
  get: function() {
    return temp
  },
  set: function(newVal) {
    console.log(newVal);
  }
})
temp.c1 = 99  // 并没有调用 setter

console.log(a.b, temp);  // {c1: 99, c2: 2} {c1: 99, c2: 2}
// 还是说明存取描述符只会根据 obj 和 prop 来判断

知道了存取描述符只会根据 obj 和 prop 来判断,那监听数据就可以这样写

void (function() {
  function watch(obj, name) {
    let value = obj[name]

    Object.defineProperty(obj, name, {
      get: function() {
        return value
      },
      set: function(newVal) {
        value = newVal
      }
    })
    value ? (obj[name] = value) : ''   // 拿到外面处理,不然会循环 setter
  }

  return (this.watch = watch)
})()

let a = { b: 11 }

watch(a, 'b')

console.log(a.b)   // 11
a.b = 12
console.log(a.b)   // 12

需要做什么处理,在 watch 里再加个回调函数就可以了。

在 es6 还提供了一种新的监控方式。
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,Proxy 可以理解成,在目标对象之前架设一层“拦截”。

let a = {
  b:{c1:1,c2:2},
}
let proxy = new Proxy(a, {
  get: function(obj, prop) {
    console.log(obj, prop);  // {b:{c1:1,c2:2}} , 'b'
    return obj[prop]
  }
})

console.log(proxy.b.c1);  // 1


let p2 = proxy.b
console.log(p2.c1);   // 1

可以发现,proxy 的监控方式是往下递归的,不只 prop,prop 下面的对象也可以触发。
而且因为是需要要 Proxy 对象来触发,所以拷贝 Proxy 对象也没影响。

@julyL
Copy link

julyL commented Oct 15, 2019

@zhl1232 proxy并不是递归的,你的代码中执行proxy.b.c1只触发了 proxy对象的get,但并没有触发proxy.b的get

@zhl1232
Copy link

zhl1232 commented Oct 15, 2019

@julyL 确实,这块和 defineProperty 的表现是一致的,理解错误,感谢大佬

@yygmind yygmind added the Vue label Dec 16, 2019
@Murphycx94
Copy link

Object.defineProperty可以监听到数组下标的变化的
只是常规的使用数组时,很少会用下标去操作,而使用api更符合我们的编码习惯

@bosens-China
Copy link

前面补充的挺详细的了,这里就补充一下数组哪方面不能监控

  • 不能监听动态length的变化
    例如,arr = [1],直接更改arr[10] = 20,这样就监听不到了,因为length在规范不允许重写,而arr[0]直接更改是可以监听到的。
  • 数组方法使用不能监听到数组的变更,例如push
    这也是为什么vue重写这些方法的原因

@Rabbitzzc
Copy link

这里其实提到了很多,简单来说,就是 length 尽量不能去改写。

  1. length 在规范中不允许改写,configurable = false
  2. a.length = 100,等于增加了 100 个属性,需要对每个属性进行监听,这样一来,性能上所有问题,使用 push 或者 pop 等重写方法更加简单

@wujianping334455
Copy link

  1. Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  2. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  3. Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

Object.defineProperty是通过遍历整个对象来劫持的,所以原本数组的监听也需要遍历才能劫持,但是数组劫持遇到类似let list = new Array(1000000)的情况,性能开销太大。所以vue源码才会重写数组的方法和监听length来进行劫持。
另外vue3 proxy用的是懒劫持,不是一上来就递归遍历整个对象,只是兼容性方面差点。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests