共计 9515 个字符,预计需要花费 24 分钟才能阅读完成。
问题形容
面试中,面试官除了问基础知识以外,还喜爱问一些框架原理。比方:你对 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>
好忘性不如烂笔头,记录一下呗