手写实现简易版vue的响应式原理(data, watch, computed)

给实习生们布置了这个作业,此处作个记录。
代码方面只实现基本的思想,并不是和vue源码一模一样的结构,为的是更好的理解响应式原理

实现范围:

  1. data:data内所有属性都绑好的get、set方法,有多层的话,递归完成多层
  2. watch:data任一个属性改变都会触发对应watch的监听函数
  3. computed:computed内的属性的data属性依赖改变后,computed的属性值会重新获取,否则读取缓存

目录结构:

  1. 实现的源代码
  2. 测试用例

实现的源代码

目录结构

  1. ├── index.html
  2. └── vue2
  3. └── index.js

./index.html(使用效果)

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <script src="./vue2/index.js"></script>
  7. </head>
  8. <body>
  9. 打开控制台
  10. <script>
  11. window.vm = new Vue2({
  12. el: '#app',
  13. data: {
  14. x: 1,
  15. y: 2,
  16. z: 3,
  17. a: {
  18. b: 4,
  19. c: 5
  20. }
  21. },
  22. watch: {
  23. x (newVal, oldVal) {
  24. console.log('我是watch的回调函数', newVal, oldVal)
  25. }
  26. },
  27. computed: {
  28. C_x () {
  29. console.log('我是computed的回调函数')
  30. return this.x
  31. }
  32. }
  33. })
  34. </script>
  35. </body>
  36. </html>

./vue2/index.js (实现细节,请看注释

  1. function Vue2(options) {
  2. this.$options = options
  3. initWatch.call(this, options.watch)
  4. initComputed.call(this, options.computed)
  5. initData.call(this, options.data)
  6. }
  7. function initWatch (data) {
  8. this.watcherList = data
  9. this.val = null
  10. }
  11. function initComputed (data) {
  12. this._computedWatchers = {} // 保存computed对应key的缓存
  13. this._computedAndDataMap = {} // 保存computed对应key 和其回调函数内的 this.xxdata 的映射关系 (如果this.xxdata变更了, 清空对应的this._computedWatchers的缓存)
  14. this._currentComputedKey = null // 保存computed对应key. 为了上面this._computedAndDataMap能拿到依赖关系
  15. Object.keys(data).forEach(key => {
  16. Object.defineProperty(this, key, {
  17. configurable: true,
  18. enumerable: true,
  19. get: function() {
  20. this._currentComputedKey = key
  21. if (!this._computedWatchers[key]) this._computedWatchers[key] = data[key].call(this)
  22. this._currentComputedKey = null
  23. return this._computedWatchers[key]
  24. },
  25. set: function (n) {
  26. console.log('computed值不能修改')
  27. }
  28. })
  29. })
  30. }
  31. function initData (data) {
  32. for (let key in data){
  33. let temp; // 利用了闭包, 保存了私有的变量 方便get和set的操作, 会常驻内存
  34. // 如果 data[key] 是一个对象, 递归重写 setter getter
  35. if(typeof data[key] === 'object'){
  36. temp = new Object()
  37. initData.call(temp, data[key]) // 递归 用Object.defineProperty监听data
  38. } else {
  39. temp = data[key]
  40. }
  41. // 将 data 中的数据直接绑定在 vue 的实例上, 好处是可以 this.x 调用数据了.
  42. Object.defineProperty(this, key, {
  43. configurable: true,
  44. enumerable: true,
  45. get: function() {
  46. console.log(key, '的get函数被执行了')
  47. if (this._currentComputedKey) { // 如果是computed内的回调函数像拿值, 做一个记录, 记录下当前key 对应哪个computed内的属性
  48. this._computedAndDataMap[key] = this._currentComputedKey
  49. }
  50. return temp
  51. },
  52. set: function (n) {
  53. console.log(key, '的----set-----函数被执行了')
  54. const watcher = this.watcherList[key] // 如果被watch监听了, 执行watch内的回调函数
  55. if (watcher) {
  56. watcher(n, this.val) // newVal 和 oldVal
  57. }
  58. temp = n
  59. this._computedWatchers[this._computedAndDataMap[key]] = null // 因为这个依赖改变了, computed对应的值也要刷新
  60. this.val = n // 保存当前val, 后面会变成oldVal
  61. }
  62. })
  63. }
  64. }

测试用例

以下是测试用例 和 细节解释: 可以亲手试试一条一条在控制台输出

  1. /* 以下是测试用例 和 细节解释: 可以亲手试试一条一条在控制台输出*/
  2. console.log(vm.x) // 会触发this.x的get监听函数
  3. console.log(vm.a.b) // 会触发this.a的get监听函数 和 this.a.b的get监听函数
  4. /* computed的属性C_x是一个函数的返回值, 需缓存这个返回值, 并且收集依赖this.x,
  5. 当this.x改变时, 需要重新跑函数 获得返回值
  6. 跑函数的过程中, 需获取this.x, 会触发this.x的get监听函数 */
  7. console.log(vm.C_x)
  8. console.log(vm.C_x) // computed的属性C_x已经有缓存了, 直接返回缓存, 不会去取this.x的值了, 不会触发this.x的get监听函数
  9. vm.x = 1111 // 会触发watch, 清掉C_x的缓存
  10. vm.x = 22222222 // 会触发watch, 清掉C_x的缓存
  11. console.log(vm.C_x) // computed的属性C_x需重新获取函数的返回值, 会重新触发this.x的 get监听函数
  12. console.log(vm.C_x) // computed的属性C_x已经有缓存了, 直接返回缓存, 不会去取this.x的值了, 不会触发this.x的get监听函数

vue-log.png


点赞一将,手留余香~