Browse Source

讲解数据被观测的条件

HcySunYang 7 years ago
parent
commit
10f9065e0a
2 changed files with 149 additions and 2 deletions
  1. 106 2
      note/7Vue的初始化之数据响应系统.md
  2. 43 0
      note/附录/core-util.md

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

@@ -525,11 +525,115 @@ function $watch (exp, fn) {
 $watch(render, render)
 ```
 
-第二个参数依然是 `render` 函数,也就是说当依赖发生变化时,会重新执行 `render` 函数,这样我们就实现了数据变化,并将变化自动应用到 `DOM`。其实这大概就是 `Vue` 的原理,但我们做的还远远不够,比如上面这句代码,第一个参数中 `render` 函数的执行使得我们能够收集依赖,当依赖变化时会重新执行第二个参数中的 `render` 函数,但不要忘了这又会触发一次数据字段的 `get` 拦截器,所以此时已经收集了两遍依赖,那么我们是不是要想办法避免依赖冗余呢?我们将这些问题留到后面,看看在 `Vue` 中它是如何处理的。
+第二个参数依然是 `render` 函数,也就是说当依赖发生变化时,会重新执行 `render` 函数,这样我们就实现了数据变化,并将变化自动应用到 `DOM`。其实这大概就是 `Vue` 的原理,但我们做的还远远不够,比如上面这句代码,第一个参数中 `render` 函数的执行使得我们能够收集依赖,当依赖变化时会重新执行第二个参数中的 `render` 函数,但不要忘了这又会触发一次数据字段的 `get` 拦截器,所以此时已经收集了两遍依赖,那么我们是不是要想办法避免收集冗余的依赖呢?除此之外我么也没有对数组做处理,我们将这些问题留到后面,看看在 `Vue` 中它是如何处理的。
 
 现在我们这个不严谨的实现暂时就到这里,意图在于让大家明白数据响应系统的整体思路,为接下来真正进入 `Vue` 源码做必要的铺垫。
 
-#### 看看访问器属性的模样
+#### observe 工厂函数
+
+了解了数据响应系统的基本思路,我们是时候回过头来深入研究 `Vue` 的数据响应系统是如何实现的了,我们回到 `initData` 函数的最后一句代码:
+
+```js
+// observe data
+observe(data, true /* asRootData */)
+```
+
+调用了 `observe` 函数观测数据,`observe` 函数来自于 `core/observer/index.js` 文件,打开该文件找到 `observe` 函数:
+
+```js
+export function observe (value: any, asRootData: ?boolean): Observer | void {
+  if (!isObject(value) || value instanceof VNode) {
+    return
+  }
+  let ob: Observer | void
+  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+    ob = value.__ob__
+  } else if (
+    shouldObserve &&
+    !isServerRendering() &&
+    (Array.isArray(value) || isPlainObject(value)) &&
+    Object.isExtensible(value) &&
+    !value._isVue
+  ) {
+    ob = new Observer(value)
+  }
+  if (asRootData && ob) {
+    ob.vmCount++
+  }
+  return ob
+}
+```
+
+如上是 `observe` 函数的全部代码, `observe` 函数接收两个参数,第一个参数是要观测的数据,第二个参数是一个布尔值,代表将要被观测的数据是否是根级数据。在 `observe` 函数的一开始是一段 `if` 判断语句:
+
+```js
+if (!isObject(value) || value instanceof VNode) {
+  return
+}
+```
+
+用来判断如果要观测的数据不是一个对象或者是 `VNode` 实例,则直接返回(`return`)。接着定义变量 `ob`,该变量用来保存 `Observer` 实例,可以发现 `observe` 函数的返回值就是 `ob`。紧接着又是一个 `if...else` 分支:
+
+```js
+if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+  ob = value.__ob__
+} else if (
+  shouldObserve &&
+  !isServerRendering() &&
+  (Array.isArray(value) || isPlainObject(value)) &&
+  Object.isExtensible(value) &&
+  !value._isVue
+) {
+  ob = new Observer(value)
+}
+```
+
+我们先看 `if` 分支的判断条件,首先使用 `hasOwn` 函数检测数据对象 `value` 自身是否含有 `__ob__` 属性,并且 `__ob__` 属性应该是 `Observer` 的实例。如果为真则直接将数据对象自身的 `__ob__` 属性的值作为 `ob` 的值:`ob = value.__ob__`。那么 `__ob__` 是什么呢?其实当一个数据对象被观测之后将会在该对象上定义 `__ob__` 属性,所以 `if` 分支的作用是用来避免重复观测一个数据对象。
+
+接着我们再来看看 `else...if` 分支,如果数据对象上没有定义 `__ob__` 属性,那么说明该对象没有被观测过,进而会判断 `else...if` 分支,如果 `else...if` 分支的条件为真,那么会执行 `ob = new Observer(value)` 对数据对象进行观测。也就是说只有当数据对象满足所有 `else...if` 分支的条件才会被观测,我们看看需要满足什么条件:
+
+* 第一个条件是 `shouldObserve` 必须为 `true`
+
+`shouldObserve` 变量也定义在 `core/observer/index.js` 文件内,如下:
+
+```js
+/**
+ * In some cases we may want to disable observation inside a component's
+ * update computation.
+ */
+export let shouldObserve: boolean = true
+
+export function toggleObserving (value: boolean) {
+  shouldObserve = value
+}
+```
+
+该变量的初始值为 `true`,在 `shouldObserve` 变量的下面定义了 `toggleObserving` 函数,该函数接收一个布尔值参数,用来切换 `shouldObserve` 变量的真假值,我们可以把 `shouldObserve` 想象成一个开关,为 `true` 时说明打开了开关,此时可以对数据进行观测,为 `false` 时可以理解为关闭了开关,此时数据对象将不会被观测。为什么这么设计呢?原因是有一些场景下确实需要这个开关从而达到一些目的,后面我们遇到的时候再仔细来说。
+
+* 第二个条件是 `!isServerRendering()` 必须为真
+
+`isServerRendering()` 函数的返回值是一个布尔值,用来判断是否是服务端渲染。也就是说只有当不是服务端渲染的时候才会观测数据。这里我们留下一个疑问:为什么服务端渲染时不对数据进行观测?对于这个问题后面我们会讲到。
+
+* 第三个条件是 `(Array.isArray(value) || isPlainObject(value))` 必须为真
+
+这个条件很好理解,只有当数据对象是数组或纯对象的时候,才有必要对其进行观测。
+
+* 第四个条件是 `Object.isExtensible(value)` 必须为真
+
+也就是说要被观测的数据对象必须是**可扩展的**。一个普通的对象默认就是可扩展的,一下三个方法都可以使得一个对象变得不可扩展:`Object.preventExtensions()`、`Object.freeze()` 以及 `Object.seal()`。
+
+* 第五个条件是 `!value._isVue` 必须为真
+
+我们知道 `Vue` 实例对象拥有 `_isVue` 属性,所以这个条件用来避免 `Vue` 实例对象被观测。
+
+当一个对象满足了以上五个条件时,就会执行 `else...if` 语句块的代码,即创建一个 `Observer` 实例:
+
+```js
+ob = new Observer(value)
+```
+
+#### Observer 的作用
+
 
 
 

+ 43 - 0
note/附录/core-util.md

@@ -80,6 +80,49 @@ export const nativeWatch = ({}).watch
 
 * 描述:在 `Firefox` 中原生提供了 `Object.prototype.watch` 函数,所以当运行在 `Firefox` 中时 `nativeWatch` 为原生提供的函数,在其他浏览器中 `nativeWatch` 为 `undefined`。这个变量主要用于 `Vue` 处理 `watch` 选项时与其冲突。
 
+##### isServerRendering
+
+源码如下:
+
+```js
+// this needs to be lazy-evaled because vue may be required before
+// vue-server-renderer can set VUE_ENV
+let _isServer
+export const isServerRendering = () => {
+  if (_isServer === undefined) {
+    /* istanbul ignore if */
+    if (!inBrowser && !inWeex && typeof global !== 'undefined') {
+      // detect presence of vue-server-renderer and avoid
+      // Webpack shimming the process
+      _isServer = global['process'].env.VUE_ENV === 'server'
+    } else {
+      _isServer = false
+    }
+  }
+  return _isServer
+}
+```
+
+* 描述:`isServerRendering` 函数的执行结果是一个布尔值,用来判断是否是服务端渲染。
+
+* 源码解析:
+
+根据 `if` 语句:
+
+```js
+if (!inBrowser && !inWeex && typeof global !== 'undefined') {...}
+```
+
+可知如果不在浏览器中(`!inBrowser`)也不是weex(`!inWeex`),同时 `global` 有定义,则可能是服务端渲染,那么继续判断:
+
+```js
+global['process'].env.VUE_ENV === 'server'
+```
+
+是否成立,其中 `global['process'.env.VUE_ENV]` 是 `vue-server-renderer` 注入的。如果成立那么说明是服务端渲染。如果上面的条件有一项不成立,那么都不认为是服务端渲染。
+
+注意,在 `isServerRendering` 中使用全局变量 `_isServer` 保存了最终的值,如果发现 `_isServer` 有定义,那么就不会重新计算,从而提升性能。毕竟环境是不会改变的,只需要求值一次即可。
+
 #### error.js 文件代码说明
 
 该文件只导出一个函数:`handleError`,在看这个函数的实现之前,我们需要回顾一下 `Vue` 的文档,我们知道 `Vue` 提供了一个全局配置 `errorHandler`,用来捕获组件生命周期函数等的内部错误,使用方法如下: