فهرست منبع

讲解定义响应式数据行为的一致性

HcySunYang 7 سال پیش
والد
کامیت
5751552cad
1فایلهای تغییر یافته به همراه120 افزوده شده و 1 حذف شده
  1. 120 1
      note/7Vue的初始化之数据响应系统.md

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

@@ -1197,9 +1197,128 @@ dep.notify()
 
 至此 `set` 函数我们就讲解完毕了。
 
-###### 保证程序行为的一致性
+###### 保证定义响应式数据行为的一致性
 
+本节我们主要讲解 `defineReactive` 函数中一段代码,即:
 
+```js
+if ((!getter || setter) && arguments.length === 2) {
+  val = obj[key]
+}
+```
+
+在之前的讲解中,我们没有详细的讲解如上代码所示的这段 `if` 语句块。该 `if` 语句有两个条件:
+
+* 第一:`(!getter || setter)`
+* 第二:`arguments.length === 2`
+
+并且这两个条件要同时满足才能会根据 `key` 去对象 `obj` 上取值:`val = obj[key]`,否则就不会触发取值的动作,触发不了取值的动作就意味着 `val` 的值为 `undefined`,这会导致 `if` 语句块后面的那句深度观测的代码无效,即不会深度观测:
+
+```js
+// val 是 undefined,不会深度观测
+let childOb = !shallow && observe(val)
+```
+
+对于第二个条件,很好理解,当传递参数的数量为 `2` 时,说明没有传递第三个参数 `val`,那么当然需要通过执行 `val = obj[key]` 去获取属性值。比较难理解的是第一个条件,即 `(!getter || setter)`,要理解这个问题你需要知道 `Vue` 代码的变更,以及为什么变更。其实在最初并没有上面这段 `if` 语句块,在 `walk` 函数中是这样调用 `defineReactive` 函数的:
+
+```js
+walk (obj: Object) {
+  const keys = Object.keys(obj)
+  for (let i = 0; i < keys.length; i++) {
+    // 这里传递了第三个参数
+    defineReactive(obj, keys[i], obj[keys[i]])
+  }
+}
+```
+
+可以发现在调用 `defineReactive` 函数的时候传递了第三个参数,即属性值。这是最初的实现,后来变成了如下这样:
+
+```js
+walk (obj: Object) {
+  const keys = Object.keys(obj)
+  for (let i = 0; i < keys.length; i++) {
+    // 在 walk 函数中调用 defineReactive 函数时暂时不获取属性值
+    defineReactive(obj, keys[i])
+  }
+}
+
+// ================= 分割线 =================
+
+// 在 defineReactive 函数内获取属性值
+if (!getter && arguments.length === 2) {
+  val = obj[key]
+}
+```
+
+在 `walk` 函数中调用 `defineReactive` 函数时去掉了第三个参数,而是在 `defineReactive` 函数体内增加了一段 `if` 分支语句,当发现调用 `defineReactive` 函数时传递了两个参数,同时只有在属性没有 `get` 函数的情况下才会通过 `val = obj[key]` 取值。
+
+为什么要这么做呢?具体可以查看这个 [issue](https://github.com/vuejs/vue/pull/7302)。简单的说就是当属性原本存在 `get` 拦截器函数时,在初始化的时候不要触发 `get` 函数,只有当真正的获取该属性的值的时候,在通过调用缓存下来的属性原本的 `getter` 函数取值即可。所以看到这里我们能够发现,如果数据对象的某个属性原本就拥有自己的 `get` 函数,那么这个属性就不会被深度观测,因为当属性原本存在 `getter` 时,是不会触发取值动作的,即 `val = obj[key]` 不会执行,所以 `val` 是 `undefined`,这就导致在后面深度观测的语句中传递给 `observe` 函数的参数是 `undefined`。
+
+举个例子,如下:
+
+```js
+const data = {
+  getterProp: {
+    a: 1
+  }
+}
+
+new Vue({
+  data,
+  watch: {
+    'getterProp.a': () => {
+      console.log('这句话会输出')
+    }
+  }
+})
+```
+
+上面的代码中,我们定义了数据 `data`,`data` 是一个嵌套的对象,在 `watch` 选项中观察了属性 `getterProp.a`,当我们修改 `getterProp.a` 的值时,以上代码是能够正常输出的,这也是预期行为。再看如下代码:
+
+```js
+const data = {}
+Object.defineProperty(data, 'getterProp', {
+  enumerable: true,
+  get: () => {
+    return {
+      a: 1
+    }
+  }
+})
+
+const ins = new Vue({
+  data,
+  watch: {
+    'getterProp.a': () => {
+      console.log('这句话不会输出')
+    }
+  }
+})
+```
+
+我们仅仅修改了定义数据对象 `data` 的方式,此时 `data.getterProp` 本身已经是一个访问器属性,且拥有 `get` 方法。此时当我们尝试修改 `getterProp.a` 的值时,在 `watch` 中观察 `getterProp.a` 的函数不会被执行。这是因为属性 `getterProp` 是一个拥有 `get` 拦截器函数的访问器属性,而当 `Vue` 发现该属性拥有原本的 `getter` 时,是不会深度观测的。
+
+那么为什么当属性拥有自己的 `getter` 时就不会对其深度观测了呢?有两方面的原因,第一:由于当属性存在原本的 `getter` 时在深度观测之前不会取值,所以在在深度观测语句执行之前取不到属性值从而无法深度观测。第二:之所以在深度观测之前不取值是因为属性原本的 `getter` 由用户定义,用户可能在 `getter` 中做任何意想不到的事情,这么做是出于避免引发不必要的行为的考虑。
+
+我们回过头来再看这段 `if` 语句块:
+
+```js
+if (!getter && arguments.length === 2) {
+  val = obj[key]
+}
+```
+
+这么做难道不会有什么问题吗?当然有问题,我们知道当数据对象的某一个属性只拥有 `get` 拦截器函数而没有 `set` 拦截器函数时,此时该属性不会被深度观测。但是经过 `defineReactive` 函数的处理之后,该属性将被重新定义 `getter` 和 `setter`,此时该属性变成了既拥有 `get` 函数又拥有 `set` 函数。并且当我们尝试给该属性重新赋值时,那么新的值将会被观测。这时候矛盾就产生了:**原本该属性不会被深度观测,但是重新赋值之后,新的值却被观测了**。
+
+这就是所谓的**定义响应式数据时行为的不一致**,为了解决这个问题,我们的解决办法是当属性拥有原本的 `setter` 时,即使拥有 `getter` 也要获取属性值并观测之,这样代码就变成了最终这个样子:
+
+```js
+if ((!getter || setter) && arguments.length === 2) {
+  val = obj[key]
+}
+```
+
+##### 响应式数据之数组的处理