资讯专栏INFORMATION COLUMN

浅析Vue响应式原理(二)

rockswang / 2133人阅读

摘要:响应式原理之之前简单介绍了和类的代码和作用,现在来介绍一下类和。对于数组,响应式的实现稍有不同。不存在时,说明不是响应式数据,直接更新。如果对象是响应式的,确保删除能触发更新视图。

Vue响应式原理之Observer

之前简单介绍了Dep和Watcher类的代码和作用,现在来介绍一下Observer类和set/get。在Vue实例后再添加响应式数据时需要借助Vue.set/vm.$set方法,这两个方法内部实际上调用了set方法。而Observer所做的就是将修改反映到视图中。

Observer
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    def(value, "__ob__", this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer有三个属性。value是响应式数据的值;dep是Dep实例,这个Dep实例用于Vue.set/vm.$set中通知依赖更新;vmCount表示把这个数据当成根data对象的实例数量,大于0时是实例化传入的根data对象。

构造函数接受一个值,表示要观察的值,这样,在Observer实例中引用了响应式数据,并将响应式数据的__ob__属性指向自身。如果被观察值是除数组以外的类型,会调用walk方法,令每个属性都是响应式。对于基本类型的值,Object.keys会返回一个空数组,所以在walk内,defineReactive只在对象的属性上执行。如果是被观察值是数组,那么会在每个元素上调用工厂函数observe,使其响应式。

对于数组,响应式的实现稍有不同。回顾一下在教程数组更新检测里的说明,变异方法会触发视图更新。其具体实现就在这里。arrayMethods是一个对象,保存了Vue重写的数组方法,具体重写方式下面再说,现在只需知道这些重写的数组方法除了保持原数组方法的功能外,还能通知依赖数据已更新。augment的用途是令value能够调用在arrayMethods中的方法,实现的方式有两种。第一种是通过原型链实现,在value.__proto__添加这些方法,优先选择这种实现。部分浏览器不支持__proto__,则直接在value上添加这些方法。

最后执行observeArray方法,遍历value,在每个元素上执行observe方法。

数组变异方法的实现

执行变异方法会触发视图功能,所以变异方法要实现的功能,除了包括原来数组方法的功能外,还要有通知依赖数据更新的功能。代码保存在/src/core/observer/array.js

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
]
methodsToPatch.forEach(function (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
  })
})

模块内,使用arrayProto保存数组原型,arrayMethods的原型是arrayProto,用来保存变异后的方法,methodsToPatch是保存变异方法名的数组。

遍历methodsToPatch,根据方法名来获取在arrayProto上的数组变异方法,然后在arrayMethods实现同名方法。

在该同名方法内,首先执行缓存的数组方法original,执行上下文是this,这些方法最终会添加到响应式数组或其原型上,所以被调用时this是数组本身。ob指向this.__ob__,使用inserted指向被插入的元素,调用ob.observeArray观察新增的数组元素。最后执行ob.dep.notify(),通知依赖更新。

observe

工厂函数,获取value上__ob__属性指向的Observer实例,如果需要该属性且未定义时,根据数据创建一个Observer实例,在实例化时会在value上添加__ob__属性。参数二表示传入的value是否是根data对象。只有根数据对象的__ob__.vmCount大于0。

isObject判断value是不是Object类型,实现如obj !== null && typeof obj === "object"

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

此处可以看出,value与Observer实例ob之间是双向引用。value.__ob__指向ob,ob.value指向value

Vue.set

在Vue实例化以后,如果想为其添加新的响应式属性,对于对象,直接使用字面量赋值是没有效果的。由响应式数据的实现可以想到,这种直接赋值的方式,并没有为该属性自定义getter/setter,在获取属性时不会收集依赖,在更新属性时不会触发更新。如果想要为已存在的响应式数据添加新属性,可以使用Vue.set/vm.$set方法,但要注意,不能在data上添加新属性。

Vue.set/vm.$set内部都是在/src/code/observer/index.js定义的set的函数。

set函数接受三个参数,参数一target表示要新增属性的对象,参数二key表示新增的属性名或索引,参数三val表示新增属性的初始值。

export function set (target: Array | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== "production" &&
    !Array.isArray(target) &&
    !isObject(target)
  ) {
    warn(`Cannot set reactive property on non-object/array value: ${target}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }

  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    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
  }
  // 不存在ob 说明不是响应式数据
  if (!ob) {
    target[key] = val
    return val
  }
  // 为target添加新属性
  defineReactive(ob.value, key, val)
  // ob.dep实际是target.__ob__.dep
  ob.dep.notify()
  return val
}

函数内部首先判断target类型,非数组或非对象的目标数据是无法添加响应式数据的。

如果是数组,且key是有效的数组索引,更新数组长度,然后调用变异方法splice,更新对应的值并触发视图更新。如果是对象,且属性keytarget的原型链上且不在Object.prototype上(即不是Object原型上定义的属性或方法),直接在target上添加或更新key

ob指向target.__ob__,如果target是Vue实例或是根data对象(ob.vmCount > 0),则无法新增数据,直接返回。

接着处理能为target添加属性的情况。不存在ob时,说明不是响应式数据,直接更新target。否则,执行defineReactive函数为ob.value新增响应式属性,ob.value实际指向target,添加之后调用ob.dep.notify()通知观察者重新求值,ob是Observer实例。

总结一下,set的内部逻辑:

target是数组时,更新长度,调用变异方法splice插入新元素即可。

target是对象时:

key在除Object.prototype外的原型链上时,直接赋值

key在原型链上搜索不到时,需要新增属性。如果target__ob__属性,说明不是响应式数据,直接赋值。否则调用defineReactive(ob.value, key, val)观察新数据,同时触发依赖。

Vue.delete
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。

Vue.delete实际指向deldel接受两个参数,参数一target表示要删除属性的对象,参数二key表示要删除的属性名。

如果target是数组且key对于的索引在target中存在,使用变异方法splice方法直接删除。

如果target是Vue实例或是根data对象则返回,不允许在其上删除属性。key不是实例自身属性时也返回,不允许删除。如果是自身属性则使用delete删除,接着判断是否有__ob__属性,如果有,说明是响应式数据,执行__ob__.dep.notify通知视图更新。

export function del (target: Array | Object, key: any) {
  if (process.env.NODE_ENV !== "production" &&
    !Array.isArray(target) &&
    !isObject(target)
  ) {
    warn(`Cannot delete reactive property on non-object/array value: ${target}`)
  }
  // 数组 直接删除元素
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" && warn(
      "Avoid deleting properties on a Vue instance or its root $data " +
      "- just set it to null."
    )
    return
  }
  // 属性不在target上
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  // 不是响应式数据
  if (!ob) {
    return
  }
  ob.dep.notify()
}
小结

关于Observer类和set/get的源码已经做了简单的分析,细心的读者可能会有一个问题:target.__ob__.dep是什么时候收集依赖的。答案就在defineReactive的源码中,其收集操作同样在响应式数据的getter中执行。

至于defineReactive的源码解析,在后面的文章再做分析。

参考链接

Vue技术内幕|揭开数据响应系统的面纱

Vue源码

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

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

相关文章

  • 浅析Vue响应原理(三)

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

    tomener 评论0 收藏0
  • 从数组入手浅析Vue响应原理

    摘要:响应式原理为了探究这一切的原因,我再次点开了的官网。在官网很下面的位置,找到了关于响应式原理的说明。因此,新添加到数组中的对象中的属性,就成了非响应式的属性了,改变它自然不会让组件重新渲染。响应式属性的对象,有这个对象就代表是响应式的。   最近在用Vue开发一个后台管理的demo,有一个非常常规的需求。然而这个常规的需求中,包含了大量的知识点。有一个产品表格,用来显示不同产品的信息。...

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

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

    lookSomeone 评论0 收藏0
  • 前方来报,八月最新资讯--关于vue2&3的最佳文章推荐

    摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...

    izhuhaodev 评论0 收藏0
  • 浅析RWD

    摘要:三响应式网页设计的基本原理标签,允许页面宽度自动调整大多数移动浏览器将页面放大为宽的视图以符合屏幕分辨率。解决方案使用,选择器清除浮动,只适用于非浏览器。由于移动设备屏幕大小的限制,在其上进行显示的内容一般是经过精心筛选的。 一、前言 现今,无论是移动设备、平板电脑、PC,屏幕大小各不相同,若是针对每个屏幕大小单独设计一个解决方案,则会大幅增加网站建设的复杂程度和运营成本。响应式网页设...

    0x584a 评论0 收藏0

发表评论

0条评论

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