1 利用defineProperty实现数据劫持
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input id='input' />
<span id="span"></span>
</body>
<script type="text/javascript">
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 --> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
</script>
</html>
2 利用ES6中的proxy实现数据劫持
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input id='input' />
<span id="span"></span>
</body>
<script type="text/javascript">
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 --> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);
// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
</script>
</html>
3 Proxy和Object.defineProperty区别
-
roxy使用比Object.defineProperty方便
-
Proxy代理整个对象,Object.defineProperty只代理对象上的某个属性
-
如果对象内部要全部递归代理,则Proxy可以只在调用时递归,而Object.defineProperty需要在一开始就全部递归,Proxy性能优于Object.defineProperty;
-
对象上定义新属性时,Proxy可以监听到,Object.defineProperty监听不到;
-
数组新增删除修改时,Proxy可以监听到,Object.defineProperty监听不到;
-
Proxy不兼容IE, Object.defineProperty不兼容IE8及以下;
-
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
如下代码所示
let target = {};
let p = new Proxy(target, {});
p.a = 37; // 操作转发到目标
console.log(target.a); // 37. 操作已经被正确地转发
target.a=4;
console.log(p.a)//4
console.log(target==p)//false
4 mvvm框架实现
下面demo实现了一个mvvm框架,基于EventTarget实现;EventTarget是一个DOM接口,由可以接收事件,并且可以创建侦听器的对象实现;
4.1 Object.defineProperty实现数据劫持
index.html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
{{message}}
<div>
111
<div>
{{message}}
</div>
</div>
<div v-html="htmlData"></div>
<input v-model="modelData" /> {{modelData}}
</div>
</body>
<script>
</script>
</html>
webpack.config.js简单配置
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let Webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
index: './src/index.js'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: { // 用babel-loader需要把es6-es5
presets: [
'@babel/preset-env'
]
}
},
},
{
test: /\.css$/,
use: [
'css-loader',
'postcss-loader',
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
filename: 'index.html',
}),
]
}
index.js代码
import Kvue from './Kvue.js';
let vm = new Kvue({
el: '#app',
data: {
message: '测试数据',
htmlData: 'html数据',
modelData: '绑定的数据'
}
})
setTimeout(() => {
console.log("setTimeout...");
vm.$options.data.message = "修改的数据";
}, 1000);
Kvue.js代码
export default class Kvue extends EventTarget {
constructor(options) {
super();
this.$options = options;
this.compile();
this.observe(this.$options.data);
}
observe(data) {
let keys = Object.keys(data);
keys.forEach(key => {
this.defineReact(data, key, data[key]);
})
}
defineReact(data, key, value) {
let _this = this;
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
console.log("get...");
return value;
},
set(newVal) {
console.log('set...');
let event = new CustomEvent(key, {
detail: newVal
});
_this.dispatchEvent(event);
value = newVal;
}
});
}
compile() {
let el = document.querySelector(this.$options.el);
this.compileNode(el);
}
compileNode(el) {
let childNodes = el.childNodes;
console.log(childNodes)
childNodes.forEach(node => {
if(node.nodeType === 1) {
// 标签
let attrs = node.attributes;
[...attrs].forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
if(attrName.indexOf("v-") === 0) {
attrName = attrName.substr(2);
if(attrName === 'html') {
node.innerHTML = this.$options.data[attrValue];
} else if(attrName === 'model') {
node.value = this.$options.data[attrValue];
node.addEventListener("input", e => {
this.$options.data[attrValue] = e.target.value;
})
}
}
})
if(node.childNodes.length > 0) {
this.compileNode(node);
}
} else if(node.nodeType === 3) {
// 文本节点
let reg = /\{\{\s*(\S+)\s*\}\}/g;
let textContent = node.textContent;
if(reg.test(textContent)) {
// console.log('存在双花括号');
// console.log(RegExp.$2);
let $1 = RegExp.$1;
// node.textContent = this.$options.data[$1];
node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
this.addEventListener($1, e => {
// console.log('触发了修改。。。');
// console.log(e.detail);
// 重新渲染视图
let oldVal = this.$options.data[$1];
let reg = new RegExp(oldVal);
node.textContent = node.textContent.replace(reg, e.detail);
})
}
}
})
}
}
4.2 Proxy实现数据劫持
Kvue.js代码里面observe修改如下:
observe(data) {
let _this = this;
this.$options.data = new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, newVal) {
let event = new CustomEvent(key, {
detail: newVal
});
_this.dispatchEvent(event);
target[key] = newVal;
return true;
}
})
}