前言

如果对Vue的数据改变如何在视图上响应还没有深入研究过,那不妨看看这篇文章,这篇文章会结合项目中的案例来解读Vue数据响应式原理。

Vue响应式思想

在new Vue时会初始化data中的变量,在Vue2.0+中会通过defineProperty给data中的变量逐一设置getter/setter方法,将data中的变量设置成响应式。Vue中只有通过defineProperty设置了getter/setter方法的对象属性被赋值时才能触发视图更新,Vue对数组中的方法进行了额外处理,思想就是基于Array的原型对象将其上的属性:push/pop、unshift/shift、splice、sort/reserve方法通过definedProperty设置成响应式,当调用数组中的这些方式时就会触发视图更新回调函数,其中对于push、unshift、splice插入的数组属性, 最终会遍历逐一设置getter/setter方法,这样当修改新添的属性的值时,也能让视图响应。

Vue中视图响应数组值的改变

defineProperty的原理

我们知道,Vue 2.0+中基于defineProperty实现的数据响应式,只有定义在data中的变量才是响应式的;而对于对象的属性的新增和删除,视图不能直接响应改变;除此之外对于[]类型,不能通过修改数组下标来修改、新增属性值。当对象属性通过defineProperty定义之后,在给属性赋值时就能触发属性对应的set方法,从而触发视图更新。下面验证一下defineProperty的使用:

1、给{}类型的属性定义getter/setter方法

function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        enumerable: true, // 可枚举
        configurable: true, // 可写
        get: function () {
          console.log('get');
          return val;
        },
        set: function (newVal) {
          // 设置时,可以添加相应的操作
          console.log('set:', val);
          val += newVal;
        }
      });
    }
    let obj = {
      name: '成龙大哥',
      say: ':其实我之前是拒绝拍这个游戏广告的,',
      books: [{name: 'hh',price: 233}]
    };
    Object.keys(obj).forEach(k => {
      defineReactive(obj, k, obj[k]);
    });
    obj.say = '后来我试玩了一下,哇,好热血,蛮好玩的'; //会触发set
    obj.age = 20; //不会触发set
    obj.books = [{name: 'yyyyyy',price: 23}]; //会触发set
    obj.books[0].name = 'ttttt';
    obj.books.push({name: 'hhhh',price: 66}); //触发get,push未找到

    console.log(obj.name + obj.say, obj);

2、给数组类型的属性值定义getter/setter方法

function defineReactive(obj, key, val) {
       Object.defineProperty(obj, key, {
           enumerable: true, // 可枚举
           configurable: true, // 可写
           get: function() {
               console.log('get');
               return val;
           },
           set: function(newVal) {
               // 设置时,可以添加相应的操作
               console.log('set:', val);
               val += newVal;
           }
       });
   }
  
   let arr = [1,2,3,4,5];
   arr.forEach((v, i) => { 
       defineReactive(arr, i, v);
   });
   arr[0] = 'oh nanana'; // 触发set
   arr.push(6) // 不会触发set
   console.log(arr)
   
   let arr2 = [{name: '1'},{name: '2'}, {name: '3'}];
   arr2.forEach((v, i) => { 
       defineReactive(arr2, i, v);
   });
   arr2.forEach((v, i) => { 
       v.status= true // 不会触发set
   });
   arr2.forEach((v, i) => { 
       v.name = true // 不会触发set
   });
   
   arr2[0] = {name: 2} // 会触发set
   arr2.splice(1,1,{name: 111}) // 会触发set
   arr2.push({name: 'tt'}) // 不会触发set
   arr2.length = 2 // 不会触发set

我们得出什么结论?

defineProperty不能检测到的

1、对象的属性的新增和删除,新增属性也可以理解为没有通过defineProperty定义setter/getter的新增的对象属性;

2、数组通过push/pop/shift/unshift/sort等方法修改值,length修改长度。

defineProperty能检测到的

1、定义了setter/getter的对象属性的值修改,包括数组中下标定义了setter/getter的情况。

那么问题来了,Vue基于defineProperty实现的数据响应,但为什么通过下标修改的数组值视图不能响应,通过push/pop/shift/unshift/sort修改的数组值可以响应呢?Vue中具体是如何实现的基于数组值的变化,视图随之响应的呢?

Vue中对数组变量的Hack

index.js

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key)
    
    if (property && property.configurable === false) {
      return
    }
    // cater for pre-defined getter/setters
    const getter = property && property.get
    const setter = property && property.set
    let childOb = !shallow && observe(val)
    
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
      const 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) {
        const value = getter ? getter.call(obj) : val
       
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
       
        if (process.env.NODE_ENV !== 'production' && customSetter) {
          customSetter()
        }
      
        if (setter) {
          setter.call(obj, newVal)
        } else {
          val = newVal
        }
        childOb = !shallow && observe(newVal)
        dep.notify()
    }
  })
}

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

可以看到在Vue中并没有对数组类型的变量进行遍历,将数组中的属性设置setter/getter方法,所以Vue中无法根据数组下标修改值来让视图响应。(为什么不提供数组下标修改值让视图响应的功能可以参考:https://segmentfault.com/a/1190000015783546) 

Vue不能根据数组下标修改值来让视图响应,却对push/pop/shift/unshift/sort等方法做了hack,通过这些方法修改数组可以让视图响应。

Vue中对数组的hack:

对于push、unshift方法新增的数组属性,splice插入的值, 最终会遍历逐一设置getter/setter方法,这样当修改新添的属性的值时,也能让视图响应。最后会调用notify()触发视图更新。

array.js

import { def } from  '../util/index'
const  arrayProto =  Array.prototype
export const  arrayMethods =  Object.create(arrayProto)

[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
  // cache original method
  const  original =  arrayProto[method]

  def(arrayMethods,  method, function  mutator (...args) {

    const  result =  original.apply(this,  args)
    const  ob = this._ob
    let  inserted

    switch (method) {
      case  'push':
      case  'unshift':
         inserted =  args
      break
      case  'splice':
         inserted =  args.slice(2)
      break
    }

    if (inserted)  ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return  result
  })
})

factory.js

function  def (obj,  key,  val,  enumerable) {
  
  Object.defineProperty(obj,  key, {
    value:  val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

Observer.prototype.observeArray = function  observeArray (items) {
  
    for (var  i = 0,  l =  items.length;  i <  l;  i++) {
      observe(items[i]);
    }
};

var  Observer = function  Observer (value) {
  
    this.value =  value;
    this.dep = new  Dep();
    this.vmCount = 0;
    def(value, '_ob', this);

    if (Array.isArray(value)) {
        var  augment =  hasProto
        ?  protoAugment
        :  copyAugment;
        augment(value,  arrayMethods,  arrayKeys);
        this.observeArray(value);
    } else {
       this.walk(value);
    }
};

  Observer.prototype.walk = function  walk (obj) {
    
      var  keys =  Object.keys(obj);
      for (var  i = 0;  i <  keys.length;  i++) {
         defineReactive$$1(obj,  keys[i],  obj[keys[i]]);
      }
};
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

function  defineReactive$$1 (
  obj,
  key,
  val,
  customSetter,
  shallow
) {

  var  dep = new  Dep();
  var  property =  Object.getOwnPropertyDescriptor(obj,  key);

  if (property &&  property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var  getter =  property &&  property.get;
  var  setter =  property &&  property.set;
  var  childOb = !shallow && observe(val);

  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();
  }

  });

}

总结

1、只有定义了setter/getter的对象属性,在修改值时才能触发视图更新;

2、Vue中提供$set(Obj, Key, Value)原理就是将对象中新增的属性设置setter/getter方法,然后赋值;所以才可以做到新增的属性值,视图能响应到;

3、Vue对数组中的方法做了hack,以下方法对数组做改变,视图可以响应到变化:

 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'

案例分析

先看看下面这个案例,其中的编辑模块应用到的数据结构包含了一个二维数组,逻辑包含了对一维数组、二维数组的属性的值添加、删除、修改功能。利用到的Vue的核心技术点主要是数组值改变如何触发视图更新。

image.png

image.png

编辑页的数据结构:

  data: {
        OperatorId: 18,
        is_auto: true,
        cycle: {
            end: `2019-04-04 00:00:00`,
            start: `2019-03-27 00:00:00`
        },
        enroll: {
            end: `2019-03-26 21:00:00`,
            start: `2019-03-25 23:52:01`
        },
        switch_type: {
            name: "按人数切期or按时间切期",
            value: 0
        },
        assigned: 140,
        max_assign: 1246,
        rebind_assign: 5,
        mode: "daily",
        isBeforeEnroll: "true",
        operators: [
          {
                assistants: [{
                    enable: true,
                    assigned: 23,
                    rebind_assign: 2,
                    max_assign: 300,
                    id: 45,
                    name: "酱酱4",
                    is_abnormal: false,
                    abnormal_assign: [{
                            open_time: "05-03 20:15:11",
                            close_time: "05-03 20:15:11",
                            count: 5
                        },
                        {
                            open_time: "05-04 20:15:11",
                            close_time: "现在",
                            count: 20
                        }
                    ]
                }],
                assistants_type: 1,
                avatar: "",
                id: 18,
                name: "骆美姗",
                is_leave: true,
                leave_day_text: "5天",
                leave_date: "05/15(D1)-05-20(D5)"
           },
            
        ],
        state: {
            enroll_doing: "false",
            enroll_over: "true"
        },
        term: 153
    },
    use_memory_usage: `2.39 mb`,
    assign_memory_usage: `2.38 mb`,
    real_memory_usage: "2 mb"
};

最主要是对operators、assistants进行添加、修改、删除,涉及到数组的嵌套,如果对Vue响应式原理了解不够透彻就很容易遇到修改了数组的值,但是视图却不更新的问题。那需要注意些什么?下面拿这个案例举个例子:

对最外层的数组operators新增一个operator属性时,可以通过this.$set来添加也可以push/unshift,但是最好用在这里就用this.$set,因为添加的operator属性值是个对象,还有对这个对象进行修改的需求, 在要想在修改值时能够正常在视图更新,对应的变量必须是设置了getter/setter方法的响应式变量,在创建的时候operator时就应该将其设置成响应式变量:

     addOperator() {
           //延缓状态且招生结束
           ...
           const arr = this.getInterTutorIndexArr
           let index = 0
           if (arr.length != 0) {
               index = arr[arr.length - 1] + 1
           }
           const operator = this.factoryOperator(index)
           this.$set(this.operatorsArr, this.operatorsArr.length, operator)
           ...
       },

而不是在修改operator的时候用$set,这里其实就可以直接用数组下标的方式来更新operator的值:

   changeOperator(
          operator,
          operatorIndex,
          is_leave,
          leave_date,
          leave_day_text,
      ) {
          ...
          let obj = Object.assign({}, operator, {
              is_leave: is_leave,
              leave_date: leave_date,
              leave_day_text: leave_day_text,
          })
           //this.operatorsAr[operatorIndex] = obj
          this.$set(this.operatorsArr, operatorIndex, obj)
         ...
      },

而对于删除一个operator:

    delOperator(operator, index) {
          //延缓状态且招生结束
          ...
          this.operatorsArr.splice(index, 1)
          this.updateOperators(this.operatorsArr)
          ...
      },
      
      updateOperators(newOperators) {
          ...
          //operatorsArr是data中的变量,是响应式的,直接赋值新值可以让视图响应
          this.operatorsArr = JSON.parse(JSON.stringify(newOperators)) 
      },  

assistants的更新类推...


贝er
58 声望6 粉丝

不仅仅是程序员