Эх сурвалжийг харах

数据响应系统目录结构整理,收集依赖部分讲解完成

HcySunYang 7 жил өмнө
parent
commit
460f8d0c14

+ 286 - 3
note/7Vue的初始化之数据响应系统.md

@@ -632,7 +632,7 @@ export function toggleObserving (value: boolean) {
 ob = new Observer(value)
 ```
 
-#### Observer 的作用
+#### Observer 构造函数
 
 其实真正将数据对象转换成响应式数据的是 `Observer` 函数,它是一个构造函数,同样定义在 `core/observer/index.js` 文件下,如下是简化后的代码:
 
@@ -658,7 +658,9 @@ export class Observer {
 
 可以清晰的看到 `Observer` 类的实例对象将拥有三个实例属性,分别是 `value`、`dep` 和 `vmCount` 以及两个实例方法 `walk` 和 `observeArray`。`Observer` 类的构造函数接收一个参数,即数据对象。下面我们就从 `constructor` 方法开始,研究实例化一个 `Observer` 类时都做了哪些事情。
 
-下面是 `constructor` 方法的全部代码:
+##### 数据对象的 `__ob__` 属性
+
+如下是 `constructor` 方法的全部代码:
 
 ```js
 constructor (value: any) {
@@ -718,6 +720,8 @@ const data = {
 }
 ```
 
+##### 响应式数据之纯对象的处理
+
 接着进入一个 `if...else` 判断分支:
 
 ```js
@@ -743,7 +747,11 @@ walk (obj: Object) {
 }
 ```
 
-`walk` 方法很简单,首先使用 `Object.keys(obj)` 获取对象属性所有可枚举的属性,然后使用 `for` 循环遍历这些属性,同时为每个属性调用了 `defineReactive` 函数。那我们就看一看 `defineReactive` 函数都做了什么,该函数也定义在 `core/observer/index.js` 文件,内容如下:
+`walk` 方法很简单,首先使用 `Object.keys(obj)` 获取对象属性所有可枚举的属性,然后使用 `for` 循环遍历这些属性,同时为每个属性调用了 `defineReactive` 函数。
+
+###### defineReactive 函数
+
+那我们就看一看 `defineReactive` 函数都做了什么,该函数也定义在 `core/observer/index.js` 文件,内容如下:
 
 ```js
 export function defineReactive (
@@ -806,6 +814,281 @@ export function defineReactive (
 }
 ```
 
+`defineReactive` 函数的核心就是将**数据对象的数据属性转换为访问器属性**,即为数据对象的属性设置一对 `getter/setter`,但其中做了很多处理边界条件的工作。`defineReactive` 接收五个参数,但是在 `walk` 方法中调用 `defineReactive` 函数时只传递了前两个参数,即数据对象和属性的键名。我们看一下 `defineReactive` 的函数体,首先定义了 `dep` 常量,它是一个 `Dep` 实例对象:
+
+```js
+const dep = new Dep()
+```
+
+我们在讲解 `Observer` 的 `constructor` 方法时看到过,在 `constructor` 方法中为数据对象定义了一个 `__ob__` 属性,该属性是一个 `Observer` 实例对象,且该对象包含一个 `Dep` 实例对象:
+
+```js
+const data = {
+  a: 1,
+  __ob__: {
+    value: data,
+    dep: dep实例对象, // new Dep() , 包含 Dep 实例对象
+    vmCount: 0
+  }
+}
+```
+
+当时我们说过 `__ob__.dep` 这个 `Dep` 实例对象的作用与我们在讲解数据响应系统基本思路一节中所说的“筐”的作用不同。至于他的作用是什么我们后面会讲到。其实与我们前面所说过的“筐”的作用相同的 `Dep` 实例对象是在 `defineReactive` 函数一开始定义的 `dep` 常量,即:
+
+```js
+const dep = new Dep()
+```
+
+这个 `dep` 常量所引用的 `Dep` 实例对象才与我们前面讲过的“筐”的作用相同。细心的同学可能已经注意到了 `dep` 在访问器属性的 `getter/setter` 中被闭包引用,如下:
+
+```js
+export function defineReactive (
+  obj: Object,
+  key: string,
+  val: any,
+  customSetter?: ?Function,
+  shallow?: boolean
+) {
+  const dep = new Dep()
+
+  // 省略...
+
+  Object.defineProperty(obj, key, {
+    enumerable: true,
+    configurable: true,
+    get: function reactiveGetter () {
+      const value = getter ? getter.call(obj) : val
+      if (Dep.target) {
+        // 这里闭包引用了上面的 dep 常量
+        dep.depend()
+        // 省略...
+      }
+      return value
+    },
+    set: function reactiveSetter (newVal) {
+      // 省略...
+
+      // 这里闭包引用了上面的 dep 常量
+      dep.notify()
+    }
+  })
+}
+```
+
+如上面的代码中注释所写的那样,在访问器属性的 `getter/setter` 中,通过闭包引用了前面定义的“筐”,即 `dep` 常量。这里大家要明确一件事情,即**每一个数据字段都通过闭包引用着属于自己的 `dep` 常量**。因为在 `walk` 函数中通过循环遍历了所有数据对象的属性,并调用 `defineReactive` 函数,所以每次调用 `defineReactive` 定义访问器属性时,该属性的 `setter/getter` 都闭包引用了一个属于自己的“筐”。假设我们有如下数据字段:
+
+```js
+const data = {
+  a: 1,
+  b: 2
+}
+```
+
+那么字段 `data.a` 和 `data.b` 都将通过闭包引用了属于自己的 `Dep` 实例对象,如下图所示:
+
+![](http://7xlolm.com1.z0.glb.clouddn.com/2018-04-05-032455.jpg)
+
+每个字段的 `Dep` 对象都被用来收集那些属于对应字段的依赖。
+
+在定义 `dep` 常量之后,是这样一段代码:
+
+```js
+const property = Object.getOwnPropertyDescriptor(obj, key)
+if (property && property.configurable === false) {
+  return
+}
+```
+
+首先通过 `Object.getOwnPropertyDescriptor` 函数获取该字段可能已有的属性描述对象,并将该对象保存在 `property` 常量中,接着是一个 `if` 语句块,判断该字段是否是可配置的,如果不可配置(`property.configurable === false`),那么直接返回(`return`),即不会继续执行 `defineReactive` 函数。这么做也是合理的,因为一个不可配置的属性是不能使用也没必要使用 `Object.defineProperty` 改变其属性定义的。
+
+再往下是这样一段代码:
+
+```js
+// cater for pre-defined getter/setters
+const getter = property && property.get
+const setter = property && property.set
+if ((!getter || setter) && arguments.length === 2) {
+  val = obj[key]
+}
+
+let childOb = !shallow && observe(val)
+```
+
+这段代码的前两句定义了 `getter` 和 `setter` 常量,分别保存了来自 `property` 对象的 `get` 和 `set` 函数,我们知道 `property` 对象是属性的描述对象,一个对象的属性很可能已经是一个访问器属性了,所以该属性很可能已经存在 `get` 或 `set` 方法。由于接下来会使用 `Object.defineProperty` 函数重新定义属性的 `setter/getter`,这会导致属性原有的 `set` 和 `get` 方法被覆盖,所以要将属性原有的 `setter/getter` 缓存,并在重新定义的 `set` 和 `get` 方法中调用缓存的函数,从而做到不影响属性的原有读取操作。
+
+上面这段代码中比较难理解的是 `if` 条件语句:
+
+```js
+(!getter || setter) && arguments.length === 2
+```
+
+其中 `arguments.length === 2` 这个条件好理解,当只传递两个参数时,说明没有传递第三个参数 `val`,那么此时需要根据 `key` 主动去对象上获取相应的值,即执行 `if` 语句块内的代码:`val = obj[key]`。那么 `(!getter || setter)` 这个条件的意思是什么呢?要理解这个条件我们需要思考一些实际应用的场景,或者说边界条件,但是现在还不适合给大家讲解,我们等到讲解完整个 `defineReactive` 函数之后,再回头来说。
+
+在 `if` 语句块的下面,是这句代码:
+
+```js
+let childOb = !shallow && observe(val)
+```
+
+定义了 `childOb` 变量,我们知道,在 `if` 语句块里面,获取到了对象属性的值 `val`,但是 `val` 本身有可能也是一个对象,那么此时应该继续调用 `observe(val)` 函数观测该对象从而深度观测数据对象。但前提是 `defineReactive` 函数的最后一个参数 `shallow` 应该是假,即 `!shallow` 为真时才会继续调用 `observe` 函数深度观测,由于在 `walk` 函数中调用 `defineReactive` 函数时没有传递 `shallow` 参数,所以该参数是 `undefined`,那么也就是说默认就是深度观测。其实非深度观测的场景我们早就遇到过了,即 `initRender` 函数中在 `Vue` 实例对象上定义 `$attrs` 属性和 `$listeners` 属性时就是非深度观测,如下:
+
+```js
+defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) // 最后一个参数 shallow 为 true
+defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
+```
+
+大家要注意一个问题,即使用 `observe(val)` 深度观测数据对象时,这里的 `val` 未必有值,因为必须在满足条件 `(!getter || setter) && arguments.length === 2` 时,才会触发取值的动作:`val = obj[key]`,所以一旦不满足条件即使属性是有值的但是由于没有触发取值的动作,所以 `val` 依然是 `undefined`。这就会导致深度观测无效,因为我们在分析 `observe` 函数的时候知道,只有当数据对象是数组或对象时才会成功被观测。对于这个问题我们后面还会详细的说。
+
+###### 被观测后的数据对象的样子
+
+现在我们需要明确一件事情,那就是一个数据对象经过了 `observe` 函数处理之后变成了什么样子,假设我们有如下数据对象:
+
+```js
+const data = {
+  a: {
+    b: 1
+  }
+}
+
+observe(data)
+```
+
+数据对象 `data` 拥有一个叫做 `a` 的属性,且属性 `a` 的值是另外一个对象,该对象拥有一个叫做 `b` 的属性。那么经过 `observe` 处理之后, `data` 和 `data.a` 这两个对象都被定义了 `__ob__` 属性,并且访问器属性 `a` 和 `b` 的 `setter/getter` 都通过闭包引用着属于自己的 `Dep` 实例对象和 `childOb` 对象:
+
+```js
+const data = {
+  // 属性 a 通过 setter/getter 通过闭包引用着 dep 和 childOb
+  a: {
+    // 属性 b 通过 setter/getter 通过闭包引用着 dep 和 childOb
+    b: 1
+    __ob__: {a, dep, vmCount}
+  }
+  __ob__: {data, dep, vmCount}
+}
+```
+
+如下图所示:
+
+![](http://7xlolm.com1.z0.glb.clouddn.com/2018-04-06-072754.jpg)
+
+需要注意的是,属性 `a` 闭包引用的 `childOb` 实际上就是 `data.a.__ob__`。而属性 `b` 闭包引用的 `childOb` 是 `undefined`,因为属性 `b` 是基本类型值,并不是对象也不是数组。
+
+###### 在 get 函数中如何收集依赖
+
+我们回过头来继续查看 `defineReactive` 函数的代码,接下来是 `defineReactive` 函数的关键代码,即使用 `Object.defineProperty` 函数定义访问器属性:
+
+```js
+Object.defineProperty(obj, key, {
+  enumerable: true,
+  configurable: true,
+  get: function reactiveGetter () {
+    // 省略...
+  },
+  set: function reactiveSetter (newVal) {
+    // 省略...
+})
+```
+
+当执行完以上代码实际上 `defineReactive` 函数就执行完毕了,对于访问器属性的 `get` 和 `set` 函数是不会执行的,因为此时没有触发属性的读取和设置操作。不过这不妨碍我们研究一下在 `get` 和 `set` 函数中都做了哪些事情,这里面就包含了我们在前面埋下伏笔的 `if` 条件语句的答案。我们先从 `get` 函数开始,看一看当属性被读取的时候都做了哪些事情,`get` 函数如下:
+
+```js
+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
+}
+```
+
+首先既然是 `getter`,那么当然要能够正确的返回属性的值,其次我们知道依赖的收集时机就是属性被读取的时候,所以 `get` 函数做了两件事:正确的返回属性值以及收集依赖,我们具体看一下代码,`get` 函数的第一句代码如下:
+
+```js
+const value = getter ? getter.call(obj) : val
+```
+
+首先判断是否存在 `getter`,我们知道 `getter` 常量中保存的属性原型的 `get` 函数,如果 `getter` 存在那么直接调用该函数,并以该函数的返回值作为属性的值,保证属性的原有读取操作正常运作。如果 `getter` 不存在则使用 `val` 作为属性的值。可以发现 `get` 函数的最后一句将 `value` 常量返回,这样 `get` 函数需要做的第一件事就完成了,即正确的返回属性值。
+
+除了正确的返回属性值,还要收集依赖,而处于 `get` 函数第一行和最后一行代码中间的所有代码都是用来完成收集依赖这件事儿的,下面我们就看一下它是如何收集依赖的,由于我们还没有讲解过 `Dep` 这个类,所以现在大家可以简单的认为 `dep.depend()` 这句代码的执行就意味着依赖被收集了。接下来我们仔细看一下代码:
+
+```js
+if (Dep.target) {
+  dep.depend()
+  if (childOb) {
+    childOb.dep.depend()
+    if (Array.isArray(value)) {
+      dependArray(value)
+    }
+  }
+}
+```
+
+首先判断 `Dep.target` 是否存在,那么 `Dep.target` 是什么呢?其实 `Dep.target` 与我们在数据响应系统基本思路一节中所讲的 `Target` 作用相同,所以 `Dep.target` 中保存的值就是要被收集的依赖(函数)。所以如果 `Dep.target` 存在的话说明有依赖需要被收集,这个时候才需要执行 `if` 语句块内的代码,如果 `Dep.target` 不存在就意味着没有需要被收集的依赖,所以当然就不需要执行 `if` 语句块内的代码了。
+
+在 `if` 语句块内第一句执行的代码就是:`dep.depend()`,执行 `dep` 对象的 `depend` 方法将依赖收集到 `dep` 这个“筐”中,这里的 `dep` 对象就是属性的 `getter/setter` 通过闭包引用的“筐”。
+
+接着又判断了 `childOb` 是否存在,如果存在那么就执行 `childOb.dep.depend()`,这段代码是什么意思呢?要想搞清楚这段代码的作用,你需要知道 `childOb` 是什么,前面我们分析过,假设有如下数据对象:
+
+```js
+const data = {
+  a: {
+    b: 1
+  }
+}
+```
+
+该数据对象经过观测处理之后,将被添加 `__ob__` 属性,如下:
+
+```js
+const data = {
+  a: {
+    b: 1,
+    __ob__: {value, dep, vmCount}
+  },
+  __ob__: {value, dep, vmCount}
+}
+```
+
+对于属性 `a` 来讲,访问器属性 `a` 的 `setter/getter` 通过闭包引用了一个 `Dep` 实例对象,即属性 `a` 用来收集依赖的“筐”。除此之外访问器属性 `a` 的 `setter/getter` 还闭包引用着 `childOb`,且 `childOb === data.a.__ob__` 所以 `childOb.dep === data.a.__ob__.dep`。所以 `childOb.dep.depend()` 这句话的执行就说明,除了要将依赖收集到属性 `a` 自己的“筐”里之外,还要将同样的依赖收集到 `data.a.__ob__.dep` 这里”筐“里,为什么要将同样的依赖分别收集到这两个不同的”筐“里呢?其实答案就在于这两个”筐“里收集的依赖的触发时机是不同的,即作用不同,两个”筐“如下:
+
+* 第一个”筐“是 `dep`
+* 第二个”筐“是 `childOb.dep`
+
+第一个”筐“里收集的依赖的触发时机是当属性值被修改时触发,即在 `set` 函数中触发:`dep.notify()`。而第二个”筐“里收集的依赖的触发时机是在使用 `$set` 或 `Vue.set` 给数据对象添加新属性时触发,我们知道由于 `js` 语言的限制,在没有 `Proxy` 之前 `Vue` 没办法拦截到给对象添加属性的操作。所以 `Vue` 才提供了 `$set` 和 `Vue.set` 等方法让我们有能力给对象添加新属性的同时触发依赖,那么触发依赖是怎么做到的呢?就是通过数据对象的 `__ob__` 属性做到的。因为 `__ob__.dep` 这个”筐“里收集了与 `dep` 这个”筐“同样的依赖。假设 `Vue.set` 函数代码如下:
+
+```js
+Vue.set = function (obj, key, val) {
+  defineReactive(obj, key, val)
+  obj.__ob__.dep.notify()
+}
+```
+
+如上代码所示,当我们使用上面的代码给 `data.a` 对象添加新的属性:
+
+```js
+Vue.set(data.a, 'c', 1)
+```
+
+上面的代码之所以能够触发依赖,就是因为 `Vue.set` 函数中触发了收集在 `data.a.__ob__.dep` 这个”筐“中的依赖:
+
+```js
+Vue.set = function (obj, key, val) {
+  defineReactive(obj, key, val)
+  obj.__ob__.dep.notify() // 相当于 data.a.__ob__.dep.notify()
+}
+
+Vue.set(data.a, 'c', 1)
+```
+
+所以 `__ob__` 属性以及 `__ob__.dep` 的主要作用是为了添加、删除属性时有能力触发依赖,而这就是 `Vue.set` 或 `Vue.delete` 的原理。
+
+###### 在 set 函数中如何触发依赖