Browse Source

补充讲解 Observer

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

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

@@ -634,6 +634,183 @@ ob = new Observer(value)
 
 #### Observer 的作用
 
+其实真正将数据对象转换成响应式数据的是 `Observer` 函数,它是一个构造函数,同样定义在 `core/observer/index.js` 文件下,如下是简化后的代码:
+
+```js
+export class Observer {
+  value: any;
+  dep: Dep;
+  vmCount: number; // number of vms that has this object as root $data
+
+  constructor (value: any) {
+    // 省略...
+  }
+
+  walk (obj: Object) {
+    // 省略...
+  }
+  
+  observeArray (items: Array<any>) {
+    // 省略...
+  }
+}
+```
+
+可以清晰的看到 `Observer` 类的实例对象将拥有三个实例属性,分别是 `value`、`dep` 和 `vmCount` 以及两个实例方法 `walk` 和 `observeArray`。`Observer` 类的构造函数接收一个参数,即数据对象。下面我们就从 `constructor` 方法开始,研究实例化一个 `Observer` 类时都做了哪些事情。
+
+下面是 `constructor` 方法的全部代码:
+
+```js
+constructor (value: any) {
+  this.value = value
+  this.dep = new Dep()
+  this.vmCount = 0
+  def(value, '__ob__', this)
+  if (Array.isArray(value)) {
+    const augment = hasProto
+      ? protoAugment
+      : copyAugment
+    augment(value, arrayMethods, arrayKeys)
+    this.observeArray(value)
+  } else {
+    this.walk(value)
+  }
+}
+```
+
+`constructor` 方法的参数就是在实例化 `Observer` 实例时传递的参数,即数据对象本身,可以发现,实例对象的 `value` 属性引用了数据对象:
+
+```js
+this.value = value
+```
+
+实例对象的 `dep` 属性,保存了一个新创建的 `Dep` 实例对象:
+
+```js
+this.dep = new Dep()
+```
+
+那么这里的 `Dep` 是什么呢?就像我们在了解数据响应系统基本思路中所讲到的,它就是一个收集依赖的“筐”。但这个“筐”并不属于某一个字段,后面我们会发现,这个框是属于某一个对象或数组的。
+
+实例对象的 `vmCount` 属性被设置为 `0`:`this.vmCount = 0`。
+
+初始化完成三个实例属性之后,使用 `def` 函数,为数据对象定义了一个 `__ob__` 属性,这个属性的值就是当前 `Observer` 实例对象。其中 `def` 函数其实就是 `Object.defineProperty` 函数的简单封装,之所以这里使用 `def` 函数定义 `__ob__` 属性是因为这样可以定义不可枚举的属性,这样后面遍历数据对象的时候就能够防止遍历到 `__ob__` 属性。
+
+假设我们的数据对象如下:
+
+```js
+const data = {
+  a: 1
+}
+```
+
+那么经过 `def` 函数处理之后,`data` 对象应该变成如下这个样子:
+
+```js
+const data = {
+  a: 1,
+  // __ob__ 是不可枚举的属性
+  __ob__: {
+    value: data, // value 属性指向 data 数据对象本身,这是一个循环引用
+    dep: dep实例对象, // new Dep()
+    vmCount: 0
+  }
+}
+```
+
+接着进入一个 `if...else` 判断分支:
+
+```js
+if (Array.isArray(value)) {
+  const augment = hasProto
+    ? protoAugment
+    : copyAugment
+  augment(value, arrayMethods, arrayKeys)
+  this.observeArray(value)
+} else {
+  this.walk(value)
+}
+```
+
+该判断用来区分数据对象到底是数组还是一个纯对象的,因为对于数组和纯对象的处理方式是不同的,为了更好理解我们先看数据对象是一个纯对象的情况,这个时候代码会走 `else` 分支,即执行 `this.walk(value)` 函数,我们知道这个函数实例对象方法,找到这个方法:
+
+```js
+walk (obj: Object) {
+  const keys = Object.keys(obj)
+  for (let i = 0; i < keys.length; i++) {
+    defineReactive(obj, keys[i])
+  }
+}
+```
+
+`walk` 方法很简单,首先使用 `Object.keys(obj)` 获取对象属性所有可枚举的属性,然后使用 `for` 循环遍历这些属性,同时为每个属性调用了 `defineReactive` 函数。那我们就看一看 `defineReactive` 函数都做了什么,该函数也定义在 `core/observer/index.js` 文件,内容如下:
+
+```js
+export function defineReactive (
+  obj: Object,
+  key: string,
+  val: any,
+  customSetter?: ?Function,
+  shallow?: boolean
+) {
+  const dep = new Dep()
+
+  const property = Object.getOwnPropertyDescriptor(obj, key)
+  if (property && property.configurable === false) {
+    return
+  }
+
+  // cater for pre-defined getter/setters
+  const getter = property && property.get
+  const setter = property && property.set
+  if ((!getter || setter) && arguments.length === 2) {
+    val = obj[key]
+  }
+
+  let childOb = !shallow && observe(val)
+  Object.defineProperty(obj, key, {
+    enumerable: true,
+    configurable: true,
+    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
+    },
+    set: function reactiveSetter (newVal) {
+      const value = getter ? getter.call(obj) : val
+      /* eslint-disable no-self-compare */
+      if (newVal === value || (newVal !== newVal && value !== value)) {
+        return
+      }
+      /* eslint-enable no-self-compare */
+      if (process.env.NODE_ENV !== 'production' && customSetter) {
+        customSetter()
+      }
+      if (setter) {
+        setter.call(obj, newVal)
+      } else {
+        val = newVal
+      }
+      childOb = !shallow && observe(newVal)
+      dep.notify()
+    }
+  })
+}
+```
+
+
+
+
+
+
 
 
 

+ 27 - 1
note/附录/core-util.md

@@ -355,7 +355,9 @@ if (capture) return
 
 #### lang.js 文件代码说明
 
-#### isReserved
+
+
+##### isReserved
 
 * 源码如下:
 
@@ -384,6 +386,30 @@ new Vue({
 
 判断一个字符串是否以 `$` 或 `_` 开头还是比较容易的,只不过 `isReserved` 函数的实现方式是通过字符串的 `charCodeAt` 方法获得该字符串第一个字符串的 `unicode`,然后与 `0x24` 和 `0x5F` 作比较。其中 `$` 对应的 `unicode` 码为 `36`,对应的十六进制值为 `0x24`;`_` 对应的 `unicode` 码为 `95`,对应的十六进制值为 `0x5F`。有的同学可能会有疑问为什么不直接用字符 `$` 和 `_` 作比较,而是用这两个字符对应的 `unicode` 码作比较,其实无论哪种比较方法差别不大,看作者更倾向于哪一种。
 
+##### def
+
+* 源码如下:
+
+```js
+/**
+ * Define a property.
+ */
+export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
+  Object.defineProperty(obj, key, {
+    value: val,
+    enumerable: !!enumerable,
+    writable: true,
+    configurable: true
+  })
+}
+```
+
+* 描述:`def` 函数是对 `Object.defineProperty` 函数的简单包装,为了调用方便
+
+* 源码分析:
+
+`def` 函数接收四个参数,分别是 源对象,要在对象上定义的键名,对应的值,以及是否可枚举,如果不传递 `enumerable` 参数则代表定义的属性是不可枚举的。
+
 #### options.js 文件代码说明
 
 *该文件的讲解集中在 [4Vue选项的规范化](/note/4Vue选项的规范化) 以及 [5Vue选项的合并](/note/5Vue选项的合并) 这两个小节中*。