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解决的事情我硬生生写了这么多代码,还只是个粗糙版,也印证了那句话,你用起来越简单的东西,一定是别人在底层帮你做了无数的事情。。。卒,希望努力提升自己的水平,不要做一个卑微的前端捞仔。。。

Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐