用ES6的class模仿Vue写一个双向绑定
- 作者:Bougie
- 创建于:2018-04-17
- 更新于:2023-03-09
最终效果如下:
点击在线尝试一下 (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
})
)
})
}
}