Vue 源码剖析 —— 对象变化侦测

栏目: JavaScript · 发布时间: 4年前

内容简介:当应用在运行时,内部状态是会不断变化的。而对于 web 应用而言这会直接导致页面不停的重新渲染。那么如何通过状态变化确定具体要重新渲染哪个部分呢?在 MVVM 框架出现之前,大多数时候都需要手动去创建并维护数据与显示层的联系,随着应用的复杂度提高,内部状态和 UI 的联系变得错综复杂,难以维护。前端 MVVM 的框架正是通过编写一个通用的Vue.js 的变化侦测与 React 不同,对 React而言,当状态发生变化时,它并不知道具体哪个状态变化了,只知道状态有可能变了,然后发送信号给框架,框架内部收到信号

当应用在运行时,内部状态是会不断变化的。而对于 web 应用而言这会直接导致页面不停的重新渲染。那么如何通过状态变化确定具体要重新渲染哪个部分呢?在 MVVM 框架出现之前,大多数时候都需要手动去创建并维护数据与显示层的联系,随着应用的复杂度提高,内部状态和 UI 的联系变得错综复杂,难以维护。前端 MVVM 的框架正是通过编写一个通用的 ViewModel 层,负责让 Model 层的变化自动同步到 View 层,还负责让 View 层的修改同步回 Model 。今天我们一起来剖析一下,当应用的内部状态改变时,Vue.js 是怎么做到侦测到变化的。

Vue.js 的变化侦测与 React 不同,对 React而言,当状态发生变化时,它并不知道具体哪个状态变化了,只知道状态有可能变了,然后发送信号给框架,框架内部收到信号后,会进行暴力比对找出来那些DOM节点需要重新渲染。对 Vue.js 而言,当状态发生改变时,它立刻就知道了,而且一定程度上知道具体哪些状态改变了,且如果一个状态上绑定了多个依赖,当状态改变时,会向所有绑定的依赖发送通知。但是,这种粒度更细也是要付出一定代价的,每个状态绑定的依赖越多,依赖跟踪在内存上的消耗就更大,这种情况在 Vue.js 2.0 引入虚拟 DOM 之后改善了很多。

问题一:如何侦听一个对象的变化

在 JS 中,我们侦听对象变化的手段无非两种: Object.defineProperty 和 ES6 的 Proxy 。由于 ES6 的支持情况不理想,Vue.js 2.0 中采用的是第一种方法,但在新版本中应该会放弃 Object.defineProperty 选择 Proxy 。因为 Object.defineProperty 是存在明显缺陷的,后文会提到。首先我们可以采用下面的函数来封装 Object.defineProperty

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      return val
    },
    set: function(newVal) {
      if (val === newVal) return
      val = newVal
    }
  })
}
复制代码

此时,思考一下,要观察数据的真正目的是什么?

目的就是当数据变化时,可以通知那些曾经使用过该数据的地方。所以我们需要先收集依赖,这样当数据变化时在去通知这些依赖。显而易见,可以在 getter 中收集依赖,在 setter 中触发依赖。

问题二:依赖收集在哪里

首先我们可以封装一个通用的依赖类,在 Vue.js 中是 Dep 类:

class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (somethingToWatch) {
      this.addSub(somethingToWatch)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let sub of subs) {
      sub.update()
    }
  }
}

function remove (arr, items) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
复制代码

接着改造一下 defineReactive

function defineReactive(data, key, val) {
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend()
      return val
    },
    set: function(newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify()
    }
  })
}
复制代码

问题三:依赖是谁

在上面的 Dep 类中出现了 somethingToWatch ,显然它正是我们在数据变化之后需要通知的对象。在 Vue.js 中,我们通知用到数据的地方有很多,比如模板中,或是自定义的一个 watch 。所以此时需要一个抽象的类来覆盖这些情况,Vue.js 中这个类为 Watcher

class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  get () {
    somethingToWatch = this
    let value = this.getter.call(this.vm, this.vm)
    somethingToWatch = undefined
    return value
  }

  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
复制代码

在这段代码中,当 Watcher 初始化时,会调用 get 方法,而在 get 方法中,我们将 somethingToWatch 指向了当前的 Watcher 实例,当我们在获取 value 值的时候又会触发数据的 getter ,从而自动将 Watcher 实例添加到 Dep 中。当数据变化时, Dep 会触发依赖列表中所有依赖的 update 方法,也就是 Watcher 中的 update 方法, Watcher 中的 update 方法。

可以看一个 vm.$watch('a.b.c', (oldVal, newVal) => {}) 的例子,当 a.b.c 变化时,要调用后面的回调函数。首先,要解析 a.b.c ,在 Vue.js 中用 parsePath 来完成:

const bailRE = /^\w+.$/
function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let segment of segments) {
      if (!obj) return
      obj = obj[segment]
    }
  }
  return obj
}
复制代码

至此,我们就拿到了 a.b.c 这个属性,且在 Watcherget 方法中访问了它,触发了它的 getter ,从而将当前 Watcher 实例添加到 a.b.c 的依赖列表里。且当 a.b.c 发生变化时,回调函数将会在 Watcher 中的 update 方法里被调用。

问题四:怎么侦测所有 key

可以看到使用 Object.defineProperty 可以侦测到对象的某个属性值变化,但是我们需要侦听所有属性值(包括子属性)的变化。现在开始封装 Observer 类来实现这一目的:

/**
 * Observer 类会被附加到每一个被侦测的 object上。
 * 一旦加上,会将 object 所有的属性都转化为 getter/setter 的形式
 * 来收集属性依赖,并且在属性变化时通知这些依赖
 */
class Observer {
  constructor(value) {
    this.value = value

    if (!Array.isArray(value)) {
      this.walk(value)
    }
  }

  /**
   * walk 将每一个属性都转为 getter/setter
   */
  walk (obj) {
    const keys = Object.keys(obj)
    for (let key of keys) {
      defineReactive(obj, key, obj[key])
    }
  }
}

function defineReactive(data, key, val) {
  // 新增,用于递归子属性
  if (typeof val == 'object') {
    new Observer(val)
  }
  ...
}
复制代码

通过定义了 Observer 类,我们将一个正常的 object 转换为了被侦测的 object 。然后判断数据类型,只有 Object 类型的数据才会调用 walk 方法将每一个属性都变为 getter/setter 模式。而改造后的 defineReactive 加上了一段新代码用于判断当子属性为 Object 时,对子属性调用 new Observer(val) ,从而形成递归。这样我们就把所有的属性都变为 getter/setter 的形式了。

问题五: Object.defineProperty 带来的隐藏问题

思考一个场景,当我们在一个 Vue 实例中,定义 data: { a: {} } ,又定义了一个方法 action () { this.a.name = 'jay' } ,如果调用了 action 方法,能不能侦听到对象 a 的改变呢? 答案是否定的,由于在初始化过程中, a 并没有 name 这个属性,也就是说在 walk 方法中,我们没有将 name 属性变为 getter/setter 模式,所以无法侦测到这个变化,也不会向依赖发送通知。

再比如,我们在 action 中删除某个已经存在的属性值, Object.defineProperty 只能判断一个数据是否被修改,故同样也是无法侦测到变化的。要解决这两个问题,我们可以调用 vm. delete 这两个API。

Object 的变化侦测过程梳理

Vue 源码剖析 —— 对象变化侦测

Data 通过 Observe 转换为 getter/setter 形式来追踪变化。当外界通过 Watcher 读取数据是,会触发 getter 从而将 Watcher 添加到依赖中。当数据发生变化时,会触发 setter ,从而向 Dep 中的依赖发送通知。 Watcher 收到通知后,会向外界发送通知,外界收到通知后,可能会触发视图更新,也可能触发用户的回调函数。

本系列文章均是深入浅出 Vue.js的学习笔记,有兴趣的小伙伴可以去看书哈。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

鼠标宣言

鼠标宣言

约翰·里德尔 / 倪萍、梅清豪 / 上海人民 / 2005-08-01 / 25.00

本书针对信息时代营销者不知该如何满足消费者的营销困境,提出了崭新的解决方案——以新技术为基础的群体筛选和推荐系统。随着信息管理软件和internet的高速发展,群体筛选技术下的推荐系统通过大量有关消费者偏好和购物记录的信息,以及对产品特征的准确把握,能够为消费者进行精确的推荐,提高了消费者的购物效率和准确度以及营销者的营销效率和竞争力。本书通过通俗而到位的讲解,向读者全面介绍了有关群体筛选技术的理......一起来看看 《鼠标宣言》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具