|
@@ -8,7 +8,7 @@
|
|
|
|
|
|
[[toc]]
|
|
|
|
|
|
-相信很多同学都对 `Vue` 的数据响应系统有或多或少的了解,本章将完整的覆盖 `Vue` 响应系统的边边角角,让你对其拥有一个完善的认识。接下来我们还是接着上一章的话题,从 `initState` 函数开始。我们知道 `initState` 函数是很多选项初始化的汇总,在 `initState` 函数内部使用 `initProps` 函数初始化 `props` 属性;使用 `initMethods` 函数初始化 `methods` 属性;使用 `initData` 函数初始化 `data` 选项;使用 `initComputed` 函数和 `initWatch` 函数初始化 `computed` 和 `watch` 选项。那么我们从哪里开始讲起呢?这里我们决定以 `initData` 为切入点为大家讲解 `Vue` 的响应系统,因为 `initData` 几乎涉及了全部的数据响应相关的内容,这样将会让大家在理解 `props`、`computed`、`watch` 等选项时不费吹灰之力,且会有一种水到渠成的感觉。
|
|
|
+相信很多同学都对 `Vue` 的数据响应系统有或多或少的了解,本章将完整地覆盖 `Vue` 响应系统的边边角角,让你对其拥有一个完善的认识。接下来我们还是接着上一章的话题,从 `initState` 函数开始。我们知道 `initState` 函数是很多选项初始化的汇总,在 `initState` 函数内部使用 `initProps` 函数初始化 `props` 属性;使用 `initMethods` 函数初始化 `methods` 属性;使用 `initData` 函数初始化 `data` 选项;使用 `initComputed` 函数和 `initWatch` 函数初始化 `computed` 和 `watch` 选项。那么我们从哪里开始讲起呢?这里我们决定以 `initData` 为切入点为大家讲解 `Vue` 的响应系统,因为 `initData` 几乎涉及了全部的数据响应相关的内容,这样将会让大家在理解 `props`、`computed`、`watch` 等选项时不费吹灰之力,且会有一种水到渠成的感觉。
|
|
|
|
|
|
话不多说,如下是 `initState` 函数中用于初始化 `data` 选项的代码:
|
|
|
|
|
@@ -56,7 +56,7 @@ export function getData (data: Function, vm: Component): any {
|
|
|
|
|
|
`getData` 函数接收两个参数:第一个参数是 `data` 选项,我们知道 `data` 选项是一个函数,第二个参数是 `Vue` 实例对象。`getData` 函数的作用其实就是通过调用 `data` 函数获取真正的数据对象并返回,即:`data.call(vm, vm)`,而且我们注意到 `data.call(vm, vm)` 被包裹在 `try...catch` 语句块中,这是为了捕获 `data` 函数中可能出现的错误。同时如果有错误发生那么则返回一个空对象作为数据对象:`return {}`。
|
|
|
|
|
|
-另外我们注意到在 `getData` 函数的开头调用了 `pushTarget()` 函数,并且在 `finally` 语句块中调用了 `popTarget()`,这么做的目的是什么呢?这么做是为了防止使用 `props` 数据初始化 `data` 数据时收集冗余依赖的,等到我们分析 `Vue` 是如何收集依赖的时候会回头来说明。总之 `getData` 函数的作用就是:**“通过调用 `data` 选项从而获取数据对象”**。
|
|
|
+另外我们注意到在 `getData` 函数的开头调用了 `pushTarget()` 函数,并且在 `finally` 语句块中调用了 `popTarget()`,这么做的目的是什么呢?这么做是为了防止使用 `props` 数据初始化 `data` 数据时收集冗余的依赖,等到我们分析 `Vue` 是如何收集依赖的时候会回头来说明。总之 `getData` 函数的作用就是:**“通过调用 `data` 选项从而获取数据对象”**。
|
|
|
|
|
|
我们再回到 `initData` 函数中:
|
|
|
|
|
@@ -151,7 +151,7 @@ ins.a // 1
|
|
|
ins.b // function
|
|
|
```
|
|
|
|
|
|
-在这个例子中无论是定义在 `data` 数据对象,还是定义在 `methods` 对象中的函数,都可以通过实例对象代理访问。所以当 `data` 数据对象中的 `key` 与 `methods` 对象中的 `key` 冲突时,岂不就会产生覆盖掉的现象,所以为了避免覆盖 `Vue` 是不允许在 `methods` 中定义与 `data` 字段的 `key` 重名的函数的。而这个工作就是在 `while` 循环中第一个语句块中的代码去完成的。
|
|
|
+在这个例子中无论是定义在 `data` 中的数据对象,还是定义在 `methods` 对象中的函数,都可以通过实例对象代理访问。所以当 `data` 数据对象中的 `key` 与 `methods` 对象中的 `key` 冲突时,岂不就会产生覆盖掉的现象,所以为了避免覆盖 `Vue` 是不允许在 `methods` 中定义与 `data` 字段的 `key` 重名的函数的。而这个工作就是在 `while` 循环中第一个语句块中的代码去完成的。
|
|
|
|
|
|
接着我们看 `while` 循环中的第二个 `if` 语句块:
|
|
|
|
|
@@ -167,7 +167,7 @@ if (props && hasOwn(props, key)) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-同样的 `Vue` 实例对象除了代理访问 `data` 数据和 `methods` 中的方法之外,还代理访问了 `props` 中的数据,所以上面这段代码的作用是如果发现 `data` 数据字段的 `key` 已经在 `props` 中有定义了,那么就会打印警告。另外这里有一个优先级的关系:**props优先级 > data优先级 > methods优先级**。即如果一个 `key` 在 `props` 中有定义了那么就不能在 `data` 中出现;如果一个 `key` 在 `data` 中出现了那么就不能在 `methods` 中出现了。
|
|
|
+同样的 `Vue` 实例对象除了代理访问 `data` 数据和 `methods` 中的方法之外,还代理访问了 `props` 中的数据,所以上面这段代码的作用是如果发现 `data` 数据字段的 `key` 已经在 `props` 中有定义了,那么就会打印警告。另外这里有一个优先级的关系:**props优先级 > data优先级 > methods优先级**。即如果一个 `key` 在 `props` 中有定义了那么就不能在 `data` 和 `methods` 中出现了;如果一个 `key` 在 `data` 中出现了那么就不能在 `methods` 中出现了。
|
|
|
|
|
|
另外上面的代码中当 `if` 语句的条件不成立,则会判断 `else if` 语句中的条件:`!isReserved(key)`,该条件的意思是判断定义在 `data` 中的 `key` 是否是保留键,大家可以在 [core/util 目录下的工具方法全解](../appendix/core-util.md) 中查看对于 `isReserved` 函数的讲解。`isReserved` 函数通过判断一个字符串的第一个字符是不是 `$` 或 `_` 来决定其是否是保留的,`Vue` 是不会代理那些键名以 `$` 或 `_` 开头的字段的,因为 `Vue` 自身的属性和方法都是以 `$` 或 `_` 开头的,所以这么做是为了避免与 `Vue` 自身的属性和方法相冲突。
|
|
|
|
|
@@ -210,7 +210,7 @@ const ins = new Vue ({
|
|
|
observe(data, true /* asRootData */)
|
|
|
```
|
|
|
|
|
|
-调用 `observe` 函数将 `data` 数据对象转换成响应式的,可以说这句代码才是响应系统的开始,不过在我们讲解 `observe` 函数之前我们有必要总结一下 `initData` 函数所做的事情,通过前面分析 `initData` 函数主要完成如下工作:
|
|
|
+调用 `observe` 函数将 `data` 数据对象转换成响应式的,可以说这句代码才是响应系统的开始,不过在讲解 `observe` 函数之前我们有必要总结一下 `initData` 函数所做的事情,通过前面的分析可知 `initData` 函数主要完成如下工作:
|
|
|
|
|
|
* 根据 `vm.$options.data` 选项获取真正想要的数据(注意:此时 `vm.$options.data` 是函数)
|
|
|
* 校验得到的数据是否是一个纯对象
|
|
@@ -221,7 +221,7 @@ observe(data, true /* asRootData */)
|
|
|
|
|
|
## 数据响应系统的基本思路
|
|
|
|
|
|
-接下来我们将重点讲解数据响应系统的实现,在具体到源码之前我们有必要了解一下数据响应系统实现的基本思路,这有助于我们更好的理解源码的目的,毕竟每一行代码都有它存在的意义。
|
|
|
+接下来我们将重点讲解数据响应系统的实现,在具体到源码之前我们有必要了解一下数据响应系统实现的基本思路,这有助于我们更好地理解源码的目的,毕竟每一行代码都有它存在的意义。
|
|
|
|
|
|
在 `Vue` 中,我们可以使用 `$watch` 观测一个字段,当字段的值发生变化的时候执行指定的观察者,如下:
|
|
|
|
|
@@ -274,7 +274,7 @@ Object.defineProperty(data, 'a', {
|
|
|
})
|
|
|
```
|
|
|
|
|
|
-这样我们就实现了对属性 `a` 的设置和获取操作的拦截,有了它我们就可以大胆的思考一些事情,比如: **能不能在获取属性 `a` 的时候收集依赖,然后在设置属性 `a` 的时候触发之前收集的依赖呢?** 嗯,这是一个好思路,不过既然要收集依赖,我们起码需要一个”筐“,然后将所有收集到的依赖通通放到这个”筐”里,当属性被设置的时候将“筐”里所有的依赖都拿出来执行就可以了,落实到代码如下:
|
|
|
+这样我们就实现了对属性 `a` 的设置和获取操作的拦截,有了它我们就可以大胆地思考一些事情,比如: **能不能在获取属性 `a` 的时候收集依赖,然后在设置属性 `a` 的时候触发之前收集的依赖呢?** 嗯,这是一个好思路,不过既然要收集依赖,我们起码需要一个”筐“,然后将所有收集到的依赖通通放到这个”筐”里,当属性被设置的时候将“筐”里所有的依赖都拿出来执行就可以了,落实到代码如下:
|
|
|
|
|
|
```js
|
|
|
// dep 数组就是我们所谓的“筐”
|
|
@@ -293,7 +293,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', () => {
|
|
@@ -479,7 +479,7 @@ function $watch (exp, fn) {
|
|
|
|
|
|
我们对 `$watch` 函数做了一些改造,首先检查要读取的字段是否包含 `.`,如果包含 `.` 说明读取嵌套对象的字段,这时候我们使用字符串的 `split('.')` 函数将字符串转为数组,所以如果访问的路径是 `a.b` 那么转换后的数组就是 `['a', 'b']`,然后使用一个循环从而读取到嵌套对象的属性值,不过需要注意的是读取到嵌套对象的属性值之后应该立即 `return`,不需要再执行后面的代码。
|
|
|
|
|
|
-下面我们再进一步,我们思考一下 `$watch` 函数的原理的是什么?其实 `$watch` 函数所做的事情就是想方设法的访问到你要观测的字段,从而触发该字段的 `get` 函数,进而收集依赖(观察者)。现在我们传递给 `$watch` 函数的第一个参数是一个字符串,代表要访问数据的哪一个字段属性,那么除了字符串之外可不可以是一个函数呢?假设我们有一个函数叫做 `render`,如下
|
|
|
+下面我们再进一步,我们思考一下 `$watch` 函数的原理是什么?其实 `$watch` 函数所做的事情就是想方设法地访问到你要观测的字段,从而触发该字段的 `get` 函数,进而收集依赖(观察者)。现在我们传递给 `$watch` 函数的第一个参数是一个字符串,代表要访问数据的哪一个字段属性,那么除了字符串之外可不可以是一个函数呢?假设我们有一个函数叫做 `render`,如下
|
|
|
|
|
|
```js
|
|
|
const data = {
|
|
@@ -694,7 +694,7 @@ this.value = value
|
|
|
this.dep = new Dep()
|
|
|
```
|
|
|
|
|
|
-那么这里的 `Dep` 是什么呢?就像我们在了解数据响应系统基本思路中所讲到的,它就是一个收集依赖的“筐”。但这个“筐”并不属于某一个字段,后面我们会发现,这个框是属于某一个对象或数组的。
|
|
|
+那么这里的 `Dep` 是什么呢?就像我们在 `了解数据响应系统基本思路` 中所讲到的,它就是一个收集依赖的“筐”。但这个“筐”并不属于某一个字段,后面我们会发现,这个筐是属于某一个对象或数组的。
|
|
|
|
|
|
实例对象的 `vmCount` 属性被设置为 `0`:`this.vmCount = 0`。
|
|
|
|
|
@@ -738,7 +738,7 @@ if (Array.isArray(value)) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-该判断用来区分数据对象到底是数组还是一个纯对象的,因为对于数组和纯对象的处理方式是不同的,为了更好理解我们先看数据对象是一个纯对象的情况,这个时候代码会走 `else` 分支,即执行 `this.walk(value)` 函数,我们知道这个函数实例对象方法,找到这个方法:
|
|
|
+该判断用来区分数据对象到底是数组还是一个纯对象,因为对于数组和纯对象的处理方式是不同的,为了更好地理解我们先看数据对象是一个纯对象的情况,这个时候代码会走 `else` 分支,即执行 `this.walk(value)` 函数,我们知道这个函数实例对象方法,找到这个方法:
|
|
|
|
|
|
```js
|
|
|
walk (obj: Object) {
|
|
@@ -791,7 +791,7 @@ export function defineReactive (
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-`defineReactive` 函数的核心就是将**数据对象的数据属性转换为访问器属性**,即为数据对象的属性设置一对 `getter/setter`,但其中做了很多处理边界条件的工作。`defineReactive` 接收五个参数,但是在 `walk` 方法中调用 `defineReactive` 函数时只传递了前两个参数,即数据对象和属性的键名。我们看一下 `defineReactive` 的函数体,首先定义了 `dep` 常量,它是一个 `Dep` 实例对象:
|
|
|
+`defineReactive` 函数的核心就是 **将数据对象的数据属性转换为访问器属性**,即为数据对象的属性设置一对 `getter/setter`,但其中做了很多处理边界条件的工作。`defineReactive` 接收五个参数,但是在 `walk` 方法中调用 `defineReactive` 函数时只传递了前两个参数,即数据对象和属性的键名。我们看一下 `defineReactive` 的函数体,首先定义了 `dep` 常量,它是一个 `Dep` 实例对象:
|
|
|
|
|
|
```js
|
|
|
const dep = new Dep()
|
|
@@ -852,7 +852,7 @@ export function defineReactive (
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-如上面的代码中注释所写的那样,在访问器属性的 `getter/setter` 中,通过闭包引用了前面定义的“筐”,即 `dep` 常量。这里大家要明确一件事情,即**每一个数据字段都通过闭包引用着属于自己的 `dep` 常量**。因为在 `walk` 函数中通过循环遍历了所有数据对象的属性,并调用 `defineReactive` 函数,所以每次调用 `defineReactive` 定义访问器属性时,该属性的 `setter/getter` 都闭包引用了一个属于自己的“筐”。假设我们有如下数据字段:
|
|
|
+如上面的代码中注释所写的那样,在访问器属性的 `getter/setter` 中,通过闭包引用了前面定义的“筐”,即 `dep` 常量。这里大家要明确一件事情,即 **每一个数据字段都通过闭包引用着属于自己的 `dep` 常量**。因为在 `walk` 函数中通过循环遍历了所有数据对象的属性,并调用 `defineReactive` 函数,所以每次调用 `defineReactive` 定义访问器属性时,该属性的 `setter/getter` 都闭包引用了一个属于自己的“筐”。假设我们有如下数据字段:
|
|
|
|
|
|
```js
|
|
|
const data = {
|
|
@@ -984,15 +984,15 @@ get: function reactiveGetter () {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-既然是 `getter`,那么当然要能够正确的返回属性的值才能,我们知道依赖的收集时机就是属性被读取的时候,所以 `get` 函数做了两件事:正确的返回属性值以及收集依赖,我们具体看一下代码,`get` 函数的第一句代码如下:
|
|
|
+既然是 `getter`,那么当然要能够正确地返回属性的值才行,我们知道依赖的收集时机就是属性被读取的时候,所以 `get` 函数做了两件事:正确地返回属性值以及收集依赖,我们具体看一下代码,`get` 函数的第一句代码如下:
|
|
|
|
|
|
```js
|
|
|
const value = getter ? getter.call(obj) : val
|
|
|
```
|
|
|
|
|
|
-首先判断是否存在 `getter`,我们知道 `getter` 常量中保存的属性原型的 `get` 函数,如果 `getter` 存在那么直接调用该函数,并以该函数的返回值作为属性的值,保证属性的原有读取操作正常运作。如果 `getter` 不存在则使用 `val` 作为属性的值。可以发现 `get` 函数的最后一句将 `value` 常量返回,这样 `get` 函数需要做的第一件事就完成了,即正确的返回属性值。
|
|
|
+首先判断是否存在 `getter`,我们知道 `getter` 常量中保存的是属性原型的 `get` 函数,如果 `getter` 存在那么直接调用该函数,并以该函数的返回值作为属性的值,保证属性的原有读取操作正常运作。如果 `getter` 不存在则使用 `val` 作为属性的值。可以发现 `get` 函数的最后一句将 `value` 常量返回,这样 `get` 函数需要做的第一件事就完成了,即正确地返回属性值。
|
|
|
|
|
|
-除了正确的返回属性值,还要收集依赖,而处于 `get` 函数第一行和最后一行代码中间的所有代码都是用来完成收集依赖这件事儿的,下面我们就看一下它是如何收集依赖的,由于我们还没有讲解过 `Dep` 这个类,所以现在大家可以简单的认为 `dep.depend()` 这句代码的执行就意味着依赖被收集了。接下来我们仔细看一下代码:
|
|
|
+除了正确地返回属性值,还要收集依赖,而处于 `get` 函数第一行和最后一行代码中间的所有代码都是用来完成收集依赖这件事儿的,下面我们就看一下它是如何收集依赖的,由于我们还没有讲解过 `Dep` 这个类,所以现在大家可以简单的认为 `dep.depend()` 这句代码的执行就意味着依赖被收集了。接下来我们仔细看一下代码:
|
|
|
|
|
|
```js
|
|
|
if (Dep.target) {
|
|
@@ -1006,7 +1006,7 @@ if (Dep.target) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-首先判断 `Dep.target` 是否存在,那么 `Dep.target` 是什么呢?其实 `Dep.target` 与我们在数据响应系统基本思路一节中所讲的 `Target` 作用相同,所以 `Dep.target` 中保存的值就是要被收集的依赖(观察者)。所以如果 `Dep.target` 存在的话说明有依赖需要被收集,这个时候才需要执行 `if` 语句块内的代码,如果 `Dep.target` 不存在就意味着没有需要被收集的依赖,所以当然就不需要执行 `if` 语句块内的代码了。
|
|
|
+首先判断 `Dep.target` 是否存在,那么 `Dep.target` 是什么呢?其实 `Dep.target` 与我们在 `数据响应系统基本思路` 一节中所讲的 `Target` 作用相同,所以 `Dep.target` 中保存的值就是要被收集的依赖(观察者)。所以如果 `Dep.target` 存在的话说明有依赖需要被收集,这个时候才需要执行 `if` 语句块内的代码,如果 `Dep.target` 不存在就意味着没有需要被收集的依赖,所以当然就不需要执行 `if` 语句块内的代码了。
|
|
|
|
|
|
在 `if` 语句块内第一句执行的代码就是:`dep.depend()`,执行 `dep` 对象的 `depend` 方法将依赖收集到 `dep` 这个“筐”中,这里的 `dep` 对象就是属性的 `getter/setter` 通过闭包引用的“筐”。
|
|
|
|
|
@@ -1032,7 +1032,7 @@ const data = {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-对于属性 `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` 这里”筐“里,为什么要将同样的依赖分别收集到这两个不同的”筐“里呢?其实答案就在于这两个”筐“里收集的依赖的触发时机是不同的,即作用不同,两个”筐“如下:
|
|
|
+对于属性 `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`
|
|
@@ -1100,7 +1100,7 @@ set: function reactiveSetter (newVal) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-与 `get` 函数类似,我们知道 `get` 函数主要完成了两部分重要的工作,一个是返回正确的属性值,另一个是收集依赖。同样的 `set` 函数也要完成两个重要的事情,第一正确的为属性设置新值,第二是能够触发相应的依赖。
|
|
|
+我们知道 `get` 函数主要完成了两部分重要的工作,一个是返回正确的属性值,另一个是收集依赖。与 `get` 函数类似, `set` 函数也要完成两个重要的事情,第一正确地为属性设置新值,第二是能够触发相应的依赖。
|
|
|
|
|
|
首先 `set` 函数接收一个参数 `newVal`,即该属性被设置的新值。在函数体内,先执行了这样一句话:
|
|
|
|
|
@@ -1149,7 +1149,7 @@ defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () =
|
|
|
}, true)
|
|
|
```
|
|
|
|
|
|
-上面的代码中使用 `defineReactive` 在 `Vue` 实例对象 `vm` 上定义了 `$attrs` 属性,可以看到传递给 `defineReactive` 函数的第四个参数是一个箭头函数,这个函数就是 `customSetter`,这个箭头函数的作用是当你尝试修改 `vm.$attrs` 属性的值时,打印一段信息即:**`$attrs` 属性是只读的**。这就是 `customSetter` 函数的作用,用来打印辅助信息,当然除此之外你可以将 `customSetter` 用在任何适合使用它的地方。
|
|
|
+上面的代码中使用 `defineReactive` 在 `Vue` 实例对象 `vm` 上定义了 `$attrs` 属性,可以看到传递给 `defineReactive` 函数的第四个参数是一个箭头函数,这个函数就是 `customSetter`,这个箭头函数的作用是当你尝试修改 `vm.$attrs` 属性的值时,打印一段信息:**`$attrs` 属性是只读的**。这就是 `customSetter` 函数的作用,用来打印辅助信息,当然除此之外你可以将 `customSetter` 用在任何适合使用它的地方。
|
|
|
|
|
|
我们回到 `set` 函数,再往下是这样一段代码:
|
|
|
|
|
@@ -1161,7 +1161,7 @@ if (setter) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-上面这段代码的意图很明显,即正确的设置属性值,首先判断 `setter` 是否存在,我们知道 `setter` 常量存储的是属性原有的 `set` 函数。即如果属性原来拥有自身的 `set` 函数,那么应该继续使用该函数来设置属性的值,从而保证属性原有的设置操作不受影响。如果属性原本就没有 `set` 函数,那么就设置 `val` 的值:`val = newVal`。
|
|
|
+上面这段代码的意图很明显,即正确地设置属性值,首先判断 `setter` 是否存在,我们知道 `setter` 常量存储的是属性原有的 `set` 函数。即如果属性原来拥有自身的 `set` 函数,那么应该继续使用该函数来设置属性的值,从而保证属性原有的设置操作不受影响。如果属性原本就没有 `set` 函数,那么就设置 `val` 的值:`val = newVal`。
|
|
|
|
|
|
接下来就是 `set` 函数的最后两句代码,如下:
|
|
|
|
|
@@ -1170,7 +1170,7 @@ childOb = !shallow && observe(newVal)
|
|
|
dep.notify()
|
|
|
```
|
|
|
|
|
|
-我们知道,由于属性被设置了新的值,那么假如我们为属性设置的新值是一个数组或者纯对象,那么该数组或纯对象是未被观测的,所以需要对新值进行观测,这就是第一句代码的作用,同时使用新的观测对象重写 `childOb` 的值。当然了,这些操作都是在 `!shallow` 为真的情况下,即需要深度观测的时候才会执行。最后是时候触发依赖了,我们知道 `dep` 是属性用来收集依赖的”筐“,现在我们需要把”筐“里的依赖都执行以下,而这就是 `dep.notify()` 的作用。
|
|
|
+我们知道,由于属性被设置了新的值,那么假如我们为属性设置的新值是一个数组或者纯对象,那么该数组或纯对象是未被观测的,所以需要对新值进行观测,这就是第一句代码的作用,同时使用新的观测对象重写 `childOb` 的值。当然了,这些操作都是在 `!shallow` 为真的情况下,即需要深度观测的时候才会执行。最后是时候触发依赖了,我们知道 `dep` 是属性用来收集依赖的”筐“,现在我们需要把”筐“里的依赖都执行一下,而这就是 `dep.notify()` 的作用。
|
|
|
|
|
|
至此 `set` 函数我们就讲解完毕了。
|
|
|
|
|
@@ -1184,7 +1184,7 @@ if ((!getter || setter) && arguments.length === 2) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-在之前的讲解中,我们没有详细的讲解如上代码所示的这段 `if` 语句块。该 `if` 语句有两个条件:
|
|
|
+在之前的讲解中,我们没有详细地讲解如上代码所示的这段 `if` 语句块。该 `if` 语句有两个条件:
|
|
|
|
|
|
* 第一:`(!getter || setter)`
|
|
|
* 第二:`arguments.length === 2`
|
|
@@ -1276,7 +1276,7 @@ const ins = new Vue({
|
|
|
|
|
|
我们仅仅修改了定义数据对象 `data` 的方式,此时 `data.getterProp` 本身已经是一个访问器属性,且拥有 `get` 方法。此时当我们尝试修改 `getterProp.a` 的值时,在 `watch` 中观察 `getterProp.a` 的函数不会被执行。这是因为属性 `getterProp` 是一个拥有 `get` 拦截器函数的访问器属性,而当 `Vue` 发现该属性拥有原本的 `getter` 时,是不会深度观测的。
|
|
|
|
|
|
-那么为什么当属性拥有自己的 `getter` 时就不会对其深度观测了呢?有两方面的原因,第一:由于当属性存在原本的 `getter` 时在深度观测之前不会取值,所以在在深度观测语句执行之前取不到属性值从而无法深度观测。第二:之所以在深度观测之前不取值是因为属性原本的 `getter` 由用户定义,用户可能在 `getter` 中做任何意想不到的事情,这么做是出于避免引发不可预见行为的考虑。
|
|
|
+那么为什么当属性拥有自己的 `getter` 时就不会对其深度观测了呢?有两方面的原因,第一:由于当属性存在原本的 `getter` 时在深度观测之前不会取值,所以在深度观测语句执行之前取不到属性值从而无法深度观测。第二:之所以在深度观测之前不取值是因为属性原本的 `getter` 由用户定义,用户可能在 `getter` 中做任何意想不到的事情,这么做是出于避免引发不可预见行为的考虑。
|
|
|
|
|
|
我们回过头来再看这段 `if` 语句块:
|
|
|
|
|
@@ -1288,7 +1288,7 @@ if (!getter && arguments.length === 2) {
|
|
|
|
|
|
这么做难道不会有什么问题吗?当然有问题,我们知道当数据对象的某一个属性只拥有 `get` 拦截器函数而没有 `set` 拦截器函数时,此时该属性不会被深度观测。但是经过 `defineReactive` 函数的处理之后,该属性将被重新定义 `getter` 和 `setter`,此时该属性变成了既拥有 `get` 函数又拥有 `set` 函数。并且当我们尝试给该属性重新赋值时,那么新的值将会被观测。这时候矛盾就产生了:**原本该属性不会被深度观测,但是重新赋值之后,新的值却被观测了**。
|
|
|
|
|
|
-这就是所谓的**定义响应式数据时行为的不一致**,为了解决这个问题,采用的办法是当属性拥有原本的 `setter` 时,即使拥有 `getter` 也要获取属性值并观测之,这样代码就变成了最终这个样子:
|
|
|
+这就是所谓的 **定义响应式数据时行为的不一致**,为了解决这个问题,采用的办法是当属性拥有原本的 `setter` 时,即使拥有 `getter` 也要获取属性值并观测之,这样代码就变成了最终这个样子:
|
|
|
|
|
|
```js
|
|
|
if ((!getter || setter) && arguments.length === 2) {
|
|
@@ -1334,7 +1334,7 @@ sayHello = function () {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-看,这样就完美的实现了我们的需求,首先使用 `originalSayHello` 变量缓存原来的 `sayHello` 函数,然后重新定义 `sayHello` 函数,并在新定义的 `sayHello` 函数中调用缓存下来的 `originalSayHello`。这样我们就保证了在不改变 `sayHello` 函数行为的前提现对其进行了功能扩展。
|
|
|
+看,这样就完美地实现了我们的需求,首先使用 `originalSayHello` 变量缓存原来的 `sayHello` 函数,然后重新定义 `sayHello` 函数,并在新定义的 `sayHello` 函数中调用缓存下来的 `originalSayHello`。这样我们就保证了在不改变 `sayHello` 函数行为的前提下对其进行了功能扩展。
|
|
|
|
|
|
这其实是一个很通用也很常见的技巧,而 `Vue` 正是通过这个技巧实现了对数据变异方法的拦截,即保持数组变异方法原有功能不变的前提下对其进行功能扩展。我们知道数组实例的变异方法是来自于数组构造函数的原型,如下图:
|
|
|
|
|
@@ -1488,7 +1488,7 @@ arrayKeys = [
|
|
|
]
|
|
|
```
|
|
|
|
|
|
-但实际上 `protoAugment` 函数虽然接收三个参数,但它并没有使用第三个参数。可能有的同学会问为什么 `protoAugment` 函数没有使用第三个参数却依然声明了第三个参数呢?原因是为了让 `flow` 更好的工作。
|
|
|
+但实际上 `protoAugment` 函数虽然接收三个参数,但它并没有使用第三个参数。可能有的同学会问为什么 `protoAugment` 函数没有使用第三个参数却依然声明了第三个参数呢?原因是为了让 `flow` 更好地工作。
|
|
|
|
|
|
我们回到 `protoAugment` 函数,如下:
|
|
|
|
|
@@ -1584,7 +1584,7 @@ methodsToPatch.forEach(function (method) {
|
|
|
def(arrayMethods, method, function mutator (...args) {
|
|
|
const result = original.apply(this, args)
|
|
|
const ob = this.__ob__
|
|
|
-
|
|
|
+
|
|
|
// 省略中间部分...
|
|
|
|
|
|
// notify change
|
|
@@ -1612,7 +1612,7 @@ const result = original.apply(this, args)
|
|
|
|
|
|
```js
|
|
|
const ob = this.__ob__
|
|
|
-
|
|
|
+
|
|
|
// 省略中间部分...
|
|
|
|
|
|
// notify change
|
|
@@ -1641,7 +1641,7 @@ def(arrayMethods, method, function mutator (...args) {
|
|
|
})
|
|
|
```
|
|
|
|
|
|
-首先我们需要思考一下数组变异方法对数组的影响是什么?无非是**增加元素**、**删除元素**以及**变更元素顺序**。有的同学可能会说还有**替换元素**,实际上替换可以理解为删除和增加的复合操作。那么在这些变更中,我们需要重点关注的是**增加元素**的操作,即 `push`、`unshift` 和 `splice`,这三个变异方法都可以为数组添加新的元素,那么为什么要重点关注呢?原因很简单,因为新增加的元素是非响应式的,所以我们需要获取到这些新元素,并将其变为响应式数据才行,而这就是上面代码的目的。下面我们看一下具体实现,首先定义了 `inserted` 变量,这个变量用来保存那些被新添加进来的数组元素:`let inserted`。接着是一个 `switch` 语句,在 `switch` 语句中,当遇到 `push` 和 `unshift` 操作时,那么新增的元素实际上就是传递给这两个方法的参数,所以可以直接将 `inserted` 的值设置为 `args`:`inserted = args`。当遇到 `splice` 操作时,我们知道 `splice` 函数从第三个参数开始到最后一个参数都是数组的新增元素,所以直接使用 `args.slice(2)` 作为 `inserted` 的值即可。最后 `inserted` 变量中所保存的就是新增的数组元素,我们只需要调用 `observeArray` 函数对其进行观测即可:
|
|
|
+首先我们需要思考一下数组变异方法对数组的影响是什么?无非是 **增加元素**、**删除元素** 以及 **变更元素顺序**。有的同学可能会说还有 **替换元素**,实际上替换可以理解为删除和增加的复合操作。那么在这些变更中,我们需要重点关注的是 **增加元素** 的操作,即 `push`、`unshift` 和 `splice`,这三个变异方法都可以为数组添加新的元素,那么为什么要重点关注呢?原因很简单,因为新增加的元素是非响应式的,所以我们需要获取到这些新元素,并将其变为响应式数据才行,而这就是上面代码的目的。下面我们看一下具体实现,首先定义了 `inserted` 变量,这个变量用来保存那些被新添加进来的数组元素:`let inserted`。接着是一个 `switch` 语句,在 `switch` 语句中,当遇到 `push` 和 `unshift` 操作时,那么新增的元素实际上就是传递给这两个方法的参数,所以可以直接将 `inserted` 的值设置为 `args`:`inserted = args`。当遇到 `splice` 操作时,我们知道 `splice` 函数从第三个参数开始到最后一个参数都是数组的新增元素,所以直接使用 `args.slice(2)` 作为 `inserted` 的值即可。最后 `inserted` 变量中所保存的就是新增的数组元素,我们只需要调用 `observeArray` 函数对其进行观测即可:
|
|
|
|
|
|
```js
|
|
|
if (inserted) ob.observeArray(inserted)
|
|
@@ -1667,7 +1667,7 @@ function copyAugment (target: Object, src: Object, keys: Array<string>) {
|
|
|
|
|
|
我们知道 `copyAugment` 函数的第三个参数 `keys` 就是定义在 `arrayMethods` 对象上的所有函数的键,即所有要拦截的数组变异方法的名称。这样通过 `for` 循环对其进行遍历,并使用 `def` 函数在数组实例上定义与数组变异方法同名的且不可枚举的函数,这样就实现了拦截操作。
|
|
|
|
|
|
-总之无论是 `protoAugment` 函数还是 `copyAugment` 函数,他们的目的只有一个:**把数组实例与代理原型或与代理原型中定义的函数联系起来,从而拦截数组变异方法**。下面我们在回到 `Observer` 类的 `constructor` 函数中,看如下代码:
|
|
|
+总之无论是 `protoAugment` 函数还是 `copyAugment` 函数,他们的目的只有一个:**把数组实例与代理原型或与代理原型中定义的函数联系起来,从而拦截数组变异方法**。下面我们再回到 `Observer` 类的 `constructor` 函数中,看如下代码:
|
|
|
|
|
|
```js
|
|
|
if (Array.isArray(value)) {
|
|
@@ -1778,7 +1778,7 @@ const ins = new Vue({
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-数据对象中的 `arr` 属性是一个数组,并且数组的一个元素是另外一个对象。我们 [被观测后的数据对象的样子](#被观测后的数据对象的样子) 一节中讲过了,上面的对象在经过观测后将变成如下这个样子:
|
|
|
+数据对象中的 `arr` 属性是一个数组,并且数组的一个元素是另外一个对象。我们在 [被观测后的数据对象的样子](#被观测后的数据对象的样子) 一节中讲过了,上面的对象在经过观测后将变成如下这个样子:
|
|
|
|
|
|
```js {3-4}
|
|
|
{
|
|
@@ -1797,7 +1797,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)
|
|
@@ -1819,7 +1819,7 @@ function dependArray (value: Array<any>) {
|
|
|
|
|
|
当被读取的数据对象的属性值是数组时,会调用 `dependArray` 函数,该函数将通过 `for` 循环遍历数组,并取得数组每一个元素的值,如果该元素的值拥有 `__ob__` 对象和 `__ob__.dep` 对象,那说明该元素也是一个对象或数组,此时只需要手动执行 `__ob__.dep.depend()` 即可达到收集依赖的目的。同时如果发现数组的元素仍然是一个数组,那么需要递归调用 `dependArray` 继续收集依赖。
|
|
|
|
|
|
-那么为什么数组需要这样处理,而纯对象不需要呢?那是因为**数组的索引是非响应式的**。现在我们已经知道了数据响应系统对纯对象和数组的处理方式是不同,对于纯对象只需要逐个将对象的属性重定义为访问器属性,并且当属性的值同样为纯对象时进行递归定义即可,而对于数组的处理则是通过拦截数组变异方法的方式,也就是说如下代码是触发不了响应的:
|
|
|
+那么为什么数组需要这样处理,而纯对象不需要呢?那是因为 **数组的索引是非响应式的**。现在我们已经知道了数据响应系统对纯对象和数组的处理方式是不同,对于纯对象只需要逐个将对象的属性重新定义为访问器属性,并且当属性的值同样为纯对象时进行递归定义即可,而对于数组的处理则是通过拦截数组变异方法的方式,也就是说如下代码是触发不了响应的:
|
|
|
|
|
|
```js {7}
|
|
|
const ins = new Vue({
|
|
@@ -1835,7 +1835,7 @@ ins.arr[0] = 3 // 不能触发响应
|
|
|
|
|
|
## Vue.set($set) 和 Vue.delete($delete) 的实现
|
|
|
|
|
|
-现在我们是时候讲解一下 `Vue.set` 和 `Vue.delete` 函数的实现了,我们知道 `Vue` 数据响应系统的原理的核心是通过 `Object.defineProperty` 函数将数据对象的属性转换为访问器属性,从而使得我们能够拦截到属性的读取和设置,但正如官方文档中介绍的那样,`Vue` 是没有能力拦截到为一个对象(或数组)添加属性(或元素)的,而 `Vue.set` 和 `Vue.delete` 就是为了解决这个问题而诞生的。同时为了方便使用 `Vue` 还在实例对象上定义了 `$set` 和 `$delete` 方法,实际上 `$set` 和 `$delete` 方法仅仅是 `Vue.set` 和 `Vue.delete` 的别名,为了证明这点,我们首先来看看 `$set` 和 `$delete` 的实现,还记得 `$set` 和 `$delete` 方法定义在哪里吗?不记得也没关系,我们可以通过查看附录 [Vue 构造函数整理-原型](/appendix/vue-prototype.html) 找到 `$set` 和 `$delete` 方法的定义位置,我们发现 `$set` 和 `$delete` 定义在 `src/core/instance/state.js` 文件的 `stateMixin` 函数中,如下代码:
|
|
|
+现在我们是时候讲解一下 `Vue.set` 和 `Vue.delete` 函数的实现了,我们知道 `Vue` 数据响应系统的原理的核心是通过 `Object.defineProperty` 函数将数据对象的属性转换为访问器属性,从而使得我们能够拦截到属性的读取和设置,但正如官方文档中介绍的那样,`Vue` 是没有能力拦截到为一个对象(或数组)添加属性(或元素)的,而 `Vue.set` 和 `Vue.delete` 就是为了解决这个问题而诞生的。同时为了方便使用, `Vue` 还在实例对象上定义了 `$set` 和 `$delete` 方法,实际上 `$set` 和 `$delete` 方法仅仅是 `Vue.set` 和 `Vue.delete` 的别名,为了证明这点,我们首先来看看 `$set` 和 `$delete` 的实现,还记得 `$set` 和 `$delete` 方法定义在哪里吗?不记得也没关系,我们可以通过查看附录 [Vue 构造函数整理-原型](../appendix/vue-prototype.md) 找到 `$set` 和 `$delete` 方法的定义位置,我们发现 `$set` 和 `$delete` 定义在 `src/core/instance/state.js` 文件的 `stateMixin` 函数中,如下代码:
|
|
|
|
|
|
```js {4-5}
|
|
|
export function stateMixin (Vue: Class<Component>) {
|
|
@@ -1856,7 +1856,7 @@ export function stateMixin (Vue: Class<Component>) {
|
|
|
|
|
|
可以看到 `$set` 和 `$delete` 的值分别是是 `set` 和 `del`,根据文件头部的引用关系可知 `set` 和 `del` 来自 `src/core/observer/index.js` 文件中定义的 `set` 函数和 `del` 函数。
|
|
|
|
|
|
-接着我们再来看看 `Vue.set` 和 `Vue.delete` 函数的定义,如果你同样不记得这两个函数时在哪里定义的也没关系,可以查看附录 [Vue 构造函数整理-全局API](/appendix/vue-global-api.html),我们发现这两个函数是在 `initGlobalAPI` 函数中定义的,打开 `src/core/global-api/index.js` 文件,找到 `initGlobalAPI` 函数如下:
|
|
|
+接着我们再来看看 `Vue.set` 和 `Vue.delete` 函数的定义,如果你同样不记得这两个函数时在哪里定义的也没关系,可以查看附录 [Vue 构造函数整理-全局API](../appendix/vue-global-api.md),我们发现这两个函数是在 `initGlobalAPI` 函数中定义的,打开 `src/core/global-api/index.js` 文件,找到 `initGlobalAPI` 函数如下:
|
|
|
|
|
|
```js {4,5}
|
|
|
export function initGlobalAPI (Vue: GlobalAPI) {
|
|
@@ -1893,7 +1893,7 @@ if (process.env.NODE_ENV !== 'production' &&
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-该 `if` 语句块的判断条件中包含两个函数,分别是 `isUndef` 和 `isPrimitive`,可以在附录 [shared/util.js 文件工具方法全解](/appendix/shared-util.html) 中找到关于这两个函数的讲解。`isUndef` 函数用来判断一个值是否是 `undefined` 或 `null`,如果是则返回 `true`,`isPrimitive` 函数用来判断一个值是否是原始类型值,如果是则返回 `true`。所以如上代码 `if` 语句块的作用是:**如果 `set` 函数的第一个参数是 `undefined` 或 `null` 或者是原始类型值,那么在非生产环境下会打印警告信息**。这么做是合理的,因为理论上只能为对象(或数组)添加属性(或元素)。
|
|
|
+该 `if` 语句块的判断条件中包含两个函数,分别是 `isUndef` 和 `isPrimitive`,可以在附录 [shared/util.js 文件工具方法全解](../appendix/shared-util.md) 中找到关于这两个函数的讲解。`isUndef` 函数用来判断一个值是否是 `undefined` 或 `null`,如果是则返回 `true`,`isPrimitive` 函数用来判断一个值是否是原始类型值,如果是则返回 `true`。所以如上代码 `if` 语句块的作用是:**如果 `set` 函数的第一个参数是 `undefined` 或 `null` 或者是原始类型值,那么在非生产环境下会打印警告信息**。这么做是合理的,因为理论上只能为对象(或数组)添加属性(或元素)。
|
|
|
|
|
|
紧接着又是一段 `if` 语句块,如下:
|
|
|
|
|
@@ -1905,7 +1905,7 @@ if (Array.isArray(target) && isValidArrayIndex(key)) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-这段代码对 `target` 和 `key` 这两个参数做了校验,如果 `target` 是一个数组,并且 `key` 是一个有效的数组索引,那么就会执行 `if` 语句块的内容。在校验 `key` 是否是有效的数组索引时使用了 `isValidArrayIndex` 函数,可以在附录 [shared/util.js 文件工具方法全解](/appendix/shared-util.html) 中查看详细讲解。也就是说当我们尝试使用 `Vue.set/$set` 为数组设置某个元素值的时候就会执行 `if` 语句块的内容,如下例子:
|
|
|
+这段代码对 `target` 和 `key` 这两个参数做了校验,如果 `target` 是一个数组,并且 `key` 是一个有效的数组索引,那么就会执行 `if` 语句块的内容。在校验 `key` 是否是有效的数组索引时使用了 `isValidArrayIndex` 函数,可以在附录 [shared/util.js 文件工具方法全解](../appendix/shared-util.md) 中查看详细讲解。也就是说当我们尝试使用 `Vue.set/$set` 为数组设置某个元素值的时候就会执行 `if` 语句块的内容,如下例子:
|
|
|
|
|
|
```js {3,7}
|
|
|
const ins = new Vue({
|
|
@@ -1918,7 +1918,7 @@ ins.$data.arr[0] = 3 // 不能触发响应
|
|
|
ins.$set(ins.$data.arr, 0, 3) // 能够触发响应
|
|
|
```
|
|
|
|
|
|
-上面的代码中我们直接修改 `arr[0]` 的值不不能够触发响应的,但是如果我们使用 `$set` 函数重新设置 `arr` 数组索引为 `0` 的元素的值,这样是能够触发响应的,我们看看 `$set` 函数是如何实现的,注意如下高亮代码:
|
|
|
+上面的代码中我们直接修改 `arr[0]` 的值是不能够触发响应的,但是如果我们使用 `$set` 函数重新设置 `arr` 数组索引为 `0` 的元素的值,这样是能够触发响应的,我们看看 `$set` 函数是如何实现的,注意如下高亮代码:
|
|
|
|
|
|
```js {2-4}
|
|
|
if (Array.isArray(target) && isValidArrayIndex(key)) {
|
|
@@ -2034,7 +2034,7 @@ export function observe (value: any, asRootData: ?boolean): Observer | void {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-`observe` 函数接收两个参数,第二个参数指示着被观测的数据对象是否是根数据对象,什么叫根数据对象呢?那就看 `asRootData` 什么时候为 `true` 即可了,我们找到 `initData` 函数中,他在 `src/core/instance/state.js` 文件中,如下:
|
|
|
+`observe` 函数接收两个参数,第二个参数指示着被观测的数据对象是否是根数据对象,什么叫根数据对象呢?那就看 `asRootData` 什么时候为 `true` 即可,我们找到 `initData` 函数中,他在 `src/core/instance/state.js` 文件中,如下:
|
|
|
|
|
|
```js {10}
|
|
|
function initData (vm: Component) {
|
|
@@ -2062,7 +2062,7 @@ export function observe (value: any, asRootData: ?boolean): Observer | void {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-可以发现,根数据对象将有用一个特质,即 `target.__ob__.vmCount > 0`,这样条件 `(ob && ob.vmCount)` 是成立的,也就是说:**当使用 `Vue.set/$set` 函数为根数据对象添加属性时,是不被允许的**。
|
|
|
+可以发现,根数据对象将拥有一个特质,即 `target.__ob__.vmCount > 0`,这样条件 `(ob && ob.vmCount)` 是成立的,也就是说:**当使用 `Vue.set/$set` 函数为根数据对象添加属性时,是不被允许的**。
|
|
|
|
|
|
那么为什么不允许在根数据对象上添加属性呢?因为这样做是永远触发不了依赖的。原因就是根数据对象的 `Observer` 实例收集不到依赖(观察者),如下:
|
|
|
|
|
@@ -2118,7 +2118,7 @@ if (Array.isArray(target) && isValidArrayIndex(key)) {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-很显然,如果我们使用 `Vue.delete/$delete` 去删除一个数组的索引时,如上这段代码将被执行,当然了前提是参数 `key` 需要是一个有效的数组索引。与为数组添加元素类似,移除数组元素同样使用了数组的 `splice` 方法,大家知道这样是能够触发响应的。
|
|
|
+很显然,如果我们使用 `Vue.delete/$delete` 去删除一个数组的索引,如上这段代码将被执行,当然了前提是参数 `key` 需要是一个有效的数组索引。与为数组添加元素类似,移除数组元素同样使用了数组的 `splice` 方法,大家知道这样是能够触发响应的。
|
|
|
|
|
|
再往下是如下这段 `if` 语句块:
|
|
|
|
|
@@ -2151,4 +2151,3 @@ ob.dep.notify()
|
|
|
首先使用 `hasOwn` 函数检测 `key` 是否是 `target` 对象自身拥有的属性,如果不是那么直接返回(`return`)。很好理解,如果你将要删除的属性原本就不在该对象上,那么自然什么都不需要做。
|
|
|
|
|
|
如果 `key` 存在于 `target` 对象上,那么代码将继续运行,此时将使用 `delete` 语句从 `target` 上删除属性 `key`。最后判断 `ob` 对象是否存在,如果不存在说明 `target` 对象原本就不是响应的,所以直接返回(`return`)即可。如果 `ob` 对象存在,说明 `target` 对象是响应的,需要触发响应才行,即执行 `ob.dep.notify()`。
|
|
|
-
|