问题形容
面试中,面试官除了问基础知识以外,还喜爱问一些框架原理。比方:你对vue的数据双向绑定mvvm是如何了解的?
网上的局部贴子可能写的有点形象,不便于疾速浏览了解。本篇文章就应用通俗易懂的简略形式,来解说并实现一个简略的vue数据双向绑定原理demo,心愿对大家有肯定的帮忙
先温习基本知识
为了便于大家更好的了解下文数据双向绑定的代码,咱们最好先温习一下旧常识,如果基础知识扎实的道友,能够间接跳过这一段。
DOM.children属性返回DOM元素有哪些元素子节点
代码:
<body> <div class="divClass"> <span>孙悟空</span> <h4>猪八戒</h4> <input type="text" value="沙和尚"> </div> <script> let divBox = document.querySelector('.divClass') console.log('元素节点', divBox); console.log('元素节点的子节点伪数组', divBox.children); </script></body>
示例图:
留神辨别:DOM.childNodes失去所有的节点
,比方元素节点、文本节点、正文节点;而,DOM.children只失去所有的元素节点
。二者返回的都是一个伪数组,但伪数组有length长度,代表有多少个节点,且能够循环遍历,遍历的每一项都是一个dom元素标签!
不过伪数组不能应用数组的办法
DOM.hasAttribute(key)/getAttribute(key)判断元素标签是否有key属性以及拜访对应value值
代码:
<body> <h3 class="styleCss" like="coding" v-bind="fire in the hole">穿梭前线</h3> <script> let h3 = document.querySelector('h3') console.log(h3.hasAttribute('v-hello')); // 看看此标签有没有加上v-hello这个属性,没的,故打印:false console.log(h3.hasAttribute('like')); // 看看此标签有没有加上like这个属性,有,故打印:true console.log(h3.getAttribute('like')); // 拜访此标签上加上的这个v-bind属性值是啥,打印:coding console.log(h3.hasAttribute('v-bind')); // 看看此标签有没有加上v-bind这个属性,,有的,故打印:true console.log(h3.getAttribute('v-bind')); // 拜访此标签上加上的这个v-bind属性值是啥,打印:fire in the hole console.log(h3.attributes); // 能够看到所有的在标签上绑定的属性名和属性值(key="value"),是一个伪数组 </script></body>
示例图:
这两个api能够用来看标签上是否绑定了vue的指令,以及看看vue指令值是啥,以便于咱们去与data中的相应数据做对应
DOM.innerHTML与DOM.innerText的区别
二者均能够批改dom的文本内容。innerHTML是合乎W3C规范的属性,所以是支流应用的dom的api。而innerText尽管兼容性要好一些,不过支流还是innerHTML
代码:
<body> <h3>西游记</h3> <button>更改dom内容</button> <script> let h3 = document.querySelector('h3') let btn = document.querySelector('button') btn.onclick = () => { h3.innerHTML = h3.innerHTML + '6' } </script></body>
示例图:
DOM.innerHtml这个api可用于更改vue中的差值表达式{{key}}对应的内容值
数据双向绑定成品效果图
咱们先看一下,咱们所要实现的成品的效果图
需要剖析
- 输入框输出值内容发生变化,页面也产生对应变动
- 点击按钮,输入框和页面都产生对应变动
即: - 页面变动(输入框引起)触发数据data变动,最终触发页面变动;
- 数据data变动(按钮引起),触发页面变动
对于MVVM的了解
简略了解
mvvm即为m v vm别离对应的是:
- m是model数据层(就是vue中的data、computed、watch啊之类的数据配置项)
- v是view视图层(视图层成果是dom重叠进去的,所以视图层能够了解为dom元素)
- vm是model数据层和view视图层的中间层view_model(vm)层,是vue中的外围,功能强大
vm能够监听视图层dom的变动
,比方监听input标签dom的value值变动,去更改model数据层中的data对应值,vm也能够监听model数据层中的data对应key的value的值的变动,
去更改input标签dom的value值。即:vm相当于一个摆渡人,可把彼岸人渡到此岸、此岸人渡到彼岸
外围了解
所以,MVVM的外围是,所以,MVVM的外围是,所以,MVVM的外围是(重要的事件说三遍:)
监听页面的DOM的内容值变动,从而告诉到data中做对应数据变动(次要是监听表单标签)
监听表单标签的变动,是应用dom.addEventListener()这个办法
当data中数据变动当前,再去及时更新页面DOM的内容变动
监听data中数据的变动,是应用Object.defineProperty()的set办法,主动帮咱们监听变动,至于更新dom,就是首先找到要更新哪个dom,如果是一般标签就更新其innerHTML值、如果是表单标签,就更改其value即可
对于Object.defineProperty的了解
对于Object.defineProperty这个办法,一言以蔽之,给对象定义响应式。论坛有很多材料帖子,在此不赘述。举荐看官网文档:https://developer.mozilla.org...
对于这个办法,咱们先了解上面案例就差不多了:
案例需要
有一个对象obj,外面有name和age属性,要让这个obj的每一个属性,都是响应式的,拜访和批改的时候,都要对应打印信息。
案例代码
复制粘贴跑一下,大抵就明确了
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title></head><body> <button id="nameId">批改名字</button> <button id="ageId">批改年龄</button> <script> let obj = { name: '孙悟空', age: 500, } for (const key in obj) { // 因为是给对象中每一个属性都增加响应式,所以要遍历对象 let value = obj[key] // 存一份对应value值,用于拜访返回,以及新值批改赋值 Object.defineProperty(obj, key, { // 给这个obj对象的每一个属性名key都定义响应式 get() { console.log('拜访之(主动触发),拜访值为:', value); return value }, set(newVal) { console.log('批改之(主动触发),批改的属性名为:', key, '属性值为:', newVal); value = newVal } }) } let nameBtn = document.querySelector('#nameId') let ageBtn = document.querySelector('#ageId') nameBtn.onclick = () => { obj.name = obj.name + '^_^ | ' } ageBtn.onclick = () => { obj.age = obj.age + 1 } // 这样的话,拜访和批改的时候都会触发啦(批改的时候是要先拜访找到,再去批改,故打印两次) </script></body></html>
案例效果图
残缺代码
代码中写了不少正文,大家跟着正文步骤浏览应该就能够了。演示的话间接复制粘贴即可。留神代码中的subArr,收集依赖,目标是看看有哪些dom元素须要做后续的响应式更新内容
打印new进去的Vue实例
如果下方的残缺代码,有助于各位道友更好的了解mvvm的话,那就给咱点个赞激励一下创作呗^_^
残缺MVVM代码
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> #app { width: 600px; height: 216px; background-color: #ccc; padding: 24px; } button { cursor: pointer; } </style></head><body> <!-- view视图层dom,为了便于了解,这里以#app的根元素外部只有一层dom为例(多层须要递归) --> <div id="app"> <input v-model="name" placeholder="请填写名字"> <span>名字是:</span><span v-bind="name"></span> <br> <br> <input v-model="age" placeholder="请填写年龄"> <span>年龄是:</span><span v-bind="age"></span> <br> <br> <h3>{{name}}</h3> <h3>{{age}}</h3> <button id="nameId">更改名字</button> <button id="ageId">更改年龄</button> <button id="resetId">复原默认</button> <button id="removeId">全副清空</button> </div> <script> // 简略函数封装 之 判断标签内是否蕴含双差值表达式 function isIncludesFourKuoHao(str) { // 不过这里不是特地谨严。谨严须要应用正则限度,大家明确思路即可 if (str.length <= 4) { // 得大于4个字符 return false } if ( // 且要有双差值表达式, str[0] == '{' & str[1] == '{' & str[str.length - 1] == '}' & str[str.length - 2] == '}' ) { return true } else { return false } } // 简略函数封装 之 获取双差值表达式之间的变量名 function getKuoHaoBetweenValue(params) { // 这里也不是特地谨严,谨严也须要应用正则,大家明确思路即可 return params.slice(2, params.length - 2) // {{name}} --> name } // 这里应用构造函数,使之领有new的性能。当然也能够应用class类编程 function Vue(options) { /** * 第一步,获取根节点dom元素,这一步的作用是有了根节点dom当前,能够通过dom.children获取其所有子节点的dom元素, * 便于咱们对子节点的dom进行操作,比方给子节点的input标签绑定input事件监听,这样就能够通过dom.value * 实时拿到用户在输入框输出的值了 * */ this.$el = document.querySelector(options.el); /** * 第二步,把data中的数据{name:'jack',age:'500'}存一份,因为咱们除了批改this.name要是响应式的,同样: * this.$data.name也要是响应式的 */ this.$data = options.data; /** * 第三步,定义一个数组收集要变动的dom元素,当咱们批改data中数据的时候,触发Object.defineProperty()的set办法执行 * 而后去subArr数组中去寻找,看看是要批改那个dom元素的数据值即可,大家打印一下,就会发现subArr寄存的是一个又 * 一个对象,对象中记录的是 哪一个dom,什么属性名key,以及对应更改innerHTML或value * */ this.subArr = [] /** * 第四步,执行模板编译操作,把data中的数据做页面出现。这里又分为两局部 * 4.1 给相应的交互输出类标签绑定事件监听,比方input标签绑定input事件,select标签绑定change事件等。为便于了解 * 本案例中只以input标签为例阐明(当然前提是:加了v-model指令做数据双向绑定才会去操作这一步) * 4.2 把v-bind和插值表达式{{}}做内容出现,即:把model中的对应数据值,并找到对应dom,更改其innerHTML的值为对应数据值 * */ this.useDataToCompileRenderPage(); // 应用data中的数据做模板编译并渲染到页面上 /** * 第五步,给m中的数据应用Object.defineProperty做数据劫持,这样的话,拜访或者批改对象的属性值时,都能够得悉。即: * 拜访时,不必额定操作。不过批改时,model中的data的值变动了,于此同时,还需同时更新dom,因为m变v也要跟着变 * 即:dataChangeUpdatePage办法的执行,只有一set更新,我就让dataChangeUpdatePage办法去更新对应的dom值 * (因为第四步当前,data中数据是渲染到页面上了,但还需让data中的数据变动,页面也跟着变动,故要做数据劫持) * */ this.definePropertyAllDataKey(); // 数据劫持data中的所有key使之成为响应式的 } // 先把data中的数据,去编译渲染到页面上 Vue.prototype.useDataToCompileRenderPage = function () { let _this = this; // 存一份this实例对象 let nodes = this.$el.children; // 获取根元素下的所有的子节点dom;值为伪数组,打印后果:[input, span, span, br, br, input, span, span, br, br, button] for (let i = 0; i < nodes.length; i++) { // 循环这个子节点dom伪数组, let node = nodes[i]; // 所有的标签,一个一个去判断,判断这个标签有没有加上v-model,有没有加上v-bind,有没有差值表达式{{}} ,以这三种状况为例 // 若dom标签节点上加上了v-model指令 if (node.hasAttribute('v-model')) { let dataKey = node.getAttribute('v-model');// 去获取v-model绑定的那个属性值,本例中为dataKey的值别离为:name、age node.addEventListener('input', function () { // 以input输入框为例:给标签绑定input输出事件监听,即:<input/>.addEventListener('input',function(){}) /** 留神,这里是页面到数据的解决,即v --> vm --> m的流程 */ _this.$data[dataKey] = node.value; // 如果是input标签,能够间接通过inputDom.value获取到input标签中用户输出的值 _this[dataKey] = node.value; // 上一行是$data更改,即:this.$data.name或age获取dom最新的值、这一行是this.name或age获取最新的值 }); /** 把model中的数据更新赋值(编译)到页面上 */ node['value'] = _this.$data[dataKey]; // inputDom.value = this.$data.name或age 赋值 /** 所以,通过这一波操作,胜利的把输入框(变动)的值,更改到数据层中了 即:v --> vm --> m */ /** 留神这里,就是收集依赖,能够提取一个办法的,为了便于了解,就不提取了 */ _this.subArr.push({ nodeLabelDom: node, // 哪个dom标签元素 whichAttribute: dataKey, // 哪一个属性name或age valueOrInnerHtml: 'value', // 更改value还是innerHTML }) } // 若dom标签节点上加上了v-bind指令 if (node.hasAttribute('v-bind')) { /** 如果是v-bind指令,只须要增加watcher即可 * */ let dataKey = node.getAttribute('v-bind'); // 去获取v-bind绑定的那个属性值,本例中为dataKey的值别离为:name、age node['innerHTML'] = _this.$data[dataKey]; // normalDom.innerHtml = this.$data.name或age 一般dom显示赋值操作 /** 留神这里,就是收集依赖,能够提取一个办法的,为了便于了解,就不提取了 */ _this.subArr.push({ nodeLabelDom: node, // 哪个dom标签元素 whichAttribute: dataKey, // 哪一个属性name或age valueOrInnerHtml: 'innerHTML', // 更改value还是innerHTML }) } // 如果蕴含双差值表达式{{}} if (isIncludesFourKuoHao(node.textContent)) { let dataKey = getKuoHaoBetweenValue(node.textContent) // 就拿到双差值表达式两头的key,属性名,这里的dataKey别离为:name、age node['innerHTML'] = _this.$data[dataKey]; // 把双差值表达式中的key做一个替换对应值 /** 留神这里,就是收集依赖,能够提取一个办法的,为了便于了解,就不提取了 */ _this.subArr.push({ nodeLabelDom: node, // 哪个dom标签元素 whichAttribute: dataKey, // 哪一个属性name或age valueOrInnerHtml: 'innerHTML', // 更改value还是innerHTML }) } } } // 再做数据劫持,遍历给data中的每一个数据都劫持,使之,都用于set和get办法 Vue.prototype.definePropertyAllDataKey = function () { let _this = this; // 存一份this以便应用 for (let key in _this.$data) { // 遍历对象{name:'孙悟空',age: 500} let value = _this.$data[key]; // value值为孙悟空、500 key的值天然是name和age Object.defineProperty(_this.$data, key, { // 应用defineProperty去增加拦挡、劫持(劫持到$data身上) get: function () { // return value; // 拜访key,拜访name或者age,就返回对应的值 }, set: function (newVal) { value = newVal; // 批改key的属性值,批改name或者age的属性值,在做失常操作value = newVal赋值的同时 // 每当更新this.$data数据时,如:this.$data.name = 'newVal'就去做对应dom的更新即可 _this.dataChangeUpdatePage(key, newVal) } }) Object.defineProperty(_this, key, { // 劫持到本人身上 get: function () { return value; }, set: function (newVal) { value = newVal; // 每当更新this数据时,如:this.name = 'newVal'就去做对应dom的更新即可 _this.dataChangeUpdatePage(key, newVal) } }) } } // 公共办法,当更新触发的时候,去依据数据做页面渲染 Vue.prototype.dataChangeUpdatePage = function (key, newVal) { let _this = this; // 存一份this实例对象 // 也要去更新对应dom的内容 _this.subArr.forEach((item) => { if (key == item.whichAttribute) { // 哪个dom的 // innerText或者value // 赋新值 item.nodeLabelDom[item.valueOrInnerHtml] = newVal; } }) } let vm = new Vue({ el: '#app', // 指定vue的根元素 /** * model数据层,为了便于了解,这里也是举例data中数据只有一层,多层须要递归 * */ data: { name: '孙悟空', age: 500, } }); console.log('vmvm', vm); // 更改名字 let nameBtn = document.querySelector('#nameId') nameBtn.onclick = () => { vm.name = vm.name + '^' // 间接拜访 } // 更改年龄 let ageBtn = document.querySelector('#ageId') ageBtn.onclick = () => { vm.$data.age = vm.$data.age * 1 + 1 // 通过$data间接拜访 } // 复原默认的名字和年龄 let resetBtn = document.querySelector('#resetId') resetBtn.onclick = () => { vm.$data.name = '孙悟空' vm.age = 500 } // 清空名字和年龄 let removeBtn = document.querySelector('#removeId') removeBtn.onclick = () => { vm.name = '' vm.$data.age = null } </script></body></html>
好忘性不如烂笔头,记录一下呗