通过proxy实现数据的双向绑定

这两天在看数据双向绑定的文章。主要还是看不懂vue源码,只能找别人的文章看看。有一篇文章是《用proxy实现一个更优雅的vue》。看完后把代码抄了一遍,画了个简单的解析图(可能有误)。然后又自己实现了一遍,做了简单的扩展。

github:为了理解MVVM而创建的项目
src: 通过Object.defineProperty实现数据的双向绑定
src-proxy-simple: 抄写的代码
src-proxy: 简单扩展

扩展后的功能:

单向绑定

指令可用属性例子
m-bind‘text’, ‘innerText’, ‘html’,
‘innerHTML’, ‘src’, ‘value’,
‘href’, ‘class’, ‘style’,
‘alt’, ‘title’
m-bind:text = “query.tip”

双向绑定

指令例子
m-modelm-model = “query.query”

事件绑定

指令可用事件例子
m-on‘click’, ‘dbclick’, ‘input’, ‘focus’,
‘blur’, ‘change’, ‘load’, ‘select’
m-on:click=”add”

总结

功力还是不够,看不懂vue源码,也实现不了什么功能。

代码:

// index.html
<!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>
  <link rel="stylesheet" href="../css/style.css">
  <script src="../js/mvvm.js"></script>
</head>
<body>
  <div class="container">
    <section id="app" class="app">
      <div class="query">
        <span class="query-tip" m-bind:text="query.tip"></span>
        <input
          class="query-input"
          m-bind:style="style"
          m-model="query.query"
          m-on:change="changeStyle"
          type="text">
        <span class="query-text"
          m-bind:title="query.query"
          m-bind:text="query.query"></span>
      </div>
      <div class="actions">
        <span class="number" m-bind:innerText="number"></span>
        <button
          m-on:click="add">add</button>
        <button
          m-on:click="sub">sub</button>
      </div>
    </section>
  </div>
</body>
<script src="../js/app.js"></script>
</html>
// style.css
.number {
  display: inline-block;
  width: 30px;
  text-align: center;
}
// mvvm.js
/**
 * 类:M
 */
class M {
  // 初始化
  constructor(options) {
    this.$el = document.querySelector(options.el)
    this.$data = options.data
    this.$methods = options.methods
    this._observe(this, '$data') // 监听数据
    this._compile(this.$el) // 编译html,监听事件,收集依赖
    this._initFill(this.$data) // 数据填充
  }
  // 监听数据————对set进行拦截
  _observe(parent, child) {
    const data = parent[child]
    parent[child] = new Proxy(data, {
      set(target, key, value) {
        let res = Reflect.set(target, key, value)
        // 数据变化时更新视图
        if (target._ob[key]) {
            target._ob[key].map(item => {
            item.update()
          })
        }
        
        return res
      }
    })
    // 遍历对象,如果元素是数组或对象,递归监听
    const keys = Object.keys(data)
    keys.map(item => {
      if (data[item] !== null && (typeof data[item]) === 'object') {
        this._observe(data, item)
      }
    })
  }
  // 编译
  _compile(el) {
    const nodes = Array.prototype.slice.call(el.children)
    nodes.map(node => {
      if (node.children.length > 0) this._compile(node)
      let attrs = null
      // 解析m-bind
      if (attrs = this._hasBind(node)) {
        attrs.map(attr => {
          this._pushWatcher(node, attr.attr, attr.key)
        })
      }
      // 解析m-model
      if (node.hasAttribute('m-model')) {
        const key = node.getAttribute('m-model')
        this._pushWatcher(node, 'value', key)
        const data = this._resolveValue(key)
        if (data !== false) {
          const tempData = data.data
          const tempKey = data.key
          node.addEventListener('input', () => {tempData[tempKey] = node.value})
        }
      }
      let events = null
      // 解析m-on
      if (events = this._hasOn(node)) {
        events.map(event => {
          const handler = this.$methods[event.key].bind(this.$data)
          console.log(event)
          node.addEventListener(event.event, handler)
        })
      }
    })
  }
  // 判断m-bind
  _hasBind(node) {
    let i = null
    let attr = null
    let key = null
    const attrs = []
    // 可以bind的属性
    const _bindAttr = ['text', 'innerText', 'html', 'innerHTML', 'src', 'value', 'href', 'class', 'style', 'alt', 'title']
    _bindAttr.map((item, index) => {
      if (node.hasAttribute('m-bind:' + item)) {
        i = index
        attr = item
        key = node.getAttribute('m-bind:' + item)
        if (attr && key) {
          if (attr === 'text') attr = 'innerText'
          if (attr === 'html') attr = 'innerHTML'
          attrs.push({ attr, key })
          attr = null
          key = null
        }
      }
    })
    
    if (attrs.length) {
      return attrs
    } else return false
  }
  // 添加watcher
  _pushWatcher(node, attr, key) {
    const data = this._resolveValue(key)
    if (data !== false) {
      const tempData = data.data
      const tempKey = data.key
      if (tempData._ob === null || tempData._ob === undefined) {
        tempData._ob = Object.create(null)
      }
      console.log(tempKey)
      if (tempData._ob[tempKey] === null || tempData._ob[tempKey] === undefined) {
        tempData._ob[tempKey] = []
      }
      tempData._ob[tempKey].push(new Watcher(node, attr, tempKey, tempData))
    }
  }
  // 解析值所在对象
  _resolveValue(key) {
    const arr = key.split('.')
    if (arr.length) {
      let data = this.$data
      for (let i = 0, len = arr.length - 1; i < len; i++) {
        if (data[arr[i]]) {
          data = data[arr[i]]
        }
      }
      return {
        data,
        key: arr[arr.length - 1]
      }
    } else {
      return false
    }
  }
  // 判断m-on
  _hasOn(node) {
    let event = null
    let key = null
    const events = []
    // 可以监听的事件
    const _event = ['click', 'dbclick', 'input', 'focus', 'blur', 'change', 'load', 'select']
    _event.map((item, index) => {
      if (node.hasAttribute('m-on:' + item)) {
        event = item
        key = node.getAttribute('m-on:' + item)
        if (event && key) {
          events.push({event, key})
          event = null
          key = null
        }
      }
    })
    if (events.length) {
      return events
    } else return false
  }
  // 数据填充初始化
  _initFill(data) {
    if (data._ob) {
      const keys = Object.keys(data._ob)
      keys.map(item => {
        data._ob[item].map(watcher => {
          watcher.update()
        })
      })
    }
    const dataKeys = Object.keys(data)
    dataKeys.map(item => {
      if ((typeof data[item]) === 'object' && item !== '_ob') {
        this._initFill(data[item])
      }
    })
  }
}

// 类:Watcher。更新视图
class Watcher {
  constructor(node, attr, key, data) {
    this.node = node
    this.attr = attr
    this.key = key
    this.data = data
  }
  update() {
    console.log(this)
    this.node[this.attr] = this.data[this.key]
  }
}
// app.js
window.m = new M({
  el: '#app',
  data: {
    number: 0,
    query: {
      tip: '输入:',
      query: ''
    },
    style: 'color: #000'
  },
  methods: {
    add() {
      this.number++
    },
    sub() {
      this.number--
    },
    changeStyle() {
      let color = ''
      let number = Number(this.query.query)
      if (!number) {
        color = 'red'
      } else if(number < 10){
        color = 'green'
      } else if (number < 100) {
        color = 'blue'
      } else {
        color = 'gray'
      }
      this.style = 'color: ' + color
    }
  }
})