用ES6的class模仿Vue写一个双向绑定

VuddGq.jpg

最终效果如下:
VudDMT.gif 点击在线尝试一下 (opens new window)

# 构造器(constructor)

构造一个 TinyVue 对象,包含基本的 el,data,methods

class TinyVue {
  constructor({ el, data, methods }) {
    this.$data = data
    this.$el = document.querySelector(el)
    this.$methods = methods
    // 初始化
    this._compile()
    this._updater()
    this._watcher()
  }
}

# 编译器(compile)

用于解析绑定到输入框和下拉框的 v-model 和元素的点击事件@click。 先创建一个函数用来载入事件:

// el为元素tagName,attr为元素属性(v-model,@click)
_initEvents(el, attr, callBack) {
    this.$el.querySelectorAll(el).forEach(i => {
        if(i.hasAttribute(attr)) {
            let key = i.getAttribute(attr)
            callBack(i, key)
        }
    })
}

# 载入输入框事件

this._initEvents('input, textarea', 'v-model', (i, key) => {
  i.addEventListener('input', () => {
    Object.assign(this.$data, { [key]: i.value })
  })
})

# 载入选择框事件

this._initEvents('select', 'v-model', (i, key) => {
  i.addEventListener('change', () =>
    Object.assign(this.$data, {
      [key]: i.options[i.options.selectedIndex].value
    })
  )
})

# 载入点击事件

点击事件对应的是 methods 中的事件

this._initEvents('*', '@click', (i, key) => {
  i.addEventListener('click', () => this.$methods[key].bind(this.$data)())
})

# 视图更新器(updater)

同理先创建公共函数来处理不同元素中的视图,包括 input、textarea 的 value,select 的选择值,div 的 innerHTML

_initView(el, attr, callBack) {
    this.$el.querySelectorAll(el, attr, callBack).forEach(i => {
        if(i.hasAttribute(attr)) {
            let key = i.getAttribute(attr),
                data = this.$data[key]
            callBack(i, key, data)
        }
    })
}

# 更新输入框视图

this._initView('input, textarea', 'v-model', (i, key, data) => {
  i.value = data
})

# 更新选择框视图

this._initView('select', 'v-model', (i, key, data) => {
  i.querySelectorAll('option').forEach((v) => {
    if (v.value == data) v.setAttribute('selected', true)
    else v.removeAttribute('selected')
  })
})

# 更新 innerHTML

这里实现方法有点 low,仅想到正则替换

let regExpInner = /\{{ *([\w_\-]+) *\}}/g
this.$el.querySelectorAll('*').forEach((i) => {
  let replaceList =
    i.innerHTML.match(regExpInner) ||
    (i.hasAttribute('vueID') && i.getAttribute('vueID').match(regExpInner))
  if (replaceList) {
    if (!i.hasAttribute('vueID')) {
      i.setAttribute('vueID', i.innerHTML)
    }
    i.innerHTML = i.getAttribute('vueID')
    replaceList.forEach((v) => {
      let key = v.slice(2, v.length - 2)
      i.innerHTML = i.innerHTML.replace(v, this.$data[key])
    })
  }
})

# 监听器(watcher)

数据变化之后更新视图

_watcher(data = this.$data) {
    let that = this
    Object.keys(data).forEach(i => {
        let value = data[i]
        Object.defineProperty(data, i, {
            enumerable: true,
            configurable: true,
            get: function () {
                return value;
            },
            set: function (newVal) {
                if (value !== newVal) {
                    value = newVal;
                    that._updater()
                }
            }
        })
    })
}

# 使用

<div id="app">
  <input type="text" v-model="text1" /><br />
  <input type="text" v-model="text2" /><br />
  <textarea type="text" v-model="text3"></textarea><br />
  <button @click="add">加一</button>
  <h1>您输入的是:{{text1}}+{{text2}}+{{text3}}</h1>
  <select v-model="select">
    <option value="volvo">Volvo</option>
    <option value="saab">Saab</option>
  </select>
  <select v-model="select">
    <option value="volvo">Volvo</option>
    <option value="saab">Saab</option>
  </select>
  <h1>您选择了:{{select}}</h1>
</div>
<script src="./TinyVue.js"></script>
<script>
  let app = new TinyVue({
    el: '#app',
    data: {
      text1: 123,
      text2: 456,
      text3: '文本框',
      select: 'saab'
    },
    methods: {
      add() {
        this.text1++
        this.text2++
      }
    }
  })
</script>

# TinyVue 全部代码

class TinyVue {
  constructor({ el, data, methods }) {
    this.$data = data
    this.$el = document.querySelector(el)
    this.$methods = methods
    this._compile()
    this._updater()
    this._watcher()
  }
  _watcher(data = this.$data) {
    let that = this
    Object.keys(data).forEach((i) => {
      let value = data[i]
      Object.defineProperty(data, i, {
        enumerable: true,
        configurable: true,
        get: function () {
          return value
        },
        set: function (newVal) {
          if (value !== newVal) {
            value = newVal
            that._updater()
          }
        }
      })
    })
  }
  _initEvents(el, attr, callBack) {
    this.$el.querySelectorAll(el).forEach((i) => {
      if (i.hasAttribute(attr)) {
        let key = i.getAttribute(attr)
        callBack(i, key)
      }
    })
  }
  _initView(el, attr, callBack) {
    this.$el.querySelectorAll(el, attr, callBack).forEach((i) => {
      if (i.hasAttribute(attr)) {
        let key = i.getAttribute(attr),
          data = this.$data[key]
        callBack(i, key, data)
      }
    })
  }
  _updater() {
    this._initView('input, textarea', 'v-model', (i, key, data) => {
      i.value = data
    })
    this._initView('select', 'v-model', (i, key, data) => {
      i.querySelectorAll('option').forEach((v) => {
        if (v.value == data) v.setAttribute('selected', true)
        else v.removeAttribute('selected')
      })
    })
    let regExpInner = /\{{ *([\w_\-]+) *\}}/g
    this.$el.querySelectorAll('*').forEach((i) => {
      let replaceList =
        i.innerHTML.match(regExpInner) ||
        (i.hasAttribute('vueID') && i.getAttribute('vueID').match(regExpInner))
      if (replaceList) {
        if (!i.hasAttribute('vueID')) {
          i.setAttribute('vueID', i.innerHTML)
        }
        i.innerHTML = i.getAttribute('vueID')
        replaceList.forEach((v) => {
          let key = v.slice(2, v.length - 2)
          i.innerHTML = i.innerHTML.replace(v, this.$data[key])
        })
      }
    })
  }
  _compile() {
    this._initEvents('*', '@click', (i, key) => {
      i.addEventListener('click', () => this.$methods[key].bind(this.$data)())
    })
    this._initEvents('input, textarea', 'v-model', (i, key) => {
      i.addEventListener('input', () => {
        Object.assign(this.$data, { [key]: i.value })
      })
    })
    this._initEvents('select', 'v-model', (i, key) => {
      i.addEventListener('change', () =>
        Object.assign(this.$data, {
          [key]: i.options[i.options.selectedIndex].value
        })
      )
    })
  }
}