|
@@ -43,7 +43,7 @@ data = vm._data = getData(data, vm)
|
|
|
|
|
|
关于这个问题,我提交了一个 `PR`,详情可以查看这里:[https://github.com/vuejs/vue/pull/7875](https://github.com/vuejs/vue/pull/7875)
|
|
|
|
|
|
-回到上面那句代码,这句话的调用了 `getData` 函数,`getData` 函数就定义在 `initData` 函数的下面,我们看看其作用是什么:
|
|
|
+回到上面那句代码,这句话调用了 `getData` 函数,`getData` 函数就定义在 `initData` 函数的下面,我们看看其作用是什么:
|
|
|
|
|
|
```js
|
|
|
export function getData (data: Function, vm: Component): any {
|
|
@@ -220,8 +220,8 @@ observe(data, true /* asRootData */)
|
|
|
|
|
|
* 根据 `vm.$options.data` 选项获取真正想要的数据(注意:此时 `vm.$options.data` 是函数)
|
|
|
* 校验得到的数据是否是一个纯对象
|
|
|
-* 检查数据对象 `data` 上的键是否与 `props` 冲突
|
|
|
-* 检查 `methods` 对象上的键是否与 `data` 上的键冲突
|
|
|
+* 检查数据对象 `data` 上的键是否与 `props` 对象上的键冲突
|
|
|
+* 检查 `methods` 对象上的键是否与 `data` 对象上的键冲突
|
|
|
* 在 `Vue` 实例对象上添加代理访问数据对象的同名属性
|
|
|
* 最后调用 `observe` 函数开启响应式之路
|
|
|
|
|
@@ -265,9 +265,9 @@ $watch('a', () => {
|
|
|
})
|
|
|
```
|
|
|
|
|
|
-要实现这个功能,说复杂也复杂说简单也简单,复杂在于我们需要考虑的内容比较多,比如如何避免收集重复的依赖,如何深度观测,如何处理数组以及其他边界条件等等。简单在于如果不考虑那么多边界条件的话,要实现这样一个功能还是很容易的,这一小节我们就从简入手,致力于让大家思路清晰,至于各种复杂情况的处理我们会在真正讲解源码的部分会依依为大家解答。
|
|
|
+要实现这个功能,说复杂也复杂说简单也简单,复杂在于我们需要考虑的内容比较多,比如如何避免收集重复的依赖,如何深度观测,如何处理数组以及其他边界条件等等。简单在于如果不考虑那么多边界条件的话,要实现这样一个功能还是很容易的,这一小节我们就从简入手,致力于让大家思路清晰,至于各种复杂情况的处理我们会在真正讲解源码的部分依依为大家解答。
|
|
|
|
|
|
-要实现上文的功能,我们面临的第一个问题是,如何才能知道属性被修改了(或被设置了)。这时候我们就要依赖 `Object.defineProperty` 函数,通过该函数将对象的属性转换为访问器属性,为属性设置一对 `getter/setter` 从而得知属性被读取和被设置,如下:
|
|
|
+要实现上文的功能,我们面临的第一个问题是,如何才能知道属性被修改了(或被设置了)。这时候我们就要依赖 `Object.defineProperty` 函数,通过该函数为对象的每个属性设置一对 `getter/setter` 从而得知属性被读取和被设置,如下:
|
|
|
|
|
|
```js
|
|
|
Object.defineProperty(data, 'a', {
|
|
@@ -280,7 +280,7 @@ Object.defineProperty(data, 'a', {
|
|
|
})
|
|
|
```
|
|
|
|
|
|
-这样我们就实现了对属性 `a` 的设置和获取操作的拦截,有了它我们就可以大胆的思考一些事情,比如:**能不能在获取属性 `a` 的时候收集依赖,然后在设置属性 `a` 的时候触发之前收集的依赖呢?**嗯,这是一个好思路,不过既然要收集依赖,我们起码需要一个”筐“,然后将所有收集到的依赖通通放到这个”筐”里,当属性被设置的时候将“筐”里所有的依赖都拿出来执行就可以了,落实到代码如下:
|
|
|
+这样我们就实现了对属性 `a` 的设置和获取操作的拦截,有了它我们就可以大胆的思考一些事情,比如: **能不能在获取属性 `a` 的时候收集依赖,然后在设置属性 `a` 的时候触发之前收集的依赖呢?** 嗯,这是一个好思路,不过既然要收集依赖,我们起码需要一个”筐“,然后将所有收集到的依赖通通放到这个”筐”里,当属性被设置的时候将“筐”里所有的依赖都拿出来执行就可以了,落实到代码如下:
|
|
|
|
|
|
```js
|
|
|
// dep 数组就是我们所谓的“筐”
|
|
@@ -299,7 +299,7 @@ Object.defineProperty(data, 'a', {
|
|
|
|
|
|
如上代码所示,我们定义了常量 `dep`,它是一个数组,这个数组就是我们所说的“筐”,当获取属性 `a` 的值时将触发 `get` 函数,在 `get` 函数中,我们将收集到的依赖放入“筐”内,当设置属性 `a` 的值时将触发 `set` 函数,在 `set` 函数内我们将“筐”里的依赖全部拿出来执行。
|
|
|
|
|
|
-但是新的问题出现了,上面的代码中我们假设 `fn` 函数就是我们需要收集的依赖(`观察者`),但 `fn` 从何而来呢?**也就是说如何在获取属性 `a` 的值时收集依赖呢?**为了解决这个问题我们需要思考一下我们现在都掌握哪些条件,这个时候我们就需要在 `$watch` 函数中做文章了,我们知道 `$watch` 函数接收两个参数,第一个参数是一个字符串,即数据字段名,比如 `'a'`,第二个参数是依赖该字段的函数:
|
|
|
+但是新的问题出现了,上面的代码中我们假设 `fn` 函数就是我们需要收集的依赖(`观察者`),但 `fn` 从何而来呢? **也就是说如何在获取属性 `a` 的值时收集依赖呢?** 为了解决这个问题我们需要思考一下我们现在都掌握哪些条件,这个时候我们就需要在 `$watch` 函数中做文章了,我们知道 `$watch` 函数接收两个参数,第一个参数是一个字符串,即数据字段名,比如 `'a'`,第二个参数是依赖该字段的函数:
|
|
|
|
|
|
```js
|
|
|
$watch('a', () => {
|
|
@@ -354,7 +354,7 @@ const data = {
|
|
|
b: 1
|
|
|
}
|
|
|
|
|
|
-for (let key in data) {
|
|
|
+for (const key in data) {
|
|
|
const dep = []
|
|
|
Object.defineProperty(data, key, {
|
|
|
set () {
|
|
@@ -483,9 +483,9 @@ function $watch (exp, fn) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-我们对 `$watch` 函数做了一些改造,首先检查要读取的字段是否包含 `.`,如果包含 `.` 说明读取嵌套对象的字段,这时候我们使用字符串的 `split('.')` 函数将字符串转为数组,所以如果访问的路径是 `a.b` 那么转换后的数组就是 `['a', 'b']`,然后使用一个循环从而读取到嵌套对象的属性值,不过需要注意的是读取到嵌套对象的属性值之后应该立即返回 `return`,不需要再执行后面的代码。
|
|
|
+我们对 `$watch` 函数做了一些改造,首先检查要读取的字段是否包含 `.`,如果包含 `.` 说明读取嵌套对象的字段,这时候我们使用字符串的 `split('.')` 函数将字符串转为数组,所以如果访问的路径是 `a.b` 那么转换后的数组就是 `['a', 'b']`,然后使用一个循环从而读取到嵌套对象的属性值,不过需要注意的是读取到嵌套对象的属性值之后应该立即 `return`,不需要再执行后面的代码。
|
|
|
|
|
|
-下面我们再进一步,我们思考一下 `$watch` 函数的原理的是什么?其实 `$watch` 函数所做的事情就是想方设法的访问到你要观测的字段,从而触发该字段的 `get` 函数,进而收集依赖(观察者)。现在我们传递给 `$watch` 函数的第一个参数是一个字符串,代表要访问数据的哪一个字段属性,那么除了字符串之外可以不可以是一个函数呢?假设我们有一个函数叫做 `render`,如下
|
|
|
+下面我们再进一步,我们思考一下 `$watch` 函数的原理的是什么?其实 `$watch` 函数所做的事情就是想方设法的访问到你要观测的字段,从而触发该字段的 `get` 函数,进而收集依赖(观察者)。现在我们传递给 `$watch` 函数的第一个参数是一个字符串,代表要访问数据的哪一个字段属性,那么除了字符串之外可不可以是一个函数呢?假设我们有一个函数叫做 `render`,如下
|
|
|
|
|
|
```js
|
|
|
const data = {
|
|
@@ -580,7 +580,7 @@ if (!isObject(value) || value instanceof VNode) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-用来判断如果要观测的数据不是一个对象或者是 `VNode` 实例,则直接返回(`return`)。接着定义变量 `ob`,该变量用来保存 `Observer` 实例,可以发现 `observe` 函数的返回值就是 `ob`。紧接着又是一个 `if...else` 分支:
|
|
|
+用来判断如果要观测的数据不是一个对象或者是 `VNode` 实例,则直接 `return` 。接着定义变量 `ob`,该变量用来保存 `Observer` 实例,可以发现 `observe` 函数的返回值就是 `ob`。紧接着又是一个 `if...else` 分支:
|
|
|
|
|
|
```js
|
|
|
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
|
|
@@ -867,7 +867,7 @@ const data = {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-那么字段 `data.a` 和 `data.b` 都将通过闭包引用了属于自己的 `Dep` 实例对象,如下图所示:
|
|
|
+那么字段 `data.a` 和 `data.b` 都将通过闭包引用属于自己的 `Dep` 实例对象,如下图所示:
|
|
|
|
|
|

|
|
|
|
|
@@ -882,7 +882,7 @@ if (property && property.configurable === false) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-首先通过 `Object.getOwnPropertyDescriptor` 函数获取该字段可能已有的属性描述对象,并将该对象保存在 `property` 常量中,接着是一个 `if` 语句块,判断该字段是否是可配置的,如果不可配置(`property.configurable === false`),那么直接返回(`return`),即不会继续执行 `defineReactive` 函数。这么做也是合理的,因为一个不可配置的属性是不能使用也没必要使用 `Object.defineProperty` 改变其属性定义的。
|
|
|
+首先通过 `Object.getOwnPropertyDescriptor` 函数获取该字段可能已有的属性描述对象,并将该对象保存在 `property` 常量中,接着是一个 `if` 语句块,判断该字段是否是可配置的,如果不可配置(`property.configurable === false`),那么直接 `return` ,即不会继续执行 `defineReactive` 函数。这么做也是合理的,因为一个不可配置的属性是不能使用也没必要使用 `Object.defineProperty` 改变其属性定义的。
|
|
|
|
|
|
再往下是这样一段代码:
|
|
|
|
|
@@ -1124,7 +1124,7 @@ if (newVal === value || (newVal !== newVal && value !== value)) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-这里就对比了新值和旧值:`newVal === value`。如果新旧值全等,那么函数直接返回(`return`),不做任何处理。但是除了对比新旧值之外,我们还注意到,另外一个条件:
|
|
|
+这里就对比了新值和旧值:`newVal === value`。如果新旧值全等,那么函数直接 `return`,不做任何处理。但是除了对比新旧值之外,我们还注意到,另外一个条件:
|
|
|
|
|
|
```js
|
|
|
(newVal !== newVal && value !== value)
|
|
@@ -1136,7 +1136,7 @@ if (newVal === value || (newVal !== newVal && value !== value)) {
|
|
|
NaN === NaN // false
|
|
|
```
|
|
|
|
|
|
-所以我们现在重新分析一下这个条件,首先 `value !== value` 成立那说明该属性的原有值就是 `NaN`,同时 `newVal !== newVal` 说明为该属性设置的新值也是 `NaN`,所以这个时候新旧值都是 `NaN`,等价于属性的值没有变化,所以自然不需要做额外的处理了,`set` 函数直接返回(`return`)。
|
|
|
+所以我们现在重新分析一下这个条件,首先 `value !== value` 成立那说明该属性的原有值就是 `NaN`,同时 `newVal !== newVal` 说明为该属性设置的新值也是 `NaN`,所以这个时候新旧值都是 `NaN`,等价于属性的值没有变化,所以自然不需要做额外的处理了,`set` 函数直接 `return` 。
|
|
|
|
|
|
再往下又是一个 `if` 语句块:
|
|
|
|
|
@@ -1235,7 +1235,7 @@ if (!getter && arguments.length === 2) {
|
|
|
|
|
|
在 `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`。
|
|
|
+为什么要这么做呢?具体可以查看这个 [issue](https://github.com/vuejs/vue/pull/7302)。简单的说就是当属性原本存在 `get` 拦截器函数时,在初始化的时候不要触发 `get` 函数,只有当真正的获取该属性的值的时候,再通过调用缓存下来的属性原本的 `getter` 函数取值即可。所以看到这里我们能够发现,如果数据对象的某个属性原本就拥有自己的 `get` 函数,那么这个属性就不会被深度观测,因为当属性原本存在 `getter` 时,是不会触发取值动作的,即 `val = obj[key]` 不会执行,所以 `val` 是 `undefined`,这就导致在后面深度观测的语句中传递给 `observe` 函数的参数是 `undefined`。
|
|
|
|
|
|
举个例子,如下:
|
|
|
|
|
@@ -1473,7 +1473,7 @@ const augment = hasProto
|
|
|
augment(value, arrayMethods, arrayKeys)
|
|
|
```
|
|
|
|
|
|
-当 `hasProto` 为真时,`augment` 引用的就是 `protoAugment` 函数,所以调用 `augment` 函数等价于调用 `protoAugment` 函数,可以看到传递给 `protoAugment` 函数的参数有三个。第一个参数是 `value`,其实就是数组实例本身;第二个参数是 `arrayMethods`,这里的 `arrayMethods` 与我们在截数据变异方法的思路中所讲解的 `arrayMethods` 是一样的,它就是代理原型;第三个参数是 `arrayKeys`,我们可以在 `src/core/observer/array.js` 文件中找到这样一行代码:
|
|
|
+当 `hasProto` 为真时,`augment` 引用的就是 `protoAugment` 函数,所以调用 `augment` 函数等价于调用 `protoAugment` 函数,可以看到传递给 `protoAugment` 函数的参数有三个。第一个参数是 `value`,其实就是数组实例本身;第二个参数是 `arrayMethods`,这里的 `arrayMethods` 与我们在拦截数据变异方法的思路中所讲解的 `arrayMethods` 是一样的,它就是代理原型;第三个参数是 `arrayKeys`,我们可以在 `src/core/observer/array.js` 文件中找到这样一行代码:
|
|
|
|
|
|
```js
|
|
|
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
|
|
@@ -1493,7 +1493,7 @@ arrayKeys = [
|
|
|
]
|
|
|
```
|
|
|
|
|
|
-但实际上 `protoAugment` 函数虽然接收三个参数,但它并没有使用第三个参数。可能有的同学会问为什么 `protoAugment` 函数没有使用第三个参数却依然生命了第三个参数呢?原因是为了让 `flow` 更好的工作。
|
|
|
+但实际上 `protoAugment` 函数虽然接收三个参数,但它并没有使用第三个参数。可能有的同学会问为什么 `protoAugment` 函数没有使用第三个参数却依然声明了第三个参数呢?原因是为了让 `flow` 更好的工作。
|
|
|
|
|
|
我们回到 `protoAugment` 函数,如下:
|
|
|
|
|
@@ -1802,7 +1802,7 @@ const ins = new Vue({
|
|
|
</div>
|
|
|
```
|
|
|
|
|
|
-在模板使用了数据 `{{arr}}`,这将会触发数据对象的 `arr` 属性的 `get` 函数,我们知道 `arr` 属性的 `get` 函数通过闭包引用了两个用来收集依赖的”筐“,一个是属于 `arr` 属性自身的 `dep` 对象,另一个是 `childOb.dep` 对象,其中 `childOb` 就是 `ob1`。这时依赖会被收集到这两个”筐“中,但大家要注意的是 `ob2.dep` 这个”筐“中,是没有收集到依赖的。有的同学会说:”模板中依赖的数据是 `arr`,并不是 `arr` 数组的第一个对象元素,所以 `ob2` 没有收集到依赖很正常啊“,这是一个错误的想法,因为依赖了数组 `arr` 就等价于依赖了数组内的所有元素,数组内所有元素的改变都可以看做是数组的改变。但由于 `ob2` 没有收集到依赖,所以现在就导致如下代码触发不了响应:
|
|
|
+在模板使用了数据 `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)
|