Vue2双向绑定
参考视频
https://www.bilibili.com/video/BV1934y1a7MN/?spm_id_from=333.999.0.0
源码地址
目录结构
基础结构 - index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=content-width, initial-scale=1.0" />
<title>Vue2 双向绑定Demo</title>
</head>
<body>
<div id="app">
<input v-model="name" type="text" />
姓名: {{name}}
<br />
<input v-model="more.like" type="text" />
点赞:{{more.like}}
<br />
<input v-model="more.follow.name" type="text" />
Follow By: {{more.follow.name}}
</div>
<script src="./myVue2.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=content-width, initial-scale=1.0" />
<title>Vue2 双向绑定Demo</title>
</head>
<body>
<div id="app">
<input v-model="name" type="text" />
姓名: {{name}}
<br />
<input v-model="more.like" type="text" />
点赞:{{more.like}}
<br />
<input v-model="more.follow.name" type="text" />
Follow By: {{more.follow.name}}
</div>
<script src="./myVue2.js"></script>
</body>
</html>
实现 - myVue2.js
1. 创建Vue实例
<!-- index.html -->
<script src="./myVue2.js"></script>
<script>
// 创建Vue实例
const vm = new Vue({
el: "#app",
data: {
name: "Kevin",
more: {
like: 123,
follow: {
name: "Venki",
},
},
},
});
</script>
<!-- index.html -->
<script src="./myVue2.js"></script>
<script>
// 创建Vue实例
const vm = new Vue({
el: "#app",
data: {
name: "Kevin",
more: {
like: 123,
follow: {
name: "Venki",
},
},
},
});
</script>
2. 创建Vue类
// myVue2.js
class Vue{
constructor(obj_instance) {
this.$data = obj_instance.data;
}
}
// myVue2.js
class Vue{
constructor(obj_instance) {
this.$data = obj_instance.data;
}
}
3. 数据劫持
示例data数据结构:
// myVue2.js
const Observer = (data_instance) => {
// 获取data_instance的key
Object.keys(data_instance).forEach((item) => {
Object.defineProperty(data_instance, item, {
enumerable: true, // 可枚举(for...in...)
configurable: true, // 可改可删除
get() {
// 当属性被访问的时候调用
},
set(newValue) {
// 当属性被修改的时候访问
},
})
})
}
// myVue2.js
const Observer = (data_instance) => {
// 获取data_instance的key
Object.keys(data_instance).forEach((item) => {
Object.defineProperty(data_instance, item, {
enumerable: true, // 可枚举(for...in...)
configurable: true, // 可改可删除
get() {
// 当属性被访问的时候调用
},
set(newValue) {
// 当属性被修改的时候访问
},
})
})
}
TIP
由于get的时候不应该访问数据return,因为这样会导致get的时候又触发get,因此需要提前取出当前item对应的值。
// myVue2.js
const Observer = (data_instance) => {
// 获取data_instance的key
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Object.defineProperty(data_instance, item, {
// ...
get() {
// 当属性被访问的时候调用
return data_instance[item]; // 错误示范
return value;
},
set(newValue) {
// 当属性被修改的时候访问
value = newValue;
}
})
})
}
// myVue2.js
const Observer = (data_instance) => {
// 获取data_instance的key
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Object.defineProperty(data_instance, item, {
// ...
get() {
// 当属性被访问的时候调用
return data_instance[item]; // 错误示范
return value;
},
set(newValue) {
// 当属性被修改的时候访问
value = newValue;
}
})
})
}


TIP
通过vm实例,发现只有第一层的more
与name
实现了监听,第二层的follow
、like
,第三层的name
都没有,因此我们需要递归实现监听每一层的item
。
// myVue2.js
const Observer = (data_instance) => {
// 递归结束条件
if(!data_instance || typeof data_instance !== 'object') return;
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
// ...
})
})
}
// myVue2.js
const Observer = (data_instance) => {
// 递归结束条件
if(!data_instance || typeof data_instance !== 'object') return;
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
// ...
})
})
}


WARNING
此时已经实现深层的监听,但是当我们将原有的值变为Object的时候,新的类型的值不会自动被监听。因此,我们需要监听新的value。
// myVue2.js
const Observer = (data_instance) => {
// ...
Object.keys(data_instance).forEach((item) => {
// ...
Object.defineProperty(data_instance, item, {
// ...
set(newValue) {
value = newValue;
Observer(value);
}
})
})
}
// myVue2.js
const Observer = (data_instance) => {
// ...
Object.keys(data_instance).forEach((item) => {
// ...
Object.defineProperty(data_instance, item, {
// ...
set(newValue) {
value = newValue;
Observer(value);
}
})
})
}
至此,完整的数据劫持方法已经实现,对Vue
类进行数据劫持
数据劫持与Vue
类定义 完整代码
// myVue2.js
class Vue {
constructor(obj_instance) {
this.$data = obj_instance.data;
Observer(this.$data);
}
}
const Observer = (data_instance) => {
// 递归结束条件结束
if (!data_instance || typeof data_instance !== "object") return;
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
value = newValue;
Observer(value);
},
});
});
};
// myVue2.js
class Vue {
constructor(obj_instance) {
this.$data = obj_instance.data;
Observer(this.$data);
}
}
const Observer = (data_instance) => {
// 递归结束条件结束
if (!data_instance || typeof data_instance !== "object") return;
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
value = newValue;
Observer(value);
},
});
});
};
4. 页面渲染
INFO
使用DocumentFragment
模拟虚拟DOM。
绑定Vue实例DOM与vm,并创建DocumentFragment存储所有DOM节点。
// myVue2.js
const Compiler = (element, vm) => {
// 绑定DOM节点 - 即div#app
vm.$el = document.querySelector(element);
const fragment = new DocumentFragment();
}
// myVue2.js
const Compiler = (element, vm) => {
// 绑定DOM节点 - 即div#app
vm.$el = document.querySelector(element);
const fragment = new DocumentFragment();
}
对类似{{ name }}
进行渲染:
const Compiler = (element, vm) => {
// ...
// (1) 获取element DOM所有子节点,并且先用fragment存储
let childNode;
while((childNode = vm.$el.firstChild)) {
fragment.append(childNode);
}
// (2) 实际上,我们在这步只需要对文本进行处理,即#text,他们的共同点在于node.nodeType === 3
// 递归渲染fragment
compileFragment(fragment);
function compileFragment(node) {
if (node.nodeType === 3) {
// 处理#text节点
}
node.childNodes.forEach((child) => {
compileFragment(child);
});
}
// (3) 将处理好的fragment进行渲染
vm.$el.append(fragment);
}
const Compiler = (element, vm) => {
// ...
// (1) 获取element DOM所有子节点,并且先用fragment存储
let childNode;
while((childNode = vm.$el.firstChild)) {
fragment.append(childNode);
}
// (2) 实际上,我们在这步只需要对文本进行处理,即#text,他们的共同点在于node.nodeType === 3
// 递归渲染fragment
compileFragment(fragment);
function compileFragment(node) {
if (node.nodeType === 3) {
// 处理#text节点
}
node.childNodes.forEach((child) => {
compileFragment(child);
});
}
// (3) 将处理好的fragment进行渲染
vm.$el.append(fragment);
}
事实上,我们需要做的是将一段文本中的{{ name }}
-> data_instance[name]
对应的值,已知name
即为key
的值。
因此我们使用正则表达式获取{{ xxx }}
包裹的数据xxx
。
正则表达式Pattern
= /\{\{\s*(\S+)\s*\}\}/
,exec
返回的数组结构 [满足的字段, 括号内的值, 满足的下标, 完整的值]
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
// 处理#text节点
const result = pattern.exec(node.nodeValue);
if(result) {
node.nodeValue = node.nodeValue.replace(pattern, vm.$data[result[1]]);
}
}
// ...
}
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
// 处理#text节点
const result = pattern.exec(node.nodeValue);
if(result) {
node.nodeValue = node.nodeValue.replace(pattern, vm.$data[result[1]]);
}
}
// ...
}
此时发现,貌似name
成功了,但是有两个undefined
,打印一下对应的key
。

说明:由于key
是由'.'一层层连接的,JS
没有递归取值,所以进行修改,我们把key
使用'.'分割成为数组,对每次一层进行深入遍历直到最深,此时拿到的值即为真正的value
。
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
// 处理#text节点
const result = pattern.exec(node.nodeValue);
if(result) {
const value = result[1].split('.').reduce((total, current)=> total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, vm.$data[result[1]]);
node.nodeValue = node.nodeValue.replace(pattern, value);
}
}
// ...
}
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
// 处理#text节点
const result = pattern.exec(node.nodeValue);
if(result) {
const value = result[1].split('.').reduce((total, current)=> total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, vm.$data[result[1]]);
node.nodeValue = node.nodeValue.replace(pattern, value);
}
}
// ...
}
此时,模板渲染已经能够正常显示了
页面模板语法渲染完整代码
// myVue2.js
// 页面渲染
const Compiler = (element, vm) => {
// 绑定DOM节点 - 即div#app
vm.$el = document.querySelector(element);
const fragment = new DocumentFragment();
// 获取$el的所有子节点
let childNode;
while ((childNode = vm.$el.firstChild)) {
fragment.append(childNode);
}
compileFragment(fragment);
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result = pattern.exec(node.nodeValue);
if (result) {
// console.log(result[1]);
const value = result[1]
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, value);
}
}
node.childNodes.forEach((child) => {
compileFragment(child);
});
}
vm.$el.append(fragment);
};
// myVue2.js
// 页面渲染
const Compiler = (element, vm) => {
// 绑定DOM节点 - 即div#app
vm.$el = document.querySelector(element);
const fragment = new DocumentFragment();
// 获取$el的所有子节点
let childNode;
while ((childNode = vm.$el.firstChild)) {
fragment.append(childNode);
}
compileFragment(fragment);
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result = pattern.exec(node.nodeValue);
if (result) {
// console.log(result[1]);
const value = result[1]
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, value);
}
}
node.childNodes.forEach((child) => {
compileFragment(child);
});
}
vm.$el.append(fragment);
};
5. 发布订阅模式
当我们修改了vm.$data
的内容的时候,此时,页面并不会重新渲染,因此我们需要实现一个更改值后实时==发布通知==的功能,那么需要新建接收通知的对象(订阅者)与发布通知的对象(发布者)。
class Publisher {
constructor() {
this.subscribers = [];
}
addSub(subscriber) {
// 添加订阅者
this.subscribers.push(subscriber);
}
notify() {
// 发布消息,通知所有订阅者更新数据
this.subscribers.forEach((subscriber) => subscriber.update());
}
}
class Subscriber {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
}
update() {
this.callback();
}
}
class Publisher {
constructor() {
this.subscribers = [];
}
addSub(subscriber) {
// 添加订阅者
this.subscribers.push(subscriber);
}
notify() {
// 发布消息,通知所有订阅者更新数据
this.subscribers.forEach((subscriber) => subscriber.update());
}
}
class Subscriber {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
}
update() {
this.callback();
}
}
此时,考虑一下我们什么时候会添加订阅者,什么时候会发布通知。 - 发布通知:一般当数据被修改的时候会通知订阅者,而数据修改的时候会触发Set
方法,因此,在Set()
的最后就可以通知更新数据。 - 添加订阅者:一般遇到、
v-model
、 v-bind
的时候新建订阅者,并且将新建的订阅者,每次新建订阅者后,callback
都会访问一次对象的值,即触发Get
方法,因此在Get()
的时候将==此次==的订阅者入订阅者队列。
const Observer = (data_instance) => {
const publisher = new Publisher();
// ...
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
// ...
get() {
publisher.addSub({此次的subscriber}) // 怎么获取此次的subscriber呢?
return value;
},
set(newValue) {
value = newValue;
Observer(value);
publisher.notify();
},
});
});
};
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result = pattern.exec(node.nodeValue);
if (result) {
const value = result[1]
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, value);
new Watcher(vm, result[1], (newValue)=>{
node.nodeValue = node.nodeValue.replace(pattern, newValue); // 正确吗?
})
}
}
// ...
}
};
const Observer = (data_instance) => {
const publisher = new Publisher();
// ...
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
// ...
get() {
publisher.addSub({此次的subscriber}) // 怎么获取此次的subscriber呢?
return value;
},
set(newValue) {
value = newValue;
Observer(value);
publisher.notify();
},
});
});
};
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result = pattern.exec(node.nodeValue);
if (result) {
const value = result[1]
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, value);
new Watcher(vm, result[1], (newValue)=>{
node.nodeValue = node.nodeValue.replace(pattern, newValue); // 正确吗?
})
}
}
// ...
}
};
TIP
这里有两个问题:
- 怎么获取此次的subscriber进行入队操作呢?
- Watcher里面还是用node.nodeValue进行替换吗? ❌
针对第一个问题:💁♂️
我们可以在new Subscriber
对象的时候,往Publisher
里面添加一个临时变量temp
存储当前的new Subscriber
即this
,入队的时候将Publisher.temp
入队即可(有的话)。
针对第二个问题:💁♀️
因为node.nodeValue实际上已经被处理(replace
)过一次了,因此提前将node.nodeValue
存储一次皆可。
const Observer = (data_instance) => {
const publisher = new Publisher();
// ...
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
// ...
get() {
publisher.addSub({此次的subscriber}) // 怎么获取此次的subscriber呢?
Publisher.temp && publisher.addSub(Publisher.temp);
return value;
},
set(newValue) {
value = newValue;
Observer(value);
publisher.notify();
},
});
});
};
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result = pattern.exec(node.nodeValue);
const initNodeValue = node.nodeValue;
if (result) {
const value = result[1]
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, value);
new Watcher(vm, result[1], (newValue)=>{
node.nodeValue = node.nodeValue.replace(pattern, newValue); // 正确吗? ❌
node.nodeValue = initNodeValue.replace(pattern, newValue);
})
}
}
// ...
}
};
class Subscriber {
constructor(vm, key, callback) {
// ...
Publisher.temp = this;
Publisher.temp = null;
}
update() {
this.callback();
const value = this.key.split(".").reduce((total, current) => total[current], this.vm.$data);
this.callback(value);
}
}
const Observer = (data_instance) => {
const publisher = new Publisher();
// ...
Object.keys(data_instance).forEach((item) => {
let value = data_instance[item];
Observer(value);
Object.defineProperty(data_instance, item, {
// ...
get() {
publisher.addSub({此次的subscriber}) // 怎么获取此次的subscriber呢?
Publisher.temp && publisher.addSub(Publisher.temp);
return value;
},
set(newValue) {
value = newValue;
Observer(value);
publisher.notify();
},
});
});
};
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
const result = pattern.exec(node.nodeValue);
const initNodeValue = node.nodeValue;
if (result) {
const value = result[1]
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.nodeValue = node.nodeValue.replace(pattern, value);
new Watcher(vm, result[1], (newValue)=>{
node.nodeValue = node.nodeValue.replace(pattern, newValue); // 正确吗? ❌
node.nodeValue = initNodeValue.replace(pattern, newValue);
})
}
}
// ...
}
};
class Subscriber {
constructor(vm, key, callback) {
// ...
Publisher.temp = this;
Publisher.temp = null;
}
update() {
this.callback();
const value = this.key.split(".").reduce((total, current) => total[current], this.vm.$data);
this.callback(value);
}
}
此时,当值被更改的时候,页面也会刷新。
6. v-model 双向绑定
现在还有一个问题就是要input
与vm.$data
双向绑定。
其实,只需要在渲染的时候,与#text
相同处理实现input
展示data
的值,并添加新的Subscriber
;当input
输入内容的时候,更新data
,添加绑定事件(addEventListener
)即可。
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {...}
// data -> input.value 单向绑定
if (node.nodeType === 1 && node.nodeName === "INPUT") {
const attributes = Array.from(node.attributes);
attributes.forEach((attr) => {
if (attr.nodeName === "v-model") {
const value = attr.nodeValue
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.value = value;
new Watcher(vm, attr.nodeValue, (newValue)=>{
node.value = newValue;
})
}
});
// input.value -> data 单向绑定
node.addEventListener('input', (e)=>{
const arr = attr.nodeValue.split('.'); // 获取所有层的key
const arr2 = arr.slice(0, arr.length-1); // 排去最后一层的key
const final = arr2.reduce((total, current)=> total[current], vm.$data); // 一直递归到倒数第二层
final[arr[arr.length-1]] = e.target.value; // 倒数第二层[最后一个key] = 最底下的值
});
}
}
// ...
}
};
const Compiler = (element, vm) => {
// ...
function compileFragment(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {...}
// data -> input.value 单向绑定
if (node.nodeType === 1 && node.nodeName === "INPUT") {
const attributes = Array.from(node.attributes);
attributes.forEach((attr) => {
if (attr.nodeName === "v-model") {
const value = attr.nodeValue
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.value = value;
new Watcher(vm, attr.nodeValue, (newValue)=>{
node.value = newValue;
})
}
});
// input.value -> data 单向绑定
node.addEventListener('input', (e)=>{
const arr = attr.nodeValue.split('.'); // 获取所有层的key
const arr2 = arr.slice(0, arr.length-1); // 排去最后一层的key
const final = arr2.reduce((total, current)=> total[current], vm.$data); // 一直递归到倒数第二层
final[arr[arr.length-1]] = e.target.value; // 倒数第二层[最后一个key] = 最底下的值
});
}
}
// ...
}
};
至此,一个基于Object.defineProperty
的简易双向绑定实现了。🕶️