|
@@ -753,7 +753,7 @@ walk (obj: Object) {
|
|
|
|
|
|
`walk` 方法很简单,首先使用 `Object.keys(obj)` 获取对象属性所有可枚举的属性,然后使用 `for` 循环遍历这些属性,同时为每个属性调用了 `defineReactive` 函数。
|
|
|
|
|
|
-#### defineReactive 函数
|
|
|
+### defineReactive 函数
|
|
|
|
|
|
那我们就看一看 `defineReactive` 函数都做了什么,该函数也定义在 `core/observer/index.js` 文件,内容如下:
|
|
|
|
|
@@ -918,7 +918,7 @@ defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null,
|
|
|
|
|
|
大家要注意一个问题,即使用 `observe(val)` 深度观测数据对象时,这里的 `val` 未必有值,因为必须在满足条件 `(!getter || setter) && arguments.length === 2` 时,才会触发取值的动作:`val = obj[key]`,所以一旦不满足条件即使属性是有值的但是由于没有触发取值的动作,所以 `val` 依然是 `undefined`。这就会导致深度观测无效。
|
|
|
|
|
|
-#### 被观测后的数据对象的样子
|
|
|
+### 被观测后的数据对象的样子
|
|
|
|
|
|
现在我们需要明确一件事情,那就是一个数据对象经过了 `observe` 函数处理之后变成了什么样子,假设我们有如下数据对象:
|
|
|
|
|
@@ -952,7 +952,7 @@ const data = {
|
|
|
|
|
|
需要注意的是,属性 `a` 闭包引用的 `childOb` 实际上就是 `data.a.__ob__`。而属性 `b` 闭包引用的 `childOb` 是 `undefined`,因为属性 `b` 是基本类型值,并不是对象也不是数组。
|
|
|
|
|
|
-#### 在 get 函数中如何收集依赖
|
|
|
+### 在 get 函数中如何收集依赖
|
|
|
|
|
|
我们回过头来继续查看 `defineReactive` 函数的代码,接下来是 `defineReactive` 函数的关键代码,即使用 `Object.defineProperty` 函数定义访问器属性:
|
|
|
|
|
@@ -1077,7 +1077,7 @@ if (Array.isArray(value)) {
|
|
|
|
|
|
如果读取的属性值是数组,那么需要调用 `dependArray` 函数逐个触发数组每个元素的依赖收集,为什么这么做呢?那是因为 `Observer` 类在定义响应式属性时对于纯对象和数组的处理方式是不同,对于上面这段 `if` 语句的目的等到我们讲解到对于数组的处理时,会详细说明。
|
|
|
|
|
|
-#### 在 set 函数中如何触发依赖
|
|
|
+### 在 set 函数中如何触发依赖
|
|
|
|
|
|
在 `get` 函数中收集了依赖之后,接下来我们就要看一下在 `set` 函数中是如何触发依赖的,即当属性被修改的时候如何触发依赖。`set` 函数如下:
|
|
|
|
|
@@ -1176,7 +1176,7 @@ dep.notify()
|
|
|
|
|
|
至此 `set` 函数我们就讲解完毕了。
|
|
|
|
|
|
-#### 保证定义响应式数据行为的一致性
|
|
|
+### 保证定义响应式数据行为的一致性
|
|
|
|
|
|
本节我们主要讲解 `defineReactive` 函数中的一段代码,即:
|
|
|
|
|
@@ -1315,7 +1315,7 @@ if (Array.isArray(value)) {
|
|
|
|
|
|
在 `if` 条件语句中,使用 `Array.isArray` 函数检测被观测的值 `value` 是否是数组,如果是数组则会执行 `if` 语句块内的代码,从而实现对数组的观测。处理数组的方式与纯对象不同,我们知道数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,我们称其为变异方法,这些方法有:`push`、`pop`、`shift`、`unshift`、`splice`、`sort` 以及 `reverse` 等。这个时候我们就要考虑一件事,即当用户调用这些变异方法改变数组时需要触发依赖。换句话说我们需要知道开发者何时调用了这些变异方法,只有这样我们才有可能在这些方法被调用时做出反应。
|
|
|
|
|
|
-#### 拦截数组变异方法的思路
|
|
|
+### 拦截数组变异方法的思路
|
|
|
|
|
|
那么怎么样才能知道开发者何时调用了数组的变异方法呢?其实很简单,我们来思考这样一个问题,如下代码中 `sayHello` 函数用来打印字符串 `'hello'`:
|
|
|
|
|
@@ -1410,7 +1410,7 @@ arrayKeys.forEach(method => {
|
|
|
|
|
|
这样就完美了。
|
|
|
|
|
|
-#### 拦截数组变异方法在 Vue 中的实现
|
|
|
+### 拦截数组变异方法在 Vue 中的实现
|
|
|
|
|
|
我们已经了解了拦截数组变异方法的思路,接下来我们就可以具体的看一下 `Vue` 源码是如何实现的。在这个过程中我们会讲解数组是如何通过变异方法触发依赖(`观察者`)的。
|
|
|
|
|
@@ -1730,9 +1730,109 @@ observeArray (items: Array<any>) {
|
|
|
|
|
|
可以发现 `observeArray` 方法的实现很简单,只需要对数组进行遍历,并对数组元素逐个应用 `observe` 工厂函数即可,这样就会递归观测数组元素了。
|
|
|
|
|
|
-#### 数组的特殊性
|
|
|
+### 数组的特殊性
|
|
|
|
|
|
+本小节我们补讲 `defineReactive` 函数中的一段代码,如下:
|
|
|
|
|
|
+```js {7-9}
|
|
|
+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
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在 [get 函数中如何收集依赖](#在-get-函数中如何收集依赖) 一节中我们已经讲解了关于依赖收集的内容,但是当时我们留下了如上代码段中高亮的那三行代码没有讲,现在我们就重点看一下高亮的三句代码,为什么当被读取的属性是数组的时候需要调用 `dependArray` 函数?
|
|
|
+
|
|
|
+为了弄清楚这个问题,假设我们有如下代码:
|
|
|
+
|
|
|
+```js {2,8-10}
|
|
|
+<div id="demo">
|
|
|
+ {{arr}}
|
|
|
+</div>
|
|
|
+
|
|
|
+const ins = new Vue({
|
|
|
+ el: '#demo',
|
|
|
+ data: {
|
|
|
+ arr: [
|
|
|
+ { a: 1 }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+首先我们观察一下数据对象:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ arr: [
|
|
|
+ { a: 1 }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+数据对象中的 `arr` 属性是一个数组,并且数组的一个元素是另外一个对象。我们 [被观测后的数据对象的样子](#被观测后的数据对象的样子) 一节中将过了,上面的对象在经过观测后将变成如下这个样子:
|
|
|
+
|
|
|
+```js {3-4}
|
|
|
+{
|
|
|
+ arr: [
|
|
|
+ { a: 1, __ob__ /* 我们将该 __ob__ 称为 ob2 */ },
|
|
|
+ __ob__ /* 我们将该 __ob__ 称为 ob1 */
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+如上代码的注释所示,为了便于区别和讲解,我们分别称这两个 `__ob__` 属性为 `ob1` 和 `ob2`,然后我们再来观察一下模板:
|
|
|
+
|
|
|
+```js
|
|
|
+<div id="demo">
|
|
|
+ {{arr}}
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+在模板使用了数据 `{{arr}}`,这将会触发数据对象的 `arr` 属性的 `get` 函数,我们知道 `arr` 属性的 `get` 函数通过闭包引用了两个用来收集依赖的”筐“,一个是属于 `arr` 属性自身的 `dep` 对象,另一个是 `childOb.dep` 对象,其中 `childOb` 就是 `ob1`。这时依赖会被收集到这两个”筐“中,但大家要注意的是 `ob2.dep` 这个”筐“中,是没有收集到依赖的。有的同学会说:”模板中依赖的数据是 `arr`,并不是 `arr` 数组的第一个对象元素,所以 `ob2` 没有收集到依赖很正常啊“,这是一个错误的想法,因为依赖了数组 `arr` 就等价于依赖了数组内的所有元素,数组内所有元素的改变都可以看做是数组的改变。但由于 `ob2` 没有收集到依赖,所以现在就导致如下代码触发不了响应:
|
|
|
+
|
|
|
+```js
|
|
|
+ins.$set(ins.$data.arr[0], 'b', 2)
|
|
|
+```
|
|
|
+
|
|
|
+我们使用 `$set` 函数为 `arr` 数组的第一对象元素添加了一个属性 `b`,这是触发不了响应的。为了能够使得这段代码可以触发响应,就必须让 `ob2` 收集到依赖,而这就是 `dependArray` 函数的作用。如下是 `dependArray` 函数的代码:
|
|
|
+
|
|
|
+```js
|
|
|
+function dependArray (value: Array<any>) {
|
|
|
+ for (let e, i = 0, l = value.length; i < l; i++) {
|
|
|
+ e = value[i]
|
|
|
+ e && e.__ob__ && e.__ob__.dep.depend()
|
|
|
+ if (Array.isArray(e)) {
|
|
|
+ dependArray(e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+当被读取的数据对象的属性值是数组时,会调用 `dependArray` 函数,该函数将通过 `for` 循环遍历数组,并取得数组每一个元素的值,如果该元素的值拥有 `__ob__` 对象和 `__ob__.dep` 对象,那说明该元素也是一个对象或数组,此时只需要手动执行 `__ob__.dep.depend()` 即可达到收集依赖的目的。同时如果发现数组的元素仍然是一个数组,那么需要递归调用 `dependArray` 继续收集依赖。
|
|
|
+
|
|
|
+那么为什么数组需要这样处理,而纯对象不需要呢?那是因为**数组的索引是非响应式的**。现在我们已经知道了数据响应系统对纯对象和数组的处理方式是不同,对于纯对象只需要逐个将对象的属性重定义为访问器属性,并且当属性的值同样为纯对象时进行递归定义即可,而对于数组的处理则是通过拦截数组变异方法的方式,也就是说如下代码是触发不了响应的:
|
|
|
+
|
|
|
+```js {7}
|
|
|
+const ins = new Vue({
|
|
|
+ data: {
|
|
|
+ arr: [1, 2]
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+ins.arr[0] = 3 // 不能触发响应
|
|
|
+```
|
|
|
+
|
|
|
+上面的代码中我们试图修改 `arr` 数组的第一个元素,但这么做是触发不了响应的,因为对于数组来讲,其索引并不是“访问器属性”。正是因为数组的索引不是”访问器属性“,所以当有观察者依赖数组的某一个元素时是触发不了这个元素的 `get` 函数的,当然也就收集不到依赖。这个时候就是 `dependArray` 函数发挥作用的时候了。
|
|
|
|
|
|
|
|
|
|