V-model的双向绑定原理
V-model的原理在学习v-model的过程中手贱自己用原生JS实现了一遍v-model的双向绑定。。。首先,v-model只是个语法糖,实际上原理给表单元素用绑定值和input或change事件,举个例子:<input v-model="value" />//实际上等于<input v-bind:value="value" @input="value = $event.tar
V-model的原理
在学习v-model的过程中手贱自己用原生JS实现了一遍v-model的双向绑定。。。
首先,v-model只是个语法糖,实际上原理给表单元素用绑定值和input或change事件,举个例子:
<input v-model="value" />
//实际上等于
<input v-bind:value="value" @input="value = $event.target.value" />
至于表单元素的用法可以自行查看Vue官网文档。
简单阐述一下,
input元素和textarea元素是绑定value属性和input事件,
checkbox和radio则是绑定checked属性和change事件,
select,option则是绑定selected属性和change事件。
接下来开始原生JS的实现过程。。。。
先贴结构
(ps:p元素仅作展示使用)
<label for="text">
<input type="text" id="text" />
</label>
<p></p>
<label for="male">
男<input type="radio" name="sex" value="boy" id="male" />
</label>
<label for="female">
女<input type="radio" name="sex" value="girl" id="female" />
</label>
<p></p>
<input type="checkbox" name="hobby" value="足球" />足球
<input type="checkbox" name="hobby" value="篮球" />篮球
<input type="checkbox" name="hobby" value="排球" />排球
<p></p>
<select>
<option value="" disabled>请选择水果</option>
<option value="苹果">苹果</option>
<option value="芒果">芒果</option>
<option value="火龙果">火龙果</option>
</select>
<p></p>
一开始的实现是这样的
let radio = document.querySelectorAll("input[type='radio']")
let checkbox = document.querySelectorAll("input[type='checkbox']")
let text = document.getElementById("text")
let p = document.querySelectorAll('p')
let div = document.querySelector('div')
let option = document.querySelectorAll('option')
let select = document.querySelector('select')
let inputText = 'hhh'
let sex = 'boy'
let checkArr = ['足球' , '篮球' , '排球']
let selected = ''
// select
//遍历option节点,判断选中哪个节点,初始化到DOM上
for(let i = 0 ; i < option.length ; i++) {
if(option[i].value === selected) option[i].selected = true
}
//给select绑定change事件,当触发事件时更新展示区域和变量值
select.onchange = function(event) {
// console.log(event.target.selectedIndex);
selected = option[event.target.selectedIndex].value
div.innerHTML = selected
}
//input
//初始化input框的值
text.value = inputText
//给input框绑定input事件,触发时更新展示区域和变量值
text.oninput = function(event) {
inputText = event.target.value
p[0].innerHTML = inputText
}
//radio
//遍历radio节点,判断是否选中,并绑定change事件
for(let i=0 ; i<radio.length ; i++) {
if(radio[i].getAttribute('value') === sex) radio[i].checked = true
radio[i].onchange = function(event) {
sex = event.target.value
p[1].innerHTML = sex
}
}
//checkbox
//遍历checkArr数组,判断是否选中,初始化DOM
checkArr.forEach((item) => {
for(let i=0 ; i<checkbox.length ; i++) {
if(checkbox[i].getAttribute('value') === item) checkbox[i].checked = true
}
})
//遍历checbox节点,绑定change事件
for(let i=0 ; i<checkbox.length ; i++) {
checkbox[i].onchange = function(event) {
if(event.target.checked) {
checkArr.push(event.target.value)
}else {
checkArr.forEach((item , index) => {
if(checkbox[i].getAttribute('value') === item)
checkArr.splice(index , 1)
})
}
p[2].innerHTML = checkArr
}
}
很麻烦的实现了一遍,后来一想每次都要写for循环很麻烦,就写了个函数把获取到的节点类数组转成数组,同时将所有的变量写成一个对象并做了一层数据代理
function selectElem(selectStr) {
return Array.from(document.querySelectorAll(selectStr))
}
let radio = selectElem("input[type='radio']")
let checkbox = selectElem("input[type='checkbox']")
let option = selectElem('option')
let p = selectElem('p')
let select = document.querySelector('select')
let text = document.getElementById("text")
let obj = {
inputText: 'hhh',
sex: 'boy',
checkArr: ['足球', '篮球', '排球'],
selected: ''
}
let proxyObj = {}
for (let key in obj) {
Object.defineProperty(proxyObj, key, {
get() {
return obj[key]
},
set(newVal) {
obj[key] = newVal
}
})
}
p[0].innerHTML = proxyObj.inputText
p[1].innerHTML = proxyObj.sex
p[2].innerHTML = proxyObj.checkArr
p[3].innerHTML = proxyObj.selected
然后就变成了这样
text.value = proxyObj.inputText
text.oninput = function(event) {
proxyObj.inputText = event.target.value
p[0].innerHTML = proxyObj.inputText
}
//radio
radio.forEach(item => {
if (item.getAttribute('value') === proxyObj.sex) item.checked = true
item.onchange = function(event) {
proxyObj.sex = event.target.value
p[1].innerHTML = proxyObj.sex
}
})
//checkbox
proxyObj.checkArr.forEach((item) => {
for (let i = 0; i < checkbox.length; i++) {
if (checkbox[i].getAttribute('value') === item) checkbox[i].checked = true
}
})
checkbox.forEach(item => {
item.onchange = function(event) {
if (event.target.checked) {
proxyObj.checkArr.push(event.target.value)
} else {
proxyObj.checkArr.forEach((checkItem, index) => {
if (item.getAttribute('value') === checkItem)
proxyObj.checkArr.splice(index, 1)
})
}
p[2].innerHTML = proxyObj.checkArr
}
})
//select
option.forEach((item) => {
if (item.value === proxyObj.selected)
item.selected = true
})
select.onchange = function(event) {
// console.log(event.target.selectedIndex);
proxyObj.selected = option[event.target.selectedIndex].value
p[3].innerHTML = proxyObj.selected
}
但是仔细一想,这样似乎只实现了DOM到数据的绑定,没有实现数据到DOM的绑定,很操蛋啊,而且对于checkbox元素,Vue在实现的时候可以是一个布尔值或者数组,而我这样实现只有数组的形式,所以又在这个基础上又完善了一遍。。。。
let obj = {
inputText : 'hhh',
sex : 'boy',
check : ['足球' , '篮球' , '排球'],
// check: true,
selected : ''
}
let proxyObj = {}
for(let key in obj) {
Object.defineProperty(proxyObj , key , {
get() {
return obj[key]
},
set(newVal) {
obj[key] = newVal
update()//数据变化时触发更新
}
})
}
//初始化函数
function init() {
setInput()
bindInput()
setRadio()
bindRadio()
setCheckbox()
bindCheckbox()
setOption()
bindSelect()
}
//选择元素
function selectElem(selectStr) {
return Array.from(document.querySelectorAll(selectStr))
}
//更新函数
function update() {
setInput()
setRadio()
setCheckbox()
setOption()
}
//设置input框,初始化和更新时使用
function setInput() {
p[0].innerHTML = proxyObj.inputText
text.value = proxyObj.inputText
}
// 给input框绑定事件
function bindInput() {
text.oninput = function(event) {
proxyObj.inputText = event.target.value
}
}
// 设置radio
function setRadio() {
radio.forEach(item => {
if(proxyObj.sex) {
if(item.getAttribute('value') === proxyObj.sex)
item.checked = true
}else {
item.checked = false
}
})
p[1].innerHTML = proxyObj.sex
}
// 绑定radio
function bindRadio() {
radio.forEach(item => {
item.onchange = function(event) {
proxyObj.sex = event.target.value
}
})
}
//判断是否为数组
function isArr(obj) {
return Array.isArray(obj)
}
// 判断数组是否为空
function isEmpty(check) {
return check.length === 0
}
// 设置checkbox的checked
function setCheckItem(bool) {
checkbox.forEach(item => {
item.checked = bool
})
}
// 设置checkbox
function setCheckbox() {
if(isArr(proxyObj.check)) {
if(!isEmpty(proxyObj.check)) {
proxyObj.check.forEach((item) => {
checkbox.forEach(checkItem => {
if(checkItem.getAttribute('value') === item)
checkItem.checked = true
})
})
}else {
setCheckItem(false)
}
}else {
if(proxyObj.check) {
setCheckItem(true)
}else {
setCheckItem(false)
}
}
p[2].innerHTML = proxyObj.check
}
// 绑定checkbox
function bindCheckbox() {
checkbox.forEach(checkItem => {
checkItem.onchange = function(event) {
if(isArr(proxyObj.check)) {
if(event.target.checked) {
proxyObj.check.push(event.target.value)
}else {
proxyObj.check.forEach((item , index) => {
if(checkItem.getAttribute('value') === item)
proxyObj.check.splice(index , 1)
})
}
p[2].innerHTML = proxyObj.check
}else {
proxyObj.check = event.target.checked
}
}
})
}
// 设置option
function setOption() {
option.forEach((item) => {
if(item.value === proxyObj.selected)
item.selected = true
})
p[3].innerHTML = proxyObj.selected
}
// 绑定select
function bindSelect() {
select.onchange = function(event) {
// console.log(event.target.selectedIndex);
proxyObj.selected = option[event.target.selectedIndex].value
}
}
init()
在这里不得不提一下,给checkbox元素绑定时的逻辑最为复杂。。。首先得判断绑定的值是否为数组,其次在判断数组是否为空,为空时就默认都不选中,不为空时就遍历checkbox节点选中value值为checkArr中的对应值,如果不是数组就判断传入的是true还是false(或者为空)如果是true就默认都选中,false就默认都不选中。在绑定事件时也要进一步判断。。。
然后我又要把它封装到一个类中,如下
class Bind {
_options = {}
constructor(options) {
this.options = options
this.dataProxy()
this.init()
}
init() {
this.setInput()
this.bindInput()
this.setRadio()
this.bindRadio()
this.setCheckbox()
this.bindCheckbox()
this.setOption()
this.bindSelect()
}
dataProxy() {
let self = this
for(let key in this.options) {
Object.defineProperty(self._options , key , {
get() {
return self.options[key]
},
set(newValue) {
self.options[key] = newValue
self.update()
}
})
}
}
update() {
this.setInput()
this.setRadio()
this.setCheckbox()
this.setOption()
}
setInput() {
this._options.p[0].textContent = this._options.inputValue
this._options.input.value = this._options.inputValue
}
bindInput() {
this._options.input.oninput = event => {
this._options.inputValue = event.target.value
}
}
setRadio() {
this._options.radio.forEach(item => {
if(this._options.radioValue) {
if(item.getAttribute('value') === this._options.radioValue)
item.checked = true
}else {
item.checked = false
}
})
p[1].textContent = this._options.radioValue
}
bindRadio() {
this._options.radio.forEach(item => {
item.onchange = event => {
this._options.radioValue = event.target.value
}
})
}
setOption() {
this._options.options.forEach(item => {
if(item.value === this._options.selectValue)
item.selected = true
})
p[3].textContent = this._options.selectValue
}
bindSelect() {
this._options.select.onchange = event => {
// console.log(event.target.selectedIndex);
this._options.selectValue = options[event.target.selectedIndex].value
}
}
isArr(obj) {
return Array.isArray(obj)
}
isEmpty(check) {
return check.length === 0
}
setCheckItem(bool) {
this._options.checkbox.forEach(item => {
item.checked = bool
})
}
setCheckbox() {
if(this.isArr(this._options.checkValue)) {
if(!this.isEmpty(this._options.checkValue)) {
this._options.checkValue.forEach((item) => {
this._options.checkbox.forEach(checkItem => {
if(checkItem.getAttribute('value') === item)
checkItem.checked = true
})
})
}else {
this.setCheckItem(false)
}
}else {
if(this._options.checkValue) {
this.setCheckItem(true)
}else {
this.setCheckItem(false)
}
}
this._options.p[2].textContent = this._options.checkValue
}
bindCheckbox() {
this._options.checkbox.forEach(checkItem => {
checkItem.onchange = event => {
if(this.isArr(this._options.checkValue)) {
if(event.target.checked) {
this._options.checkValue.push(event.target.value)
}else {
this._options.checkValue.forEach((item , index) => {
if(checkItem.getAttribute('value') === item)
this._options.checkValue.splice(index , 1)
})
}
this._options.p[2].textContent = this._options.checkValue
}else {
this._options.checkValue = event.target.checked
}
}
})
}
}
使用如下:
let radio = selectElem("input[type='radio']")
let checkbox = selectElem("input[type='checkbox']")
let options = selectElem('option')
let p = selectElem('p')
let select = document.querySelector('select')
let input = document.getElementById("text")
let bind = new Bind({
input,
inputValue: 'hhh',
radio,
radioValue: '',
select,
options,
selectValue:'',
checkbox,
checkValue: [],
p
})
很简陋,而且只能传一个元素,new一个对象只能处理一个input,一个radio数组,一个checkbox数组,和一个select元素。。。而且本来应该在类中进一步判断new Bind({options})时是否传入对应值,再执行对应的初始化和绑定。。。Vue底层是用发布者订阅者模式进行监听的,当数据发生变化就通知对应的订阅者触发更新。
最后,想说Vue是真的好用,一个v-model解决的事情我硬生生写了这么多代码,还只是个粗糙版,也印证了那句话,你用起来越简单的东西,一定是别人在底层帮你做了无数的事情。。。卒,希望努力提升自己的水平,不要做一个卑微的前端捞仔。。。
更多推荐
所有评论(0)