Vue双方向データバインドの原理(データハイジャックとパブリッシャ-サブスクライバ)

38239 ワード


<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>Documenttitle>
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: '  '
        }
      },
      methods: {
        reset() {
          this.name = '  '
          this.car.name = '  '
          this.car.color = '  '
        }
      }
    })
  script>
body>

html>