当前位置:   article > 正文

了解defineProperty,实现一个简单的vue数据响应式_defineprops怎么变成响应式

defineprops怎么变成响应式

1、前言

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

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

2、对象进行读写监听

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

3、观察者模式

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

const father = {
  eat() {
    console.log('不给出去玩!准备吃饭了')
  },
}

const son = {
  play() {
    console.log('爸,我出去玩会~')
  },
}

son.play()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

我们新建一个事件触发器

const EventObj = new EventTarget()
  • 1

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

EventObj.addEventListener('callFather', father.eat)

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

4、发布订阅模式

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

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

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

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

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

const dep = new Dep()
  • 1

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

const father = {
  eat() {
    dep.addSub(
      new Watcher(() => {
        console.log('爸爸:不给出去玩!准备吃饭了')
      })
    )
  },
}

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

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

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

此时创建一个儿子对象

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

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

setTimeout(son.play, 2000)
  • 1

结果

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

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

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

class LVue {
  constructor(option) {
    this.$option = option
    this._data = option.data
    this.compile()
  }
  compile() {
    const el = document.querySelector(this.$option.el)
    this.compileNodes(el)
  }
  compileNodes(el) {
    /* 获取所有子节点 */
    const childNodes = el.childNodes
    childNodes.forEach(node => {
      /* 如果为元素节点,并且该节点内部还有内容,就继续进行遍历编译 */
      if (node.nodeType === 1 && node.childNodes.length > 0) {
        /* 元素节点 */
        this.compileNodes(node)
      } else if (node.nodeType === 3) {
        /* 文本节点 */
        const textContent = node.textContent
        // 创建正则。匹配{{}}中的内容
        const reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g
        /* 匹配成功的就是我们需要的内容 */
        if (reg.test(textContent)) {
          // 获得匹配到的内容
          const name = RegExp.$1
          // 替换内容
          node.textContent = node.textContent.replace(reg, this._data[name])
        }
      }
    })
  }
}

new LVue({
  el: '#app',
  data: {
    name: 'layouwen',
    age: 22,
    address: '广州市荔湾区',
  },
})
  • 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

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

// 继承 EventTarget 实现监听事件
/* new content start */
class LVue extends EventTarget {
  /* new content end */
  constructor(option) {
    super()
    this.$option = option
    this._data = option.data
    this.observe(option.data)
    this.compile()
  }
  observe(data) {
    const keys = Object.keys(data)
    const that = this
    keys.forEach(key => {
      let value = data[key]
      Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get() {
          return value
        },
        set(newValue) {
          /* new content start */
          // 触发对应 key 事件
          that.dispatchEvent(new CustomEvent(key, { detail: newValue }))
          /* new content end */
          // 更新闭包中缓存的 value
          value = newValue
        },
      })
    })
  }
  compile() {
    const el = document.querySelector(this.$option.el)
    this.compileNodes(el)
  }
  compileNodes(el) {
    /* 获取所有子节点 */
    const childNodes = el.childNodes
    childNodes.forEach(node => {
      /* 如果为元素节点,并且该节点内部还有内容,就继续进行遍历编译 */
      if (node.nodeType === 1 && node.childNodes.length > 0) {
        /* 元素节点 */
        this.compileNodes(node)
      } else if (node.nodeType === 3) {
        /* 文本节点 */
        const textContent = node.textContent
        // 创建正则。匹配{{}}中的内容
        const reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g
        /* 匹配成功的就是我们需要的内容 */
        if (reg.test(textContent)) {
          console.log(this, 'this1')
          // 获得匹配到的内容
          const name = RegExp.$1
          // 替换内容
          node.textContent = node.textContent.replace(reg, this._data[name])
          /* new content start */
          // 监听 name 对应的事件
          this.addEventListener(name, e => {
            console.log(this, 'this2')
            const newValue = e.detail
            const oldValue = this._data[name]
            node.textContent = node.textContent.replace(oldValue, newValue)
            console.log('页面数据刷新了')
          })
          /* new content end */
        }
      }
    })
  }
}

const lvue = new LVue({
  el: '#app',
  data: {
    name: 'layouwen',
    age: 22,
    address: '广州市荔湾区',
  },
})
setTimeout(() => {
  lvue._data.name = '梁又文'
  setTimeout(() => {
    lvue._data.age = 23
  }, 1000)
}, 2000)
  • 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

6、发布订阅模式版本

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

<div id="app">
  <h1>{{name}}</h1>
  <input type="text" v-model="name" />
  <h2>v-text</h2>
  <div v-text="name"></div>
  <div v-html="name"></div>
  <div>
    <p>年龄:{{age}}</p>
    <p>地址:{{address}}</p>
  </div>
</div>
<script>
  class LVue2 {
    constructor(option) {
      this.$option = option
      this.$data = option.data
      this.observer()
      this.compile()
    }
    observer() {
      const keys = Object.keys(this.$data)
      keys.forEach(keyName => {
        const dep = new Dep()
        let value = this.$data[keyName]
        Object.defineProperty(this.$data, keyName, {
          configurable: true,
          enumerable: true,
          get() {
            if (Dep.target) {
              dep.addSub(Dep.target)
            }
            return value
          },
          set(newValue) {
            dep.notify(newValue)
            value = newValue
          },
        })
      })
    }
    compile() {
      const el = document.querySelector(this.$option.el)
      this.compileNodes(el)
    }
    compileNodes(el) {
      const childNodes = el.childNodes
      childNodes.forEach(node => {
        if (node.nodeType === 1) {
          /* new content start */
          // 新增 v-model 属性监听
          let attrs = node.attributes
          ;[...attrs].forEach(attr => {
            const attrName = attr.name
            const attrValue = attr.value
            if (attrName === 'v-model') {
              node.value = this.$data[attrValue]
              node.addEventListener('input', e => {
                this.$data[attrValue] = e.target.value
              })
            } else if (attrName === 'v-text') {
              node.innerText = this.$data[attrValue]
              new Watcher(this.$data, attrValue, newValue => {
                node.innerText = newValue
              })
            } else if (attrName === 'v-html') {
              node.innerHTML = this.$data[attrValue]
              new Watcher(this.$data, attrValue, newValue => {
                node.innerHTML = newValue
              })
            }
          })
          /* new content end */
          if (node.childNodes.length > 0) {
            this.compileNodes(node)
          }
        } else if (node.nodeType === 3) {
          const textContent = node.textContent
          const reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g
          if (reg.test(textContent)) {
            const valueName = RegExp.$1
            node.textContent = node.textContent.replace(reg, this.$data[valueName])
            new Watcher(this.$data, valueName, newValue => {
              const oldValue = this.$data[valueName]
              node.textContent = node.textContent.replace(oldValue, newValue)
            })
          }
        }
      })
    }
  }

  class Dep {
    constructor() {
      this.subs = []
    }
    addSub(sub) {
      this.subs.push(sub)
    }
    notify(newValue) {
      this.subs.forEach(sub => {
        sub.update(newValue)
      })
    }
  }

  class Watcher {
    constructor(data, key, cb) {
      this.cb = cb
      // 保存实例对象到 Dep 中的 target 中
      Dep.target = this
      // 为了触发 get 收集依赖
      data[key]
      Dep.target = null
    }
    update(newValue) {
      this.cb(newValue)
    }
  }

  const lvue2 = new LVue2({
    el: '#app',
    data: {
      name: '梁又文',
      age: 23,
      address: '广州市荔湾区',
    },
  })
  setTimeout(() => (lvue2.$data.name = '梁文文'), 1000)
  setTimeout(() => (lvue2.$data.name = '我是v-text的内容'), 2000)
  setTimeout(() => (lvue2.$data.name = '<h3>我是v-html的内容</h3>'), 3000)
</script>
  • 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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/259125
推荐阅读
相关标签
  

闽ICP备14008679号