vue2.x和vue3.x双向绑定原理的区别
vue作为mvvm模式的框架,双向绑定一直是它的一个亮点,在vue2.x的使用过程中都曾遇到过很多坑点,为了避免这些坑又做了许多不必要的操作。那么为什么会有这些问题呢?我们先来看下vue2.x的原理,相比这个有了解的都知道是Object.defineProperty(),这里不多说直接上代码:
<!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>Vue 2.x双向绑定原理</title>
</head>
<body>
<!--
Vue 2.x 双向绑定原理,是利用了Object.defineProperty的存取描述符中的get和set
-->
<input type="text" id="app">
<p id="content"></p>
</body>
<script>
const obj = {};
let val = '';
Object.defineProperty(obj, 'value', {
get() {
return val;
},
set(nVal) {
val = nVal;
document.getElementById('content').innerHTML = val;
}
});
document.getElementById('app').oninput = (e) => {
obj.value = e.target.value;
}
</script>
</html>
vue2.x的双向绑定是由Object.defineProperty()实现的,缺点也是Object.defineProperty()所带来的,vue3.x采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
话不多说上代码:
<!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>Vue 3.x双向绑定原理</title>
</head>
<body>
<input type="text" id="app">
<p id="value"></p>
</body>
<script>
const obj = {};
const proxyObj = new Proxy(obj, {
get(target, key) {
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
document.getElementById('value').innerHTML = target[key];
}
});
document.getElementById('app').oninput = (e) => {
proxyObj[0] = e.target.value;
}
</script>
</html>
乍看之下只是改了监听的方式,但是使用proxy后提升了数组内部等definepropty的缺点,
除此之外还有以下优点:
1 defineProperty只能监听某个属性,不能对全对象监听
2 可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可)
3 可以监听数组,不用再去单独的对数组做特异性操作
Vue3.x双向绑定原理深入解析
下边我们再来对Vue3.x双向绑定原理深入解析,会对这个发布者和订阅者更加清晰了解,同样话不多说上代码,代码能讲解很多内容:
custom.vue.js 文件
/**
* 自定义vue.js 深入解析vue3.x如何利用proxy来实现双向绑定原理的
*/
// vue构造函数
class Vue {
constructor(options) {
// 挂载节点
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
// 所有订阅者集合,目标格式(一对多的关系)
this.deps = {};
// 调用观察者
this.observer(this.$data);
// 调用指令解析器
this.compile(this.$el);
}
}
// 指令解析器
Vue.prototype.compile = function (el) {
let els = el.children;
for (let i = 0; i < els.length; i++) {
const domEle = els[i];
if (domEle.children.length) {
// 递归获取子节点
this.compile(domEle);
}
// 当节点存在v-model指令
if (domEle.hasAttribute('v-model')) {
let attrVal = domEle.getAttribute('v-model');
domEle.addEventListener('input', (() => {
// 添加一个订阅者
this.deps[attrVal].push(new Watcher(domEle, "value", this, attrVal));
let thisElement = domEle;
return () => {
// 更新数据层的数据
this.$data[attrVal] = thisElement.value;
}
})());
}
if (domEle.hasAttribute('v-html')) {
let attrVal = domEle.getAttribute('v-html');
// 添加一个订阅者
this.deps[attrVal].push(new Watcher(domEle, 'innerHTML', this, attrVal));
}
if (domEle.innerHTML.match(/{{([^\{|\}]+)}}/)) {
//获取插值表达式内容 - 这里针对单个,表达式做例子
let attrVal = domEle.innerHTML.replace(/[{{|}}]/g, '');
this.deps[attrVal].push(new Watcher(domEle, "innerHTML", this, attrVal)); //添加一个订阅者
}
if (domEle.hasAttribute('v-on:click')) {
let attrVal = domEle.getAttribute('v-on:click');
// 将this指向改为this.$data
domEle.addEventListener('click', this.$methods[attrVal].bind(this.$data));
}
}
}
// 定义观察者,2.x和3.x的区别也就是在这一块
Vue.prototype.observer = function (data) {
const _this = this;
for (const key in data) {
_this.deps[key] = [];
}
let handler = {
get (target, property) {
return target[property];
},
set (target, key, value) {
let res = Reflect.set(target, key, value);
const watchers = _this.deps[key];
watchers.map(item => {
item.update();
});
return res;
}
}
this.$data = new Proxy(data, handler);
}
// 定义阅读者
class Watcher {
constructor(el, attr, vm, attrVal) {
this.el = el;
this.attr = attr;
this.vm = vm;
this.val = attrVal;
// 更新视图
this.update();
}
}
// 更新视图
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.val];
}
使用:
<!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>深入解析vue3.x双向绑定</title>
<script src="./custom.vue.js"></script>
</head>
<body>
<div id="app">
姓名:
<input type="text" v-model="username">
<p v-html="username"></p>
年龄:
<input type="text" v-model="age">
<p><span>{{age}}</span>岁</p>
<button v-on:click="handler">Click!!!</button>
<h3>{{msg}}</h3>
</div>
</body>
<script>
const vm = new Vue({
el: '#app',
data: {
username: '',
age: '',
msg: ''
},
methods: {
handler() {
console.log(`我是${this.username},我有${this.age}岁`);
this.msg = `我是${this.username},我有${this.age}岁`;
}
}
})
</script>
</html>
实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者
以上内容来自网络学习和整理,如有问题欢迎讨论