演示效果
一、问题:在 new Vue()
的时候发生了什么?vue双向绑定是如何实现的?
回顾在vue中的用法:
new Vue({
el: '#app',
data: {
nickname: '双流儿',
age: 18
}
})
二、分析
- 在vue内部其实是使用的发布订阅模式,其中observe方法设置需要观察(监听)的数据,compile方法遍历dom节点(解析指令),拿到指令绑定的key,再根据key设置需要观察的数据和订阅管理器
- 在执行new操作的时候传入了el-需要挂载到的dom id,data-绑定的数据。
今天我们来实现一个v-model、v-text(包括{{ xxx }}) - dom结构
<div id="app">
<h1>昵称</h1>
<div v-text="nickname"></div>
<input type="text" v-model="nickname">
<br>
<h1>年龄</h1>
<div>{{ age }}</div>
<input type="text" v-model="age">
</div>
- vue2实例
new Vue({
el: '#app',
data: {
nickname: '双流儿',
age: 18
}
});
三、定义一个Vue类
class Vue {
constructor({ el, data }) {
// 获取dom
this.$el = document.querySelector(el);
// 监听(观察)的数据
this.$data = data || {};
// 订阅每个key(订阅管理器)
this.$directives = {};
this.observe(this.$data);
this.compile(this.$el);
}
}
四、监听器observe
// 设置监听数据
observe(data) {
const _this = this;
for (const key in data) {
// 当前每项的value
let value = data[key];
if (typeof value === 'object') this.observe(value);
Object.defineProperty(data, key, {
enumerable: true, // 设置属性可枚举
configurable: true, // 设置属性可删除
get() {
return value;
},
set(newValue) {
// 新的值与原来的值相等就不用执行以下(更新)操作
if (newValue === value) return;
value = newValue;
// 监听到值改变后更新对应指令的数据
_this.$directives[key].forEach(fn => {
fn.update();
});
}
});
}
}
五、解析器compile
// 设置指令(设置每个订阅者)
setDirective(node, key, attr) {
const watcher = new Watcher({ node, key, attr, data: this.$data });
if (this.$directives[key]) this.$directives[key].push(watcher);
else this.$directives[key] = [watcher];
}
// 解析器-遍历拿到dom上的指令(这里其实是把指令当做自定义属性来处理)
compile(dom) {
const _this = this;
const reg = /\{\{(.*)\}\}/; // 来匹配{{ xxx }}中的xxx
const ndoes = dom.childNodes; // 节点集
// ndoes是类数组对象不能使用es迭代器,需要转成数据
Array.from(ndoes).forEach(node => {
// 如果node还有子项,执行递归
if (node.childNodes.length) _this.compile(node);
// 在本例中使用nodeType来判断是什么类型,如nodeType为3时表示node的子节点有且仅有一个文本类型,也就是{{ xxx }}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
const key = RegExp.$1.trim(); // $1获取reg匹配到的第一个值
// 声明 {{ xxx }} 为text类型
_this.setDirective(node, key, 'nodeValue');
}
}
if (node.nodeType === 1) {
// v-text
if (node.hasAttribute('v-text')) {
const key = node.getAttribute('v-text'); // key就是实例化Vue是传入的nickname/age
node.removeAttribute('v-text'); // 移除node上的自定义属性
_this.setDirective(node, key, 'textContent');
}
// v-model 且node必须是input标签
if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
const key = node.getAttribute('v-model'); // key就是实例化Vue是传入的nickname/age
node.removeAttribute('v-model'); // 移除node上的自定义属性
_this.setDirective(node, key, 'value');
// 设置input事件监听
node.addEventListener('input', e => {
_this.$data[key] = e.target.value;
});
}
}
});
}
六、观察者Watcher
class Watcher {
constructor({ node, key, attr, data }) {
this.node = node; // 指令对应的DOM节点
this.key = key; // data的key
this.attr = attr; // 绑定的html原生属性,本例v-text对应textContent
this.data = data; // 监听的数据
this.update(); // 初始化更新数据
}
// 更新
update() {
this.node[this.attr] = this.data[this.key];
}
}
以上使用 Object.defineProperty
来实现数据劫持,那么怎么使用ES6的Proxy代理数据呢?
我们只需要修改 observe
方法
七、使用Proxy实现监听器
observe(data) {
const _this = this;
this.$data = new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
const status = Reflect.set(target, key, value);
if (status) {
// 当status为true时,表示数据已经改变
_this.$directives[key].forEach(fn => {
fn.update();
});
}
return status;
}
});
}
八、Object.defineProperty vs Proxy
从上可以看出,在使用Object.defineProperty时,需要递归遍历data中的每个属性,Proxy不需要,所以Proxy性能会优于Object.defineProperty,这就是说vue3初始化比vue2性能更好的原因之一。
九、在vue3中实现数据双向绑定
思路同上,这里是把Vue作为一个对象
class Watcher {
constructor({ node, key, attr, data }) {
this.node = node; // 指令对应的DOM节点
this.key = key; // data的key
this.attr = attr; // 绑定的html原生属性,本例v-text对应textContent
this.data = data; // 监听的数据
this.update(); // 初始化更新数据
}
// 更新
update() {
this.node[this.attr] = this.data[this.key];
}
}
const Vue = {
$data: {},
$directives: {},
createApp({ data }) {
const _this = this;
this.$data = new Proxy(typeof data === 'function' ? data(): data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
const status = Reflect.set(target, key, value);
if (status) {
// 当status
_this.$directives[key].forEach(fn => {
fn.update();
});
}
return status;
}
});
return this;
},
mount(el) {
this.$el = document.querySelector(el);
this.compile(this.$el);
},
// 设置指令(设置每个订阅者)
setDirective(node, key, attr) {
const watcher = new Watcher({ node, key, attr, data: this.$data });
if (this.$directives[key]) this.$directives[key].push(watcher);
else this.$directives[key] = [watcher];
},
compile(dom) {
const _this = this;
const reg = /\{\{(.*)\}\}/; // 来匹配{{ xxx }}中的xxx
const ndoes = dom.childNodes; // 节点集
// ndoes是类数组对象不能使用es迭代器,需要转成数据
Array.from(ndoes).forEach(node => {
// 如果node还有子项,执行递归
if (node.childNodes.length) _this.compile(node);
// 在本例中使用nodeType来判断是什么类型,如nodeType为3时表示node的子节点有且仅有一个文本类型,也就是{{ xxx }}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
const key = RegExp.$1.trim(); // $1获取reg匹配到的第一个值
// 声明 {{ xxx }} 为text类型
_this.setDirective(node, key, 'nodeValue');
}
}
if (node.nodeType === 1) {
// v-text
if (node.hasAttribute('v-text')) {
const key = node.getAttribute('v-text'); // key就是实例化Vue是传入的nickname/age
node.removeAttribute('v-text'); // 移除node上的自定义属性
_this.setDirective(node, key, 'textContent');
}
// v-model 且node必须是input标签
if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
const key = node.getAttribute('v-model'); // key就是实例化Vue是传入的nickname/age
node.removeAttribute('v-model'); // 移除node上的自定义属性
_this.setDirective(node, key, 'value');
// 设置input事件监听
node.addEventListener('input', e => {
_this.$data[key] = e.target.value;
});
}
}
});
}
};
const obj = {
data() {
return {
nickname: '双流儿',
age: 18
}
}
}
Vue.createApp(obj).mount('#app');
- vue3实例
const obj = {
data() {
return {
nickname: '双流儿',
age: 18
}
}
}
Vue.createApp(obj).mount('#app');
总结
不管哪种思路都需要:
- 观察者observe
- 解析器compile
- 监听器Watcher