什么是数据响应式

UI的变化受数据的改变而发生改变。在Vue中,data属性是响应式的,通过修改data内的对象数据,在UI上会有与之对应的改变。

如何做到数据响应式

利用Object.defineProperty()方法和代理可以实现这一功能。

先看看使用defineProperty并不用代理时会发生什么?

  1. let obj = {}
  2. let obj._n = 0
  3. Object.defineProperty(obj,'n',{
  4. get(){
  5. return this._n
  6. },
  7. set(value){
  8. if(value < 0) return
  9. this._n = value
  10. }
  11. })

这里要注意一点,为什么要用一个中间变量obj._n而不是就直接操作新定义的obj.n 呢?这是由于通过defineProperty新添加的属性n在我们访问时自动调用getter即get方法,如果返回值是this.n ,那么会因为这句话再次调用getter,从而发生无限递归的错误。

但这也引发了另一个问题,虽然使用obj.n = -1会因为setter限制了传入的数据范围,但这也不影响通过直接修改obj._n实现对obj.n的修改。

此时就可以通过代理来避免被修改的问题:

  1. function proxy({data}){
  2. let value = data.n
  3. // 如果对同名变量使用defineProperty改造,会将原来的自动删除,再新建一个
  4. // 这个defineProperty操作的还是原始对象
  5. Object.defineProperty(data, 'n', {
  6. get(){
  7. return value
  8. },
  9. set(newValue){
  10. if(newValue<0) return
  11. value = newValue
  12. }
  13. })
  14. // 就加了上面几句,这几句话会监听 data
  15. const obj = {}
  16. // 这个defineProperty操作的就是代理对象
  17. Object.defineProperty(obj, 'n', {
  18. get(){
  19. return data.n
  20. },
  21. set(value){
  22. if(newValue<0) return
  23. data.n = value
  24. }
  25. })
  26. return obj // obj 就是代理
  27. }
  28. let mydata = {n:0}
  29. let temp = proxy({data:mydata})
  30. mydata.n = -1
  31. console.log(temp.n)

此时不论再修改mydata的值将不会影响到temp.n的值。

把上面这个例子稍微整合一下和创建Vue实例做一下对比:

  1. const temp = proxy({
  2. data:{
  3. n:0
  4. }
  5. })
  6. const vm = new Vue({
  7. data:{
  8. n:0
  9. }
  10. })

可以发现惊人的相似。Vue也是用的defineProperty+代理的方式来监听数据改变的。上面一个例子我们知道修改mydata将不会影响temp.n的值,Vue也是一样,我们传给Vue的构造选项data将会被Vue实例vm代理,我们操作的数据都是基于vm内的数据,不再是传入的原始数据,这即是vue实现数据响应的原因。

为了证明传入的原始数据不再被vue管理,来看一个例子:

  1. <div id="app">
  2. <span class="spanA">
  3. {{obj.a}}
  4. </span>
  5. <span class="spanB">
  6. {{obj.b}}
  7. </span>
  8. </div>
  1. const app = new Vue({
  2. el:'#app',
  3. data: {
  4. obj: {
  5. a:'a'
  6. }
  7. }
  8. })
  9. app.obj.b = 'b'

b会显示到视图上吗?答案是不会,因为b并没有被vue实例app所管理,要实现b也被管理需要使用vue的set方法(是defineProperty的封装):

  1. Vue.set(app.obj,'b','b')
  2. // 或者
  3. app.$set(app.obj,'b','b')

再来看一个特殊的例子:

  1. const app = new Vue({
  2. el:'#app',
  3. data: {
  4. obj: {
  5. a:'a'
  6. }
  7. }
  8. })
  9. // 改 b 之前同时改一下 a
  10. app.obj.a = 'a1'
  11. app.obj.b = 'b'

此时视图会显示什么内容呢?答案是 a 和 b 都被更新显示了。

按道理obj.b不应该显示出来,用为这个数据一开始并没有被vue监听,但是它为什么会显示出来呢?

要理解为什么 spanB 会更新,要点是理解视图更新其实是异步的。
当我们让 a 从 ‘a’ 变成 ‘a1’ 时,Vue 会监听到这个变化,但是 Vue 并不能马上更新视图,因为 Vue 是使用 Object.defineProperty 这样的方式来监听变化的,监听到变化后会创建一个视图更新任务到任务队列里。

所以在视图更新之前,要先把余下的代码运行完才行,也就是会运行 b = ‘b’。
等到视图更新的时候,由于 Vue 会去做 diff,于是 Vue 就会发现 a 和 b 都变了,自然会去更新 spanA 和 spanB。

除了set方法对普通属性值加入监听外,vue还继承了原生数组并重写了一些常用的数组方法以实现对数组的动态监听。

总结:数据响应式原理是让数据得到监管,且不让他人随意操作原始数据并更改到目标内容,如果用普通函数来实现数据到UI的更新,由于传入函数的是一个对象的引用,那么这个函数会轻易收到外界操作的干扰,通过代理的方式可以避免这种情况的发生。