Browse Source

响应系统的基本思路完成

HcySunYang 7 years ago
parent
commit
1c3d7c12db
1 changed files with 90 additions and 3 deletions
  1. 90 3
      note/7Vue的初始化之数据响应系统.md

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

@@ -319,15 +319,15 @@ Object.defineProperty(data, 'a', {
 
 // Target 是全局变量
 let Target = null
-function $watch (path, fn) {
+function $watch (exp, fn) {
   // 将 Target 的值设置为 fn
   Target = fn
   // 读取字段值,触发 set 函数
-  data[path]
+  data[exp]
 }
 ```
 
-上面的代码中,首先我们定义了全局变量 `Target`,然后在 `$watch` 中将 `Target` 的值设置为 `fn` 也就是依赖,接着读取字段的值 `data[path]` 从而触发 `set` 函数,在 `set` 函数中,由于此时 `Target` 变量就是我们要收集的依赖,所以将 `Target` 添加到 `dep` 数组。现在我们添加如下测试代码:
+上面的代码中,首先我们定义了全局变量 `Target`,然后在 `$watch` 中将 `Target` 的值设置为 `fn` 也就是依赖,接着读取字段的值 `data[exp]` 从而触发 `set` 函数,在 `set` 函数中,由于此时 `Target` 变量就是我们要收集的依赖,所以将 `Target` 添加到 `dep` 数组。现在我们添加如下测试代码:
 
 ```js
 $watch('a', () => {
@@ -438,9 +438,96 @@ $watch('a.b', () => {
 })
 ```
 
+来看看目前 `$watch` 函数的代码:
 
+```js
+function $watch (exp, fn) {
+  Target = fn
+  // 读取字段值,触发 set 函数
+  data[exp]
+}
+```
+
+读取字段值的时候我们直接使用 `data[exp]`,如果按照 `$watch('a.b', fn)` 这样调用 `$watch` 函数,那么 `data[exp]` 等价于 `data['a.b']`,这显然是不正确的,正确的读取字段值的方式应该是 `data['a']['b']`。所以我们需要稍微做一点小小的改造:
+
+```js
+const data = {
+  a: {
+    b: 1
+  }
+}
+
+function $watch (exp, fn) {
+  Target = fn
+  let pathArr,
+      obj = data
+  // 检查 exp 中是否包含 .
+  if (/\./.test(exp)) {
+    // 将字符串转为数组,例:'a.b' => ['a', 'b']
+    pathArr = exp.split('.')
+    // 使用循环读取到 data.a.b
+    pathArr.forEach(p => {
+      obj = obj[p]
+    })
+    return
+  }
+  data[exp]
+}
+```
+
+我们对 `$watch` 函数做了一些改造,首先检查要读取的字段是否包含 `.`,如果包含 `.` 说明读取嵌套对象的字段,这时候我们使用字符串的 `split('.')` 函数将字符串转为数组,所以如果访问的路径是 `a.b` 那么转换后的数组就是 `['a', 'b']`,然后使用一个循环从而读取到嵌套对象的属性值,不过需要注意的是读取到嵌套对象的属性值之后应该立即返回 `return`,不需要再执行后面的代码。
+
+下面我们再进一步,我们思考一下 `$watch` 函数的原理的是什么?其实 `$watch` 函数所做的事情就是想方设法的访问到你要观测的字段,从而触发该字段的 `get` 函数,进而收集观察者(依赖)。现在我们传递给 `$watch` 函数的第一个参数是一个字符串,代表要访问数据的哪一个字段属性,那么除了字符串之外可以不可以是一个函数呢?假设我们有一个函数叫做 `render`,如下
+
+```js
+const data = {
+  name: '霍春阳',
+  age: 24
+}
+
+function render () {
+  return document.write(`姓名:${data.name}; 年龄:${data.age}`)
+}
+```
+
+可以看到 `render` 函数依赖了数据对象 `data`,那么 `render` 函数的执行是不是会触发 `data.name` 和 `data.age` 这两个字段的 `get` 拦截器呢?答案是肯定的,当然会!所以我们可以将 `render` 函数作为 `$watch` 函数的第一个参数:
+
+```js
+$watch(render, render)
+```
+
+为了能够保证 `$watch` 函数正常执行,我们需要对 `$watch` 函数做如下修改:
+
+```js
+function $watch (exp, fn) {
+  Target = fn
+  let pathArr,
+      obj = data
+  // 如果 exp 是函数,直接执行该函数
+  if (typeof exp === 'function') {
+    exp()
+    return
+  }
+  if (/\./.test(exp)) {
+    pathArr = exp.split('.')
+    pathArr.forEach(p => {
+      obj = obj[p]
+    })
+    return
+  }
+  data[exp]
+}
+```
+
+在上面的代码中,我们检测了 `exp` 的类型,如果是函数则直接执行之,由于 `render` 函数的执行会触发数据字段的 `get` 拦截器,所以依赖会被收集。同时我们要注意传递给 `$watch` 函数的第二个参数:
+
+```js
+$watch(render, render)
+```
 
+第二个参数依然是 `render` 函数,也就是说当依赖发生变化时,会重新执行 `render` 函数,这样我们就实现了数据变化,并将变化自动应用到 `DOM`。其实这大概就是 `Vue` 的原理,但我们做的还远远不够,比如上面这句代码,第一个参数中 `render` 函数的执行使得我们能够收集依赖,当依赖变化时会重新执行第二个参数中的 `render` 函数,但不要忘了这又会触发一次数据字段的 `get` 拦截器,所以此时已经收集了两遍依赖,那么我们是不是要想办法避免依赖冗余呢?我们将这些问题留到后面,看看在 `Vue` 中它是如何处理的。
 
+现在我们这个不严谨的实现暂时就到这里,意图在于让大家明白数据响应系统的整体思路,为接下来真正进入 `Vue` 源码做必要的铺垫。
 
 #### 看看访问器属性的模样