手抄Vue(四)—— 封装Observer类

Vue.js 中,将数据对象转化为响应式数据的是 Observer 构造函数。我准备结合前面几篇已经整理出来的思路,实现一个自己的 Observer

为了让代码结构更加清晰,同时考虑到可复用性,我先从前面几篇已有的实现中抽一些功能较为独立的代码出来:

  • defineReactive 方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    function defineReactive(obj, key) {
    const dep = []
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get () {
        dep.push(target)
        return value
      },
      set (newVal) {
        if (newVal === value) return
        value = newVal
        dep.forEach(f => {
          f()
        })
      }
    })
    }
    

    该方法用来将数据对象 obj 上的数据属性 key 转化为响应式属性。

dep 是“依赖收集器”,属性 keygetter setter 都通过闭包引用着自己的 deptarget 仍然作为全局变量存在,中转依赖以帮助 getter 收集依赖。setter 会执行对应 getter 收集到的所有依赖,但如果发现设置的值与原值无异,则直接 return,什么也不做。

这是直接从 手抄Vue(一)—— 简单实现数据响应 里拿过来的代码,但如果要封装一个功能完善、可复用性高的方法的话,肯定还要考虑一些边界条件与异常场景,比如,如果传递进来的属性本来就是不可配置的?这时就得加个判断:

1
2
3
4
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && !property.configurable) {
  return
}

首先获取到对象 obj 上属性 key 的属性描述符对象,然后进行判断,如果属性描述符对象存在,并且该属性本来就不可配置,那么直接 return

再比如,如果传进来的属性本来就有 getter setter 函数对 ?那就要把原来的 getter setter 缓存起来,在新定义的 getter 里除却收集依赖这项工作以外,还要将缓存起来的 getter 执行并将结果返回。同样,在新定义的 setter 里,除去执行依赖的工作以外,还要将设置的新值 newVal 与缓存的 getter 执行之后得到的值比较,如果相等则直接 return,什么都不做。并且要将缓存起来的 setter 执行一遍,以替代原来的赋值操作 value = newVal

反映至代码即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function defineReactive(obj, key) {
  const dep = []

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && !property.configurable) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  let value = obj[key]
  Object.defineProperty(obj, key, {
    get () {
      getter && (value = getter.call(obj))
      dep.push(target)
      return value
    },
    set (newVal) {
      getter && (value = getter.call(obj))
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        value = newVal
      }
      dep.forEach(f => {
        f()
      })
    }
  })
}

上面有这么一句:

1
2
3
if (newVal === value || (newVal !== newVal && value !== value)) {
  return
}

其实本来是这样的:

1
2
3
if (newVal === value) {
  return
}

但是考虑到 NaN 的情况:

1
NaN === NaN // false

这会导致:

1
newVal === value // false

所以应该在判断条件中加上:

1
newVal !== newVal && value !== value

利用 NaN 与自身不相等的特性判断出 NaN,最后就成了:

1
newVal === value || (newVal !== newVal && value !== value)

值得注意的是:

1
2
3
Infinity === Infinity // true
-Infinity === -Infinity // true
1 / 0 === 2 / 0 // true
  • walk 方法
    1
    2
    3
    4
    5
    6
    
    function walk(obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
    }
    

    该方法用于遍历数据对象 obj 的每一个属性,同时调用之前定义的 defineReactive 方法,将遍历到的属性转化为响应式属性。

  • hasProto
    1
    
    const hasProto = '__proto__' in {}
    

    该变量用于判断浏览器是否支持 __proto__ 属性。

  • arrayMethods 对象 ```js const mutationMethods = [ ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’ ] const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto)

mutationMethods.forEach(method => { arrayMethods[method] = function (…args) { const result = arrayProto[method].apply(this, args) console.log(我截获了对数组的${method}操作) return result } })

1
2
3
4
5
6
7
8
9
10
11
12
该对象用于代理数组的变异方法以实现拦截。

* `def` 方法
```js
function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

该方法是 Object.defineProperty 的简单封装,用于定义一个属性,可以控制该属性是否可枚举。

  • protoAugment 方法
    1
    2
    3
    
    function protoAugment(target, src) {
    target.__proto__ = src
    }
    

    该方法用于在浏览器支持 __proto__ 属性时,通过修改原型链,让 __proto__ 指向 src,来增强目标对象或数组。

  • copyAugment 方法
    1
    2
    3
    4
    5
    6
    
    function copyAugment(target, src, keys) {
    for (let i = 0, l = keys.length; i < l; i++) {
      const key = keys[i]
      def(target, key, src[key])
    }
    }
    

    该方法用来遍历 keys,并在目标对象 target 上定义不可枚举的属性,该属性的键为 keys 中的元素,值为该元素在 src 中对应的属性值。

  • isPlainObject 方法
    1
    2
    3
    
    function isPlainObject(obj) {
    return Object.prototype.toString.call(obj) === '[object Object]'
    }
    

    该方法用于判断给定的变量是否为纯对象。

有了以上这些方法和属性之后,Observer 类也就应运而生了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Observer {
  constructor (value) {
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, mutationMethods)
    } else {
      this.walk(value)
    }
  }
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

但现在有两个问题,一个是这个类没有实现深度观测,再一个是没有对调用 Observer 时传进来的参数做检测,以防止传进来 undefined null 100 'kobe' 等等不能被观测的数据类型。并且我希望调用 Observer 的时候传进来的只能是数组或者纯对象。综合这些因素,再封装一层出来会比较好:

1
2
3
4
5
function observe(value) {
  if (Array.isArray(value) || isPlainObject(value)) {
    return new Observer(value)
  }
}

observe 会判断给定的 value 如果是数组或者纯对象的话再去 new 出来 Observer,并将结果返回。

有了 observe,深度观测就可以这样来实现:在 defineReactive 方法中,对给定的 obj[key] 以及 setter 中的 newVal 调用 observe 方法进行观测,因为这两者都可能是数组或者纯对象,如果不是,observe 方法内部已经统一做了判断,外部调用时无需特殊处理。即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function defineReactive(obj, key) {
  const dep = []

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && !property.configurable) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  let value = obj[key]
  // 这里
  observe(value)
  Object.defineProperty(obj, key, {
    get () {
      getter && (value = getter.call(obj))
      dep.push(target)
      return value
    },
    set (newVal) {
      getter && (value = getter.call(obj))
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        value = newVal
      }
      // 这里
      observe(newVal)
      dep.forEach(f => {
        f()
      })
    }
  })
}

但其实发现还有一个问题,现在数组、纯对象以及纯对象内嵌套数组、纯对象内嵌套纯对象这几种情形都已经实现了(深度)观测,但数组内嵌套纯对象以及数组内嵌套数组还没有实现,所以要再写这么一个方法:

1
2
3
4
5
function observeArray(items) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

该方法用来遍历给定的数组,即 items,再分别对每一个元素 items[i] 执行 observe 方法,即可对数组里面的嵌套情形进行深度观测。同时 Observer 类要做以下改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Observer {
  constructor (value) {
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, mutationMethods)
      // 二
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // 一
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

注释一的地方,给 Observer 类添加一个实例方法,也就是我刚写的 observeArray

注释二的地方,调用 observeArray 方法,并将数组 value 作为参数传入。

那么最终,代码就是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
const mutationMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

const hasProto = '__proto__' in {}
function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
}

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

function defineReactive(obj, key) {
  const dep = []

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && !property.configurable) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  let value = obj[key]
  observe(value)
  Object.defineProperty(obj, key, {
    get () {
      getter && (value = getter.call(obj))
      dep.push(target)
      return value
    },
    set (newVal) {
      getter && (value = getter.call(obj))
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        value = newVal
      }
      observe(newVal)
      dep.forEach(f => {
        f()
      })
    }
  })
}

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
mutationMethods.forEach(method => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args)
    console.log(`我截获了对数组的${method}操作`)
    return result
  }
})

function observe(value) {
  if (Array.isArray(value) || isPlainObject(value)) {
    return new Observer(value)
  }
}

class Observer {
  constructor (value) {
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, mutationMethods)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

function protoAugment(target, src) {
  target.__proto__ = src
}
function copyAugment(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

function myWatch(exp, fn) {
  target = fn
  if (typeof exp === 'function') {
    exp()
    return
  }
  let pathArr,
      obj = data
  if (/\./.test(exp)) {
    pathArr = exp.split('.')
    pathArr.forEach(p => {
      target = fn
      obj = obj[p]
    })
    return
  }
  data[exp]
}

添加以下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const data = {
  name: 'kobe bryant',
  otherInfo: {
    height: 198,
    numbers: [8, 24]
  },
  teammates: [
    'paul gasol',
    {
      name: 'shaq',
      numbers: [32, 34, 33]
    }
  ]
}

function render() {
  document.body.innerText = `我最喜欢的NBA球员是${data.name},他身高${data.otherInfo.height}cm,穿过${data.otherInfo.numbers.length}个球衣号码,${data.otherInfo.numbers[0]}${data.otherInfo.numbers[1]},他的队友有${data.teammates[0]}${data.teammates[1].name},其中,${data.teammates[1].name}在湖人时期穿的球衣号码为${data.teammates[1].numbers[1]}号`
}

observe(data)
myWatch(render, render)

data.name = 'michael'
data.otherInfo.height = 198.1
data.otherInfo.numbers.push(23)
data.teammates[1].name = 'scott pippen'
data.teammates[1].numbers.push(33)

执行以后发现,无论嵌套关系如何对属性的赋值操作均触发了 render 函数,对两个数组data.otherInfo.numbersdata.teammates[1].numberspush 操作也执行了扩展的功能即打印 '我截获了对数组的push操作'这句信息。但是数组的 push 操作没有触发页面重新渲染,这是因为对数组变异方法的整个代理过程中没有收集依赖也没有触发依赖,这个问题先留下,等我写到 Dep 类的时候再回过头来写这个问题。但其实站在这篇博客的角度来看,Observer 类的封装就算是初步完成了。