1、vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过`Object.defineProperty()`来劫持各个属性的`setter`,`getter`,在数据变动时发布消息给订阅者,触发相应的监听回调
2、当 data 有变化的时候,通过ES5 中的 Object.defineProperty() 方法中的 set 方法劫持属性值的设置操作和 get 方法劫持属性值的获取操作,实现数据变化视图 view 跟着变化;
而视图 view 变化数据跟着变化则是通过底层的 input 事件来进行 data 的响应更改
3、实现步骤:
1.实现一个监听者Oberver来劫持并监听所有的属性,一旦有属性发生变化就通知订阅者
2.实现一个订阅者watcher来接受属性变化的通知并执行相应的方法,从而更新视图
3.实现一个解析器compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相对应的订阅者
4、什么是 Object.defineProperty() 看一下代码
let obj = { name: '阿豪', value: '真帅' }
Object.defineProperty(obj, 'sss', {
value: "18",
enumerable: true, //这一句控制属性可以枚举 enumerable 改为true 就可以参与遍历了 默认值false
writable: true,// 控制属性可以被修改 加上这一句话就可以个修改了 默认值false
configurable: true, // 控制属性可以被删除 默认值false
})
console.log(obj); // 这样可以添加一个 sss 是比较浅色的 不参与遍历
obj.sss = '100' // 现在修改 sss 为 100 Object.defineProperty 来修改修改不了 sss
console.log(obj);
delete (obj.sss) // 删除 sss
console.log(obj);
var num = 222
let obj = { name: '阿豪', value: '真帅' }
Object.defineProperty(obj, 'sss', {
get() { // 当有人读取obj.sss 的时候 get函数就会调用,并且返回就是 sss 的值
console.log('有人读取sss 属性了');
return num
},
set(value) {// 当有人修改obj.sss 的时候 get函数就会调用, 且会收到具体的值
console.log('有人修改了sss 值 值是' + value);
num = value // 做个牵引 修改成功
}
})
console.log(obj.sss); // 有人读取sss 属性了 // hello
obj.sss = 9999
console.log(obj); // 有人修改了sss 值 值是9999
5、硬核手写v-model
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>双向绑定</title>
</head>
<body>
<!-- 实现vue -->
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script type="text/javascript">
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val
},
set: function (newVal) {
if (newVal === val) {
return
}
val = newVal;
console.log('新值:' + val);
// 一旦更新立马通知
dep.notify();
}
})
}
/*观察者函数*/
function observe(obj, vm) {
for (let key of Object.keys(obj)) {
defineReactive(vm, key, obj[key]);
}
}
function nodeToFragment(node, vm) {
var fragment = document.createDocumentFragment();
var child;
while (child = node.firstChild) {
compile(child, vm);
fragment.appendChild(child);
}
return fragment
}
/*编译函数*/
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/; // 来匹配 {{ xxx }} 中的xxx
// 如果是元素节点
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析元素节点的所有属性
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 看看是与哪一个数据相关
node.addEventListener('input', function (e) {
vm[name] = e.target.value; // 将实例的text 修改为最新值
});
node.value = vm[name]; // 将data的值赋给该node
node.removeAttribute('v-model');
}
};
}
// 如果是文本节点
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取到匹配的字符串
name = name.trim();
// node.nodeValue = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name); // 不直接通过赋值的操作,而是通过绑定一个订阅者
}
}
}
/*Watcher构造函数*/
function Watcher(vm, node, name) {
Dep.target = this; // Dep.target 是一个全局变量
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update() {
this.get();
this.node.nodeValue = this.value; // 注意,这是更改节点内容的关键
},
get() {
this.value = this.vm[this.name]; // 触发相应的get
}
}
/*dep构造函数*/
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
notify() {
this.subs.forEach(function (sub) {
sub.update();
})
}
}
/*Vue构造函数*/
function Vue(options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
// 处理完所有dom节点后,重新将内容添加回去
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>