Vue 双向数据绑定原理(数据劫持和发布者-订阅者)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div class="app">
    <form>
      <label for="name">
        名字:
        <input id="name" type="text" v-model="name">
      </label>
      <label for="carName">
        车牌:
        <input id="carName" type="text" v-model="car.name">
      </label>
      <label for="carColor">
        颜色:
        <input id="carColor" type="text" v-model="car.color">
      </label>
      <button @click="reset">重置</button>
    </form>
    <h4>
      我的名字叫:<span v-text="name"></span>,我有一辆<span v-html="car.color"></span><span v-text="car.name"></span>
    </h4>
  </div>

  <script>
    /*
      原理:数据劫持
      思路:
        1. 遍历数据,并利用 Object.defineProperty 进行数据劫持(根据键名);绑定订阅者,数据发生变化时统一更新。
        2. 遍历编译 虚拟DOM 树,生成订阅者对象,绑定到数据对象上。
      对象:
        MyVm:模拟 Vue 对象
          属性:
            _el:根节点
            _data:数据
            _methods:方法
            _property:数据与订阅者对应关系(存放订阅者对象)
          功能:
            _listen:遍历数据并劫持
            _compile:编译生成订阅者
        Watcher:订阅者对象
          属性:
            _el:需要操作的节点
            _vm:vm对象
            _attr:需要操作的DOM属性
            _dataName:数据依赖
          功能:
            _update:更新订阅者
    */

    class MyVm {
      constructor({
        el,
        data,
        methods
      }) {
        this._el = document.querySelector(el)
        this._data = data
        this._methods = methods
        this._property = {}

        this._listen(this._data, this._property)
        this._compile(this._el)
      }

      _listen(data, parent) {
        const that = this
        Object.keys(data).map(key => {
          if (data.hasOwnProperty(key)) {
            let value = data[key]

            // 绑定订阅者
            parent[key] = {
              _subscriber: []
            }

            // 数据劫持
            Object.defineProperty(data, key, {
              get() {
                return value
              },
              set(newValue) {
                value = newValue
                // 更新订阅者
                parent[key]._subscriber.map(watcher => watcher._update())
              }
            })

            // 递归遍历
            if (typeof value === 'object') {
              this._listen(value, parent[key])
            }
          }
        })
      }

      _compile(root) {
        const nodeList = root.children
        if (nodeList.length != 0) {
          Object.values(nodeList).map(node => {
            // 编译 v-html 属性
            if (node.hasAttribute('v-html')) {
              // 找到对应数据
              const attr = node.getAttribute('v-html').split('.')
              let _p = this._property
              attr.map(a => {
                _p = _p[a]
              })

              // 绑定订阅者
              const watcher = new Watcher({
                el: node,
                vm: this,
                attr: 'innerHTML',
                dataName: attr
              })
              _p._subscriber.push(watcher)
            }

            // 编译 v-text 属性
            if (node.hasAttribute('v-text')) {
              // 找到对应数据
              const attr = node.getAttribute('v-text').split('.')
              let _p = this._property
              attr.map(a => {
                _p = _p[a]
              })

              // 绑定订阅者
              const watcher = new Watcher({
                el: node,
                vm: this,
                attr: 'innerText',
                dataName: attr
              })
              _p._subscriber.push(watcher)
            }

            // 编译 v-model 属性
            if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
              // 找到对应数据
              const attr = node.getAttribute('v-model').split('.')
              let _p = this._property
              attr.map(a => {
                _p = _p[a]
              })

              // 绑定订阅者
              const watcher = new Watcher({
                el: node,
                vm: this,
                attr: 'value',
                dataName: attr
              })
              _p._subscriber.push(watcher)

              // 监听事件
              node.addEventListener('input', () => {
                let _d = this._data
                attr.map((a, i) => {
                  if (i < attr.length - 1) {
                    _d = _d[a]
                  } else {
                    _d[a] = node.value
                  }
                })
              })
            }

            // 编译事件
            Object.values(node.attributes).map(attr => {
              if (attr.name.match(/^@.*/)) {
                const type = attr.name.substring(1)
                const method = node.getAttribute(attr.name)
                // 绑定事件
                node.addEventListener(type, this._methods[method].bind(this._data))
              }
            })

            // 递归遍历
            this._compile(node)
          })
        }
      }
    }

    class Watcher {
      constructor({
        el,
        vm,
        attr,
        dataName
      }) {
        this._el = el
        this._vm = vm
        this._attr = attr
        this._dataName = dataName

        // 加载默认数据
        this._update()
      }

      _update() {
        this._dataName.map(a => {
          let _d = this._vm._data
          this._dataName.map((item, index) => {
            _d = _d[item]
          })
          this._el[this._attr] = _d
        })
      }
    }

    const app = new MyVm({
      el: '.app',
      data: {
        name: '张三',
        car: {
          name: '宝马',
          color: '<span style="color: red">红色</span>'
        }
      },
      methods: {
        reset() {
          this.name = '张三'
          this.car.name = '宝马'
          this.car.color = '白色'
        }
      }
    })
  </script>
</body>

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值