数据双向绑定
① 明确: vue 双向绑定时通过 js Object.defineProperty 来实现的
② 特征:每个 属性都有 get 和 set 方法
一 、 Object.defineProperty
① vue 是通过 js 的 Object.defineProperty 来实现数据劫持的,当数据读写时分别触发 get/set 方法
② 语法
Object.defineProperty (带监听的对象,带监听的属性,{ get(){}, set(){} })
③ 仿数据绑定(简单版)
<input type="text" name="" id="inputObj"> <script> let dataObj ={ msg:'cgw' } // 1.拦截数据 let tmpData; Object.defineProperty(dataObj,'msg',{ get(){ return tmpData; }, set(newData){ tmpData = newData; // 2.将数据同步到视图 let inputObj = document.querySelector('#inputObj'); inputObj.value = newData; } }) // 3.将数据同步到模型 let inputObj = document.querySelector('#inputObj'); inputObj.onkeyup = () => { dataObj.msg = inputObj.value; } </script>
④ 面试题
*1- 双向绑定原理是什么
答:通过 js 的 Object.defineProperty 实现数据的劫持
二 、vue 双向绑定原理(完整版)
① 核心原理
② 代码(完整版)
<div id="app"> <input type="text" v-model="username"> {{username}} </div> <script> function Watcher (vm, node, name, nodeType) { this.name = name; this.node = node; this.vm = vm; this.nodeType = nodeType; this.update = function() { if (this.nodeType == 'text') { this.node.nodeValue = this.vm.data[this.name]; } if (this.nodeType == 'input') { this.node.value = this.vm.data[this.name]; } } Dep.target = this; } function Dep () { this.subs = [] this.addSub = function(sub) { this.subs.push(sub); }, this.notify = function() { this.subs.forEach(function(sub) { sub.update(); }); } } //记录所有绑定数据的节点 // let watchers = []; function observer(vm) { Object.keys(vm.data).forEach(function(key){ let dep = new Dep() let value = vm.data[key] Object.defineProperty(vm.data, key,{ get(){ // 添加订阅者 watcher 到主题对象 Dep dep.addSub(Dep.target) return value }, set(newValue){ if(newValue === value) return value = newValue //通过该主题的所有订阅者更新数据 dep.notify(); } }) }) } //编译解析指令 function compile(node, vm) { let reg = /\{\{(.*)\}\}/g //正则匹配页面指令 //元素节点 if(node.nodeType === 1) { let attr = node.attributes; //解析节点的属性 for(let i = 0;i < attr.length; i++) { if(attr[i].nodeName == 'v-model') { //------------------------------- node.addEventListener('input',function(e){ vm.data[name] = e.target.value; }); //----------------------------- let name = attr[i].nodeValue //获取v-model绑定的属性名 //记录绑定数据的元素节点 // watchers.push({ name: name, node:node, nodeType: 'input', vm:vm }) new Watcher(vm, node, name, 'input') node.value = vm.data[name] //将data中的值赋给该节点 node.removeAttribute('v-model'); //解析完毕不要出现vue指令 } } } //如果节点类型为text if(node.nodeType === 3) { if(reg.test(node.nodeValue)) { let name = RegExp.$1;//获取匹配到的字符串 name = name.trim(); //记录绑定数据的文本节点 // watchers.push({ name:name, node:node, nodeType: 'text', vm:vm }) new Watcher(vm, node, name, 'text'); node.nodeValue = vm.data[name]; } } } function Vue(options) { //初始化 let el = options.el this.data = options.data //监听模型数据变化 observer(this) //将挂载目标劫持 -> 存到节点容器中(数据处理) -> 再放到挂载目标中 let flag = document.createDocumentFragment() let child let dom = document.querySelector(el); while(child = dom.firstChild){ compile(child, this) //挂载目标中的节点挨个过滤(解析指令) flag.appendChild(child) //放到DocumentFragment页面渲染就不会显示/劫持 } dom.appendChild(flag)//将DocumentFragment放到挂载目标中 } let vm = new Vue({ el:'#app', data:{ username:'webopenfather', age: 5 } }) </script>