资讯专栏INFORMATION COLUMN

从数组入手浅析Vue响应式原理

dkzwm / 1167人阅读

摘要:响应式原理为了探究这一切的原因,我再次点开了的官网。在官网很下面的位置,找到了关于响应式原理的说明。因此,新添加到数组中的对象中的属性,就成了非响应式的属性了,改变它自然不会让组件重新渲染。响应式属性的对象,有这个对象就代表是响应式的。

  最近在用Vue开发一个后台管理的demo,有一个非常常规的需求。然而这个常规的需求中,包含了大量的知识点。有一个产品表格,用来显示不同产品的信息。然后表格要有一个内嵌编辑的功能,点击操作栏的编辑按钮,对应行的信息列就变成输入框。第一版的代码大致上像这样。



  逻辑很简单,我在表格数据数组中,给每一个对象都加入一个初始值为false的属性"edit",然后根据这个属性的值,使用v-show来决定渲染的是文本还是输入框,是“编辑”还是“保存”。
  然而运行起来之后的表现并不是像我想的一样,事实上,点击编辑按钮后,对应产品的“产品描述”并没有变成输入框,编辑按钮也没有变成保存按钮。而我通过vue-devtool查看数据发现,事实上对应的edit属性确实已经变了,只是页面上的组件没有正确渲染。这让我很困惑,说好的双向绑定呢,为什么model层上的变化没有响应到view层上呢。

原因分析

  首先,由于页面初始显示是正确的,把edit的初始值改成true后,也会有输入框出现,所以肯定不是代码逻辑的问题。当我试着把v-show的判断条件改成数组中的对象原本就有的属性时,发现编辑状态的切换突然变得正常了。而一旦我把判断条件改回后来插入的edit时,一切又变得不正常了。因此我推测,一定是数据绑定出了什么问题。
  我在网上查了一下,有些类似的问题,大多数的解决方案是,el-table加上一个随机数key值:key="Math.random()"。试了一下,发现真的有用。之所以有用是因为,每次对这个表格有操作,key值都会变,这就相当于产生了一个新的table,浏览器就会根据model层的数据重新渲染,这时候显示当然就正确了。但可想而知,这样也会造成极大的性能浪费,而且这也没有解决数据绑定的问题。
  我又试着对代码做了一些修改。我把map和赋值操作放到了同一句里面去,代码变成了这样

 this.$store.dispatch(GET_PRODUCTS).then(() => {
       this.products = this.$store.getters.products.map((item: any) => {
         item.edit = false;
         return item;
       });
    });

神奇的事发生了,居然一切都恢复正常了。那么我就知道了,问题出在了数组和map函数上。

响应式原理

  为了探究这一切的原因,我再次点开了Vue的官网。在官网很下面的位置,找到了关于响应式原理的说明。这张图很好地说明了Vue实现双向绑定的原理。

  当一个javscript对象传入Vue实例的data中时,Vue会遍历该对象的所有属性,同时使用 Object.defineProperty方法将这些属性全都转成 getter/setter每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的数据发生变化,也就是setter触发时,会通知watcher,从而使它关联的组件重新渲染。
  而由于javascript的限制,Vue不能检测到对象的添加或者删除。并且Vue在初始化实例时就对属性执行了setter/getter转化过程,所以属性必须开始就在对象上,这样才能让Vue转化它。而动态添加的根级别的属性,则不会转化成响应式的属性。也就是说,往已经创建的实例上添加的根级别的属性,都会是非响应式的。但是,可以使用 Vue.set(object, propertyName, value) 或者vm.$set(object, propertyName, value)方法向嵌套对象添加响应式属性。
  这里,数组相关的注意事项被额外提了出来。由于 JavaScript 的限制,Vue 不能检测以下数组的变动:

当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

当你修改数组的长度时,例如:vm.items.length = newLength

  解决方法也很简单,使用上面提到的set方法就可以解决这个问题。与此同时,官网上还有一段专门针对数组的变异方法的说明。
  所谓的变异方法,顾名思义,会改变调用了这些方法的原始数组。相比之下,也有非变异 (non-mutating method) 方法,例如 filter()、concat() 和 slice() 。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。并且,Vue还非常智能的会对于没有变化的dom进行重用,并不会整个进行更新。
  看到这儿,我终于找到问题的关键在哪儿了。其实网上的各种说法都不准确,真正出问题的点在于map函数的使用上。map是一个非变异方法,方法本身并不会改变原数组,而是会返回一个新数组。因此,Vue并没有对map方法进行包装,而是建议替换原数组。然而我在用的时候并没有注意到这一点,在使用的时候利用指针特性,把map方法当做变异方法来用,直接改变原数组,这自然就不会被Vue检测到了。因此,新添加到数组中的对象中的edit属性,就成了非响应式的属性了,改变它自然不会让组件重新渲染。

解决方法

原理都已经搞清楚了,接下来我总结了一下这类数组问题的几种解决方法。

1.添加随机数key(不建议)

  在el-table标签上添加:key="Math.random()",不管做了什么,都强制刷新整个表格,非常不推荐,极大的性能消耗。

2.正确使用数组方法

  在使用数组方法的时候,分清变异方法和非变异方法,用非变异方法的时候,要用新数组替代旧数组,而不是直接变换原数组。

3.使用Vue.set方法(建议)

  我在"vue/src/core/observer/index.js"中找到了set方法的源码。我们发现set函数接收三个参数分别为 target、key、val,其中target的值为数组或者对象,这正好和官网给出的调用Vue.set()方法时传入的参数参数对应上。然后往下看实现,我基本上给每一行都加上了注释。

export function set (target: Array | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== "production" &&
    (isUndef(target) || isPrimitive(target))
  ) {//判断target的类型是否符合要求,若不符合要求,且不在生产环境下,就抛出警告。
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {//如果target是数组,且key值合法
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)//用包装好的变异方法splice进行赋值。
    return val
  }
  if (key in target && !(key in Object.prototype)) {//如果key是target中原有的属性,就直接赋值。
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__//响应式属性的observer对象,有这个对象就代表是响应式的。
  if (target._isVue || (ob && ob.vmCount)) {//如果当前的target对象是vue实例对象或者是根数据对象,就抛出警告。
    process.env.NODE_ENV !== "production" && warn(
      "Avoid adding reactive properties to a Vue instance or its root $data " +
      "at runtime - declare it upfront in the data option."
    )
    return val
  }
  if (!ob) {//如果不存在observer,那就不是响应式对象,直接赋值。
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)//给新属性添加依赖,以后直接修改属性就能重新渲染。
  ob.dep.notify()//直接触发依赖。
  return val
}

可以看到,set方法对于数组的处理其实非常简单,就是调用了包装好的splice方法。那么再来看一下包装Array变异方法的代码实现,我同样给每一行加上了注释。其实做的事情也不多,主要就是给每个新添加的元素都加上观察者。

...
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]//保存原方法。
  def(arrayMethods, method, function mutator (...args) {//修改方法映射,调用数组方法的时候实际上调用的是对应的mutator方法。
    const result = original.apply(this, args)//调用原方法,先把结果求出来
    const ob = this.__ob__//获取observer
    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
  })
})

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105707.html

相关文章

  • 浅析Vue响应原理(三)

    摘要:响应式原理之不论如何,最终响应式数据都要通过来实现,实际要借助新增的。在函数内,首先实例化一个实例,会在稍后添加为响应式数据自定义的中发挥作用。只有数组和对象才可能是响应式,才能返回实例。参考链接技术内幕揭开数据响应系统的面纱源码 Vue响应式原理之defineReactive defineReactive 不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助...

    tomener 评论0 收藏0
  • 浅析Vue响应原理(二)

    摘要:响应式原理之之前简单介绍了和类的代码和作用,现在来介绍一下类和。对于数组,响应式的实现稍有不同。不存在时,说明不是响应式数据,直接更新。如果对象是响应式的,确保删除能触发更新视图。 Vue响应式原理之Observer 之前简单介绍了Dep和Watcher类的代码和作用,现在来介绍一下Observer类和set/get。在Vue实例后再添加响应式数据时需要借助Vue.set/vm.$se...

    rockswang 评论0 收藏0
  • 浅析Vue响应原理(一)

    摘要:浅析响应式原理一的特点之一是响应式,视图随着数据的更新而更新,在视图中修改数据后实例中的数据也会同步更新。对于每个响应式数据,会有两个实例,第一个是在中的闭包遍历,用途显而易见。接收一个回调函数,会在重新求值且值更新后执行。 浅析Vue响应式原理(一) Vue的特点之一是响应式,视图随着数据的更新而更新,在视图中修改数据后Vue实例中的数据也会同步更新。内部借助依赖(下文中的Dep类)...

    lookSomeone 评论0 收藏0
  • [Vue.js进阶]源码角度剖析计算属性的原理

    摘要:前言最近在学习计算属性的源码,发现和普通的响应式变量内部的实现还有一些不同,特地写了这篇博客,记录下自己学习的成果文中的源码截图只保留核心逻辑完整源码地址可能需要了解一些响应式的原理版本计算属性的概念一般的计算属性值是一个函数,这个函数showImg(https://user-gold-cdn.xitu.io/2019/5/6/16a8b98f1361f6f6); 前言 最近在学习Vue计...

    melody_lql 评论0 收藏0
  • Vue.nextTick浅析

    摘要:浅析的特点之一就是响应式,但数据更新时,并不会立即更新。尽管已经更新,但新增的元素并不立即插入到中。实际在中,执行了,这也是自动绑定到执行上下文的原因。在内,使用数组保存回调函数,表示当前状态,使用函数来执行回调队列。 Vue.nextTick 浅析 Vue 的特点之一就是响应式,但数据更新时,DOM 并不会立即更新。当我们有一个业务场景,需要在 DOM 更新之后再执行一段代码时,可以...

    MartinDai 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<