几种实现双向绑定的做法
发布者-订阅者模式(backbone.js)
脏值检查(angular.js)
数据劫持(vue.js)
Vue 响应式原理最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的。BTW:Vue3则是使用proxy方法来劫持。
必须实现以下几点技术:
- 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
- 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
先看完整代码(复制粘贴则可以实现功能)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue 双向绑定源码实现</title>
<style>
.text{
display: block;
padding: 10px;
margin-bottom: 20px;
text-align: left;
width: 100%;
}
.cell{
margin-top: 10px;
background-color: #f2f2f2;
padding: 20px;
width: 200px;
color: #424242;
}
.cell input{
padding: 8px;
}
</style>
</head>
<body>
<div id="app">
<h2>
Vue 双向绑定源码实现
</h2>
<div class="cell">
<div class="text" v-text="msg"></div>
<!-- 输入值将发生改变-->
<input type="text" v-model="msg" >
</div>
</div>
<script>
class Vue{
// 构造函数
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.options = options;
// 保存数据model与view相关的指令、当model改变时、会触发指令更新、保证view实时更新
this.directives = {};
if (this.$el){
// 观察者
this.Observer(this.$data);
// 解析器
this.Compile(this.$el);
}
}
/**
* 编译模板
* 例如: <div v-text="msg"> </div> => <div> msg的值 </div>
* @method compile
* @param el 根元素. 深度遍历
*/
Compile(el){
const {children: nodes} = el;
[...nodes].forEach(node => {
if (node.childNodes && node.childNodes.length){
this.Compile(node);
}
if (node.hasAttribute('v-text')){
const property = node.getAttribute('v-text');
const value = this.$data[property];
this.directives[property].push(new Watch( node, this, property));
// node.removeAttribute(`v-text`);
} else if (node.hasAttribute('v-model')){
const property = node.getAttribute('v-model');
const value = this.$data[property];
node.value = value;
// 监听输入框的变化
node.oninput = () => {
// node.value = 当前输入框的值
// 改动data值. 去通知观察者
this.$data[property] = node.value;
}
}
})
}
/**
* 观察者
* 对data的属性进行监听
* @method Observer
* @param data newVue里的data数据
*/
Observer(data) {
if (data && typeof data === 'object'){
Object.keys(data).forEach(key => {
// 如果属性是对象,进项向下监听
if (typeof data[key] === 'object'){
this.Observer(data[key]);
}
let value = data[key];
// 每个属性做响应式
this.directives[key] = [];
const updateAction = this.directives[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
// 发生改动
if (value !== newValue){
value = newValue;
// 通知 Watch更新列表
updateAction.forEach(item => {
item.updater();
})
}
}
})
})
}
}
/**
* Watch
* 数据改动 => 视图更新
* @param node 当前节点
* @param vm newVue的this
* @param property data里的属性
*/
class Watch {
constructor(node, vm, property) {
this.node = node;
this.vm = vm;
this.property = property;
// 更新操作
this.updater()
}
// 更新者
updater(){
if (this.node.hasAttribute('v-text')){
this.node.innerText = this.vm.$data[this.property]
}
}
}
const app = new Vue({
el: '#app',
data: {
msg: '输入后将发生改变',
person: {
age: 18,
name: 'pro'
},
}
})
</script>
</body>
</html>
初始化
const app = new Vue({
el: '#app',
data: {
msg: '输入后将发生改变',
person: {
age: 18,
name: 'pro'
},
}
})
F12控制台,输入app,将输出app相关数据
实现入口
this.$el 传入第一个根元素: #App
this.$data new Vue里的data
this.directives = {} 响应式方法集合
class Vue{
// 构造函数
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.options = options;
// 保存数据model与view相关的指令、当model改变时、会触发指令更新、保证view实时更新
this.directives = {};
if (this.$el){
// 观察者
this.Observer(this.$data);
// 解析器
this.Compile(this.$el);
}
}
}
实现第一步骤:建立观察者Observer
使用Object.defineProperty对对象的属性进行劫持
注意重点: 如果对象的属性是对象的话,我们就进行递归劫持,俗称深度遍历
/**
* 观察者
* 对data的属性进行监听
* @method Observer
* @param data newVue里的data数据
*/
Observer(data) {
if (data && typeof data === 'object'){
Object.keys(data).forEach(key => {
// 如果属性是对象,进项向下监听
if (typeof data[key] === 'object'){
this.Observer(data[key]);
}
let value = data[key];
// 每个属性做响应式
this.directives[key] = [];
const updateAction = this.directives[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value;
},
set(newValue) {
// 发生改动
if (value !== newValue){
value = newValue;
// 通知 Watch更新列表
updateAction.forEach(item => {
item.updater();
})
}
}
})
})
}
实现第二步骤:建立解析器Compile
从根元素#App开始深度遍历,只要符合带‘v-’开头的指令就进行渲染
/**
* 编译模板
* 例如: <div v-text="msg"> </div> => <div> msg的值 </div>
* @method compile
* @param el 根元素. 深度遍历
*/
Compile(el){
const {children: nodes} = el;
[...nodes].forEach(node => {
if (node.childNodes && node.childNodes.length){
this.Compile(node);
}
if (node.hasAttribute('v-text')){
const property = node.getAttribute('v-text');
const value = this.$data[property];
// 添加响应式
this.directives[property].push(new Watch( node, this, property));
// node.removeAttribute(`v-text`);
} else if (node.hasAttribute('v-model')){
const property = node.getAttribute('v-model');
const value = this.$data[property];
node.value = value;
// 监听输入框的变化
node.oninput = () => {
// node.value = 当前输入框的值
// 改动data值. 去通知观察者
this.$data[property] = node.value;
}
}
})
}
第三步骤实现一个Watcher
有值改动时,观察者observer通知watcher,watcher进行视图更新
/**
* Watch
* 数据改动 => 视图更新
* @param node 当前节点
* @param vm newVue的this
* @param property data里的属性
*/
class Watch {
constructor(node, vm, property) {
this.node = node;
this.vm = vm;
this.property = property;
// 更新操作
this.updater()
}
// 更新者
updater(){
if (this.node.hasAttribute('v-text')){
this.node.innerText = this.vm.$data[this.property]
}
}
}