1、前言

本文章相关代码地址:https://github.com/layouwen/blog_demo_defineproperty

如果本文章对你有所帮助,请不要吝啬你的 Start 哦~

2、对象进行读写监听

  1. const obj = {
  2. name: 'layouwen',
  3. }
  4. Object.defineProperty(obj, 'name', {
  5. configurable: true,
  6. enumerable: true,
  7. get() {
  8. console.log('触发get')
  9. return 'layouwen'
  10. },
  11. set(newValue) {
  12. console.log('触发set')
  13. return newValue
  14. },
  15. })
  16. obj.name // 触发get
  17. obj.name = 'yuouwen' // 触发set

3、观察者模式

下面有个场景。当儿子说要出去玩的时候,爸爸告诉孩子不能出去玩。

  1. const father = {
  2. eat() {
  3. console.log('不给出去玩!准备吃饭了')
  4. },
  5. }
  6. const son = {
  7. play() {
  8. console.log('爸,我出去玩会~')
  9. },
  10. }
  11. son.play()

我们新建一个事件触发器

  1. const EventObj = new EventTarget()

在执行 play 方法是,通知爸爸

  1. EventObj.addEventListener('callFather', father.eat)
  2. const son = {
  3. play() {
  4. console.log('爸,我出去玩会~')
  5. EventObj.dispatchEvent(new CustomEvent('callFather'))
  6. },
  7. }

4、发布订阅模式

跟上面的场景一致,我们换个思路实现。首先创建一个保存需要执行函数的队列。提供两个方法:添加新的任务 addSub,执行所有任务 notify

  1. class Dep {
  2. constructor() {
  3. this.subs = []
  4. }
  5. addSub(sub) {
  6. this.subs.push(sub)
  7. }
  8. notify() {
  9. this.subs.forEach(item => item.update())
  10. }
  11. }

创建一个用于新建任务的类,提供一个方法:执行自己的任务 update

  1. class Watcher {
  2. constructor(callback) {
  3. this.callback = callback
  4. }
  5. update() {
  6. this.callback()
  7. }
  8. }

实例化 Dep,用于将新建的任务加入到队列中。

  1. const dep = new Dep()

创建两个对象,模拟妈妈和爸爸。

  1. const father = {
  2. eat() {
  3. dep.addSub(
  4. new Watcher(() => {
  5. console.log('爸爸:不给出去玩!准备吃饭了')
  6. })
  7. )
  8. },
  9. }
  10. const mother = {
  11. eat() {
  12. dep.addSub(
  13. new Watcher(() => {
  14. console.log('妈妈:不给出去玩!准备吃饭了')
  15. })
  16. )
  17. },
  18. }

执行里面的方法,使其加入到等待任务中

  1. father.eat()
  2. mother.eat()

此时创建一个儿子对象

  1. const son = {
  2. play() {
  3. console.log('儿子:爸,我出去玩会~')
  4. dep.notify()
  5. },
  6. }

我设置一个延迟,在 2 秒回触发儿子的 play 方法。看看是否会将爸爸和妈妈中的等待任务给执行。

  1. setTimeout(son.play, 2000)

结果

  1. 儿子:爸,我出去玩会~
  2. 爸爸:不给出去玩!准备吃饭了
  3. 妈妈:不给出去玩!准备吃饭了

5、观察模式模拟 Vue 的数据监听响应

先实现通过正则表达式,将{{value}}内的值替换成,data 中的数值

  1. class LVue {
  2. constructor(option) {
  3. this.$option = option
  4. this._data = option.data
  5. this.compile()
  6. }
  7. compile() {
  8. const el = document.querySelector(this.$option.el)
  9. this.compileNodes(el)
  10. }
  11. compileNodes(el) {
  12. /* 获取所有子节点 */
  13. const childNodes = el.childNodes
  14. childNodes.forEach(node => {
  15. /* 如果为元素节点,并且该节点内部还有内容,就继续进行遍历编译 */
  16. if (node.nodeType === 1 && node.childNodes.length > 0) {
  17. /* 元素节点 */
  18. this.compileNodes(node)
  19. } else if (node.nodeType === 3) {
  20. /* 文本节点 */
  21. const textContent = node.textContent
  22. // 创建正则。匹配{{}}中的内容
  23. const reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g
  24. /* 匹配成功的就是我们需要的内容 */
  25. if (reg.test(textContent)) {
  26. // 获得匹配到的内容
  27. const name = RegExp.$1
  28. // 替换内容
  29. node.textContent = node.textContent.replace(reg, this._data[name])
  30. }
  31. }
  32. })
  33. }
  34. }
  35. new LVue({
  36. el: '#app',
  37. data: {
  38. name: 'layouwen',
  39. age: 22,
  40. address: '广州市荔湾区',
  41. },
  42. })

我们实现了简单的替换后,我们开始实现数据劫持监听

  1. // 继承 EventTarget 实现监听事件
  2. /* new content start */
  3. class LVue extends EventTarget {
  4. /* new content end */
  5. constructor(option) {
  6. super()
  7. this.$option = option
  8. this._data = option.data
  9. this.observe(option.data)
  10. this.compile()
  11. }
  12. observe(data) {
  13. const keys = Object.keys(data)
  14. const that = this
  15. keys.forEach(key => {
  16. let value = data[key]
  17. Object.defineProperty(data, key, {
  18. configurable: true,
  19. enumerable: true,
  20. get() {
  21. return value
  22. },
  23. set(newValue) {
  24. /* new content start */
  25. // 触发对应 key 事件
  26. that.dispatchEvent(new CustomEvent(key, { detail: newValue }))
  27. /* new content end */
  28. // 更新闭包中缓存的 value
  29. value = newValue
  30. },
  31. })
  32. })
  33. }
  34. compile() {
  35. const el = document.querySelector(this.$option.el)
  36. this.compileNodes(el)
  37. }
  38. compileNodes(el) {
  39. /* 获取所有子节点 */
  40. const childNodes = el.childNodes
  41. childNodes.forEach(node => {
  42. /* 如果为元素节点,并且该节点内部还有内容,就继续进行遍历编译 */
  43. if (node.nodeType === 1 && node.childNodes.length > 0) {
  44. /* 元素节点 */
  45. this.compileNodes(node)
  46. } else if (node.nodeType === 3) {
  47. /* 文本节点 */
  48. const textContent = node.textContent
  49. // 创建正则。匹配{{}}中的内容
  50. const reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g
  51. /* 匹配成功的就是我们需要的内容 */
  52. if (reg.test(textContent)) {
  53. console.log(this, 'this1')
  54. // 获得匹配到的内容
  55. const name = RegExp.$1
  56. // 替换内容
  57. node.textContent = node.textContent.replace(reg, this._data[name])
  58. /* new content start */
  59. // 监听 name 对应的事件
  60. this.addEventListener(name, e => {
  61. console.log(this, 'this2')
  62. const newValue = e.detail
  63. const oldValue = this._data[name]
  64. node.textContent = node.textContent.replace(oldValue, newValue)
  65. console.log('页面数据刷新了')
  66. })
  67. /* new content end */
  68. }
  69. }
  70. })
  71. }
  72. }
  73. const lvue = new LVue({
  74. el: '#app',
  75. data: {
  76. name: 'layouwen',
  77. age: 22,
  78. address: '广州市荔湾区',
  79. },
  80. })
  81. setTimeout(() => {
  82. lvue._data.name = '梁又文'
  83. setTimeout(() => {
  84. lvue._data.age = 23
  85. }, 1000)
  86. }, 2000)

6、发布订阅模式版本

通过依赖收集,对用 notify 触发所有收集的依赖实现响应式。简单模拟了一下 v-model、v-text 以及 v-html

  1. <div id="app">
  2. <h1>{{name}}</h1>
  3. <input type="text" v-model="name" />
  4. <h2>v-text</h2>
  5. <div v-text="name"></div>
  6. <div v-html="name"></div>
  7. <div>
  8. <p>年龄:{{age}}</p>
  9. <p>地址:{{address}}</p>
  10. </div>
  11. </div>
  12. <script>
  13. class LVue2 {
  14. constructor(option) {
  15. this.$option = option
  16. this.$data = option.data
  17. this.observer()
  18. this.compile()
  19. }
  20. observer() {
  21. const keys = Object.keys(this.$data)
  22. keys.forEach(keyName => {
  23. const dep = new Dep()
  24. let value = this.$data[keyName]
  25. Object.defineProperty(this.$data, keyName, {
  26. configurable: true,
  27. enumerable: true,
  28. get() {
  29. if (Dep.target) {
  30. dep.addSub(Dep.target)
  31. }
  32. return value
  33. },
  34. set(newValue) {
  35. dep.notify(newValue)
  36. value = newValue
  37. },
  38. })
  39. })
  40. }
  41. compile() {
  42. const el = document.querySelector(this.$option.el)
  43. this.compileNodes(el)
  44. }
  45. compileNodes(el) {
  46. const childNodes = el.childNodes
  47. childNodes.forEach(node => {
  48. if (node.nodeType === 1) {
  49. /* new content start */
  50. // 新增 v-model 属性监听
  51. let attrs = node.attributes
  52. ;[...attrs].forEach(attr => {
  53. const attrName = attr.name
  54. const attrValue = attr.value
  55. if (attrName === 'v-model') {
  56. node.value = this.$data[attrValue]
  57. node.addEventListener('input', e => {
  58. this.$data[attrValue] = e.target.value
  59. })
  60. } else if (attrName === 'v-text') {
  61. node.innerText = this.$data[attrValue]
  62. new Watcher(this.$data, attrValue, newValue => {
  63. node.innerText = newValue
  64. })
  65. } else if (attrName === 'v-html') {
  66. node.innerHTML = this.$data[attrValue]
  67. new Watcher(this.$data, attrValue, newValue => {
  68. node.innerHTML = newValue
  69. })
  70. }
  71. })
  72. /* new content end */
  73. if (node.childNodes.length > 0) {
  74. this.compileNodes(node)
  75. }
  76. } else if (node.nodeType === 3) {
  77. const textContent = node.textContent
  78. const reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g
  79. if (reg.test(textContent)) {
  80. const valueName = RegExp.$1
  81. node.textContent = node.textContent.replace(reg, this.$data[valueName])
  82. new Watcher(this.$data, valueName, newValue => {
  83. const oldValue = this.$data[valueName]
  84. node.textContent = node.textContent.replace(oldValue, newValue)
  85. })
  86. }
  87. }
  88. })
  89. }
  90. }
  91. class Dep {
  92. constructor() {
  93. this.subs = []
  94. }
  95. addSub(sub) {
  96. this.subs.push(sub)
  97. }
  98. notify(newValue) {
  99. this.subs.forEach(sub => {
  100. sub.update(newValue)
  101. })
  102. }
  103. }
  104. class Watcher {
  105. constructor(data, key, cb) {
  106. this.cb = cb
  107. // 保存实例对象到 Dep 中的 target 中
  108. Dep.target = this
  109. // 为了触发 get 收集依赖
  110. data[key]
  111. Dep.target = null
  112. }
  113. update(newValue) {
  114. this.cb(newValue)
  115. }
  116. }
  117. const lvue2 = new LVue2({
  118. el: '#app',
  119. data: {
  120. name: '梁又文',
  121. age: 23,
  122. address: '广州市荔湾区',
  123. },
  124. })
  125. setTimeout(() => (lvue2.$data.name = '梁文文'), 1000)
  126. setTimeout(() => (lvue2.$data.name = '我是v-text的内容'), 2000)
  127. setTimeout(() => (lvue2.$data.name = '<h3>我是v-html的内容</h3>'), 3000)
  128. </script>