了解defineProperty,实现一个简单的vue数据响应式

本文介绍了JavaScript中对象的读写监听、观察者模式和发布订阅模式,以及如何利用这些模式模拟Vue的数据监听响应。通过正则表达式替换模板变量,并使用Object.defineProperty实现数据劫持,最后通过依赖收集和发布订阅模式实现了简单的响应式系统。文章还展示了如何应用于v-model、v-text和v-html的场景。
摘要由CSDN通过智能技术生成

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

3、观察者模式

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

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

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

son.play()

我们新建一个事件触发器

const EventObj = new EventTarget()

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

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

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

4、发布订阅模式

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

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

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

class Watcher {
  constructor(callback) {
    this.callback = callback
  }
  update() {
    this.callback()
  }
}

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

const dep = new Dep()

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

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

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

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

father.eat()
mother.eat()

此时创建一个儿子对象

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

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

setTimeout(son.play, 2000)

结果

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

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: '广州市荔湾区',
  },
})

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

// 继承 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)

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值