乐趣区

关于vue.js:Vue2响应式原理解析一从设计出发

Vue 的响应式零碎是 Vue 最有意思的个性之一,data 只须要返回一个一般的字面量对象,在运行时批改它的属性就会引起界面的更新。当初都是数据驱动界面开发,这种设计对于程序员开发来说十分爽,关注点只用放在数据变动的逻辑上。并且 Vue 把这个个性形象成了一个独立的 observer 模块,能够独自剥离应用,比方小程序开发框架 Wepy 就采纳了这个模块来实现响应式。

这段时间我看了 Vue 2.x 对于 observer 的源码,这里呢也谈一下我对 observer 设计与要害局部实现的了解,以下的内容是基于 Vue 2.x 的源码剖析。尽管当初曾经有很多剖析 Vue 响应式的文章了,心愿我的了解也能给读者一些启发吧,把这块儿的常识排汇为本人所用。

如何追踪数据扭转?

响应式最外围的问题是:如何去追踪 data 对象的属性扭转呢?如果这一点无奈实现或者对于开发者来说编码体验不好,那么响应式设计前面的路就不好走了。Vue 2.x 这里是基于 Object.defineProperty 来做的,将这些属性全副包装上 getter/setter,也就是劫持对象的属性拜访。这种实现形式对于开发者来说根本是齐全无感的。上面我摘出要害的代码片段来阐明一下是如何实现的(假设咱们设置的 data 是一个一般的字面量对象),如果你只是想学习设计思路也没必要去看残缺的源码。

首先在 src/core/instance/state.js 中有如下代码:

function initData (vm: Component) {
  // 这就是咱们申明的 data 数据对象,vm 是 Vue 对象
  let data = vm.$options.data
  
  // ...
  
  // 察看 data
  observe(data, true /* asRootData */)
}

下面这段代码是初始化 Vue 对象的 data,而后对 data 对象进行了察看设置。咱们跳转到 observe() 的局部:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // ...
  let ob: Observer | void
  // 先不关注
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (shouldObserve && // 这里有一堆的条件,先不关注...) {
    // 关注这里!ob = new Observer(value)
  }  
}

这里呈现了一个 Observer 类,并且传入了 value 参数,就是咱们设置的 data 对象,这里就是怎么劫持属性拜访的要害类了,咱们跳转到 Observer 的源码,在 Observer 构造函数中会调用这么一段代码:

export class Observer {
    // ...
  
  constructor (value: any) {
    // 仍然省略大量代码...
    
    if (Array.isArray(value)) {// 先不思考数组} else {
      // 看这里
      this.walk(value)
    }
  }
}

这里呈现了一个 walk() 办法,把 data 作为参数传递进去了,来看看 walk() 是什么:

walk (obj: Object) {const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // 终于到了这里...
    defineReactive(obj, keys[i])
  }
}

这个 walk() 外面去遍历了 data 的所有本身属性,而后对每个 key 调用了 defineReactive() 函数。defineReactive() 从这个名字来看就是“定义响应的”,并且第一个参数是传入的 data,第二个参数是属性 key,看来就是针对 data 的每个 key 去设置对应的属性如何进行响应式了,到这里咱们终于找到了劫持的大门~

劫持

defineReactive() 函数次要通过从新定义 data 上每个 key 的属性描述符来达到挟持属性拜访的目标。利用 Object.defineProperty 为每个属性定义新的存取描述符,而后利用存取描述符的 getter/setter 来劫持属性的存取。

到这里咱们要思考一个问题,咱们能间接在 setter 外面去触发界面的更新吗?

响应式设计有很多的利用场景,比方 Vue 提供了计算属性 computed 和侦听属性 watch。如果咱们把更新界面的操作间接写到 setter 外面的话,当再呈现多一种场景需要的时候就须要在 setter 外面多加一段代码。一旦这样做了,就会频繁的批改 setter,这段代码就会变得不稳固和软弱。所以咱们要防止这种坏设计(bad design),而应该将这段代码 关闭 起来,使它能适应将来的 扩大

形象

Abstraction is the Key.

方向确定了,就要寻找一种实现计划。这里咱们先对响应式这种需要做一层形象:某个用户代码须要在 data 的某些属性发生变化时失去告诉,从而执行特定的工作(代码)

某个用户代码是未知的,失去告诉后须要执行的工作代码也是未知的,咱们惟一晓得的是:用户代码关怀 data 某些属性的变动并冀望失去告诉。基于以上的形象,咱们很容易就想到 观察者 模式了~

在 Vue 的响应式设计里,data 数据对象的属性是被观察者,刷新界面、计算属性和侦听属性都属于观察者。这些观察者 订阅 data 中他们感兴趣的局部(也就是 key 对应的对象属性),当这些属性发生变化后,被观察者 公布 告诉,观察者就去执行他们本人的工作了~

首先假如一下,一旦这个模式被设计编码进去,是一个好的设计吗?针对响应式的场景需要,咱们只须要为每个属性建设一个观察者的关联列表,当属性扭转时去挨个告诉这些观察者就好了,而这些观察者是谁咱们并不需要关怀,这样就实现了咱们形象的目标。

这里先给一张图看看 Vue 是怎么设计的:

Dep 类记录了依赖关系,Watcher 就是形象的观察者。那 Vue 是怎么建设依赖关系的呢?上面就来逐个解析下。

记录依赖关系

咱们先想一下依赖关系在什么时候去收集是适合的?

如下面探讨的,Vue 中的观察者比方计算属性等是一个函数,在函数的执行过程中会读取 data 的若干属性,这就象征会拜访到属性的 getter。那么天然把收集依赖的工作放在 getter 外面是适合的,那具体要怎么设计怎么实现呢?

咱们还是先从要害代码动手,来看看 defineReactive()getter 的代码:

const dep = new Dep()
// ...

get: function reactiveGetter () {const value = getter ? getter.call(obj) : val
  if (Dep.target) { // Dep.target 就是以后的观察者
    dep.depend() // 建设依赖关系
    // ...
  }
  return value
},

getter 里会判断 Dep.target 是不是有值(以后的观察者),如果有值的话就执行 dep.depend() 建设依赖关系。这里 Dep.target 为什么就是以后的观察者呢?

后面咱们说过了观察者都被形象成了 WatcherWatcher 的构造函数会传入一个 expOrFn(就是客户代码,比方:计算属性的定义函数),而后被保留为一个叫 getter 的成员。在 Watcher 构造函数的最初会执行 Watcherget() 办法,get() 的要害局部如下:

get () {pushTarget(this) // 这里设置 Dep.target
  let value
  const vm = this.vm
  try {value = this.getter.call(vm, vm)
  } catch (e) {// ...} finally {popTarget() // 运行完 getter 后勾销设置 Dep.target
    // ...
  }
  return value
}

看到这里咱们就大抵明确了,每个 Watcher 在执行客户代码以前,会把本人设置为 Dep.target,并在运行完客户代码后勾销 Dep.target 的设置,pushTarget()popTarget() 的代码如下:

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {targetStack.push(target)
  Dep.target = target
}

export function popTarget () {targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

// 为什么会有 targetStack 数组 将在当前的文章中解析

Watcher 中的 get() 办法联合 defineReactive()getter 的局部咱们终于大抵晓得是如何收集依赖了:

每段客户代码都被形象为 Watcher 中的 getter 并被包裹在 get() 中执行,在执行前 Dep.target 设置为 Watcher 本身,执行完后勾销设置。

依赖关系发生变化

Vue 记录了依赖的双向关系。Dep 上有个 subs 数组记录了察看这个属性的观察者们,每个观察者 Watcher 有个 deps 数组记录了须要察看的属性相干 Dep 实例。这里有点奇怪的是为什么须要记录双向关系呢,如果只是告诉观察者的话,只须要在 Dep 上的 subs 数组就好了呀~

这是因为 Watcher 须要在某些场景去除掉一些不再须要的依赖关系,那么就须要比对原先的依赖 deps 数组和以后最新的依赖数组之间的差别(所以 Watcher 有配合的 depIdsnewDepIdsnewDeps 来做这件事)。那什么场景会产生依赖关系的变动呢?比方,在计算属性的函数中,依据某些逻辑变量的判断来援用不同的 data 属性,那么这种状况下依赖关系就会产生变动,用一段代码来阐明下:

const vm = new Vue({
  el: '#demo',
  data: {
    a: 1,
    b: 2,
    c: false
  },
  computed: {some: function () {if (c) {return a + b} else {return b}
    }
  }
})

如果 c 的值是 true,计算属性 some 依赖的 data 属性是 ab;如果 c 的值扭转成了 false,则 some 就只依赖 b 了。所以在业务场景中,依赖关系的确有可能会发送变动的。

告诉更新

记录了依赖关系后,当属性发生变化时去告诉就很简略了。回到 defineReactive() 中的 setter 代码:

const dep = new Dep()
// ...

set: function reactiveSetter (newVal) {
  // ...
  
  // 后面的按下不表,当前再解释,先只关注这句话
  dep.notify()}

当属性被赋值时,就执行 dep.notify(),外面会一一去告诉 Watcher 执行 update()。前面咱们会具体再说下 update() 的过程,这里咱们只须要晓得更新告诉是怎么产生的就行了。

接下来

以上就是我了解的 Vue 响应式是如何去设计的,以及依赖收集的要害代码解读。我感觉一个好的框架或者第三方库他们的设计思路和形象形式是十分值得咱们去学习的,咱们常说的看源码学习次要也是学的这部分。其次要关注作者在实现上的一些技巧,比方这里的依赖收集形式就是比拟有特点的。

程度无限,了解和解读难免会有错漏,欢送指出哇~ 下一篇将写一下计算属性的一些设计、实现和其余局部。

退出移动版