3分钟带你搞懂Vue双向绑定原理及问题剖析
一、什么是Vue双向绑定?
所谓双向绑定,指的是vue实例中的data与其渲染的DOM元素的内容保持一致,无论谁被改变,另一方会相应的更新为相同的数据。
二、Vue的双向绑定
⌚双向绑定的原理
Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图
通过Object.defineProperty()
来劫持各个属性的setter, getter
,在数据发生变动时通知Vue实例,触发相应的getter和setter回调函数。
当把一个普通 Javascript 对象传给Vue 实例来作为它的 data 选项时, Vue 将遍历它的属性,用Object.defineProperty
将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
⚡双向绑定的核心
关于VUE双向数据绑定,其核心是 Object.defineProperty() 方法
Object.defineProperty(obj, prop, descriptor)
这个方法内有三个参数,分别为 obj(要定义其上属性的对象)、prop(要定义或修改的属性)、descriptor(具体的改变方法)
简单来说,就是用这个方法定义一个值,当调用时我们使用了它里面的get方法,当我们给这个属性赋值时,同时又调用了里面的set方法
let obj = {}
Object.defineProperty(obj,'youjia',{
get:function(){
console.log('调用了get方法')
},
set:function(){
console.log('调用了set方法')
}
})
obj.youjia;
obj.youjia = 'jiayou'
三、单向绑定与双向绑定的区别,适合的场景?
❇️单向绑定
单向绑定的优点是相应的可以带来单向数据流,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于应用的可维护性。
缺点则是代码量会相应的上升,数据的流转过程变长,从而出现很多类似的样板代码。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得啰嗦及繁琐。
♻️双向绑定
优点是在表单交互较多的场景下,会简化大量业务无关的代码。
缺点就是由于都是“暗箱操作”,我们无法追踪局部状态的变化(虽然大部分情况下我们并不关心),潜在的行为太多也增加了出错时 debug 的难度。同时由于组件数据变化来源入口变得可能不止一个,新手玩家很容易将数据流转方向弄得紊乱,如果再缺乏一些“管制”手段,最后就很容易因为一处错误操作造成应用雪崩。
四、简单实现一个JS双向绑定
✅核心源码
<!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>
<div id="app">
<h1>演示JS实现Vue双向绑定</h1>
<p>
<label>请输入内容:</label>
<input type="text" id="txt">
</p>
<p>
<label>显示的内容:</label>
<span id="show"></span>
</p>
</div>
</body>
</html>
<script type="text/javascript">
var obj = {};
Object.defineProperty(obj, 'txt', {
get: function() {
return obj;
},
set: function(val) {
document.getElementById("txt").value = val;
document.getElementById("show").innerText = val;
}
})
document.addEventListener("keyup", function (e) {
obj.txt = e.target.value;
})
</script>
五、Vue.js实现简单双向绑定
⏰效果图
✅核心源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>演示Vue实现双向绑定</h1>
<p>
<label>请输入内容:</label>
<input type="text" v-model="msg">
</p>
<p>
<label>显示的内容:</label>
<span>{{msg}}</span>
</p>
</div>
</body>
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 引入axios-->
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script type="text/javascript">
new Vue({
el:"#app",
data() {
return {
msg:''
}
},
mounted(){
},
methods: {
}
})
</script>
</html>
✅问题解决
第一个按钮修改后无法显示是因为直接用下标赋值会出现数据不一致情况,可见,数组中的元素已经修改,但页面元素无法显示
解决方案如下
解决方案是通过$set方法来设置数组的值,该方法有三个参数,第一个是要被设置值的目标对象,第二个是设置值在数组中的索引,第三个是设置的值
this.$set(obj, index, value)
第二个按钮就是这样实现了数组的内容修改以及双向绑定的正确显示
♨️对象无法双向绑定的解决方案
✨对象双向绑定解决效果图
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>修改对象的属性,但页面中没有发生改变</h1>
{{obj.name}}
<button @click="clk1">修改对象的属性1</button>
<button @click="clk2">修改对象的属性2</button>
</div>
</body>
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 引入axios-->
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script type="text/javascript">
new Vue({
el:"#app",
data() {
return {
obj:{
//初始化属性
name: ''
}
}
},
mounted(){
},
methods: {
clk1() {
console.log("修改对象的属性,未发生变化!");
this.obj.name = 'zhangsan';
console.log(this.obj);
},
clk2() {
this.$set(this.obj, "name", "zhangsan");
console.log("属性添加成功~")
console.log(this.obj);
}
}
})
</script>
</html>
✅问题解决
由于在data函数中未定义对象的属性,所以导致双向绑定失败!
解决方案如下
1、在data函数中的对象初始化对象的属性
new Vue({
el:"#app",
data() {
return {
obj:{
//初始化属性
name: ''
}
}
},
mounted(){
},
methods: {
clk1() {
console.log("修改对象的属性,未发生变化!");
this.obj.name = 'zhangsan';
console.log(this.obj);
},
})
2、使用$set设置属性
clk2() {
this.$set(this.obj, "name", "zhangsan");
console.log("属性添加成功~")
console.log(this.obj);
}