Obeject.defineProperty() 实现一个简单的双向绑定

Vue 内部使用了 Obeject.defineProperty() 来实现双向绑定,通过这个函数可以监听到 setget的事件

首先我们来了解一下 Obeject.defineProperty () 的使用方法

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅

<div id="app">
    {{name}}
</div>

在解析如上模板代码时,遇到 {name} 就会给属性 name 添加发布订阅

// 通过 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('#app').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 

接下来,对 defineReactive 函数进行改造

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加

完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">{{name}}</div>
  <script>
    window.onload = function () {
      function observer (obj) {
        // 判断类型
        if (!obj || typeof obj !== 'object') {
          return
        }
        Object.keys(data).forEach(key => {
          defineReactive(obj, key, data[key])
        })
        function defineReactive (obj, key, val) {
          // 递归子属性
          observer(val)
          let dp = new Dep()
          Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function () {
              console.log('get value')
              // 将 Watcher 添加到订阅
              if (Dep.target) {
                dp.addSub(Dep.target)
              }
              return val
            },
            set: function (newVal) {
              console.log('change value')
              val = newVal
              // 执行 watcher 的 update 方法
              dp.notify()
            }
          })
        }
      }

      // 通过 Dep 解耦
      class Dep {
        constructor () {
          this.subs = []
        }
        addSub (sub) {
          // sub 是 Watcher 实例
          this.subs.push(sub)
        }
        notify () {
          this.subs.forEach(sub => {
            sub.update()
          })
        }
      }

      // 全局属性,通过该属性配置 Watcher
      Dep.target = null
      
      // 全局 update 函数,下边会传入到 Watcher 实例中
      function update (value) {
        document.querySelector('#app').innerHTML = value
      }

      class Watcher {
        constructor (obj, key, cb) {
          // 将 Dep.target 指向自己
          // 然后出发属性的 getter 添加监听
          // 最后将 Dep.target 置空
          Dep.target = this
          this.cb = cb
          this.obj = obj
          this.key = key
          this.value = obj[key]
          Dep.target = null
        }
        update () {
          // 获取新值
          this.value = this.obj[this.key]
          // 调用 update 方法更新 Dom
          this.cb(this.value)
        }
      }
      
      var data = {name: 'yck'}
      // debugger
      observer(data)
      // 模拟解析到 {{name}} 触发的操作
      new Watcher(data, 'name', update)
      // update Dom innerText
      data.name = 'yyy'

    }
  </script>
</body>
</html>
Proxy 与 Obeject.defineProperty 对比
  • Object.defineProperty 虽然已经能够实现双向绑定了,但是他还是有缺陷的。
    • 只能对属性进行数据劫持,所以需要深度遍历整个对象
    • 对于数组不能监听到数据的变化

虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法,并且也是有缺陷的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值