// 需了解
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
// configurable当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
// enumerable当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
// value该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
// writable当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (en-US)改变。
// get属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
// set属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
// vue2基本原理
// 1. new Vue()初始化
// 2.1 data属性 -> Observer劫持data对象(监听所有danta属性),遍历所有属性,使用Object.defineProperty把这些属性转为getter、setter,并监听data对象属性的变化
// 2.2 el模板 -> Compiler解析模板
// 3.1 Dep管理订阅数据对象,数据变化时通知所有订阅数据对象
// 3.2 view -> 解析模板后初始化视图
// 4. Watcher -> 添加订阅数据对象,Dep通知变化notice(),updata()更新视图,Compiler订阅数据,绑定数据更新方法
class Vue {
constructor(options = {}) {
// 1.保存数据
this.$options = options;
this.$data = typeof options.data === 'function' ? options.data() : options.data;
this.$el = options.el;
// 2.将this.$data中的数据加入到响应式系统中,监听data所有属性
new Observer(this.$data);
// 3.用this代理this.$data中的数据
this._proxy();
// 4.解析el模板
new Compiler(this.$el, this.$data);
}
_proxy() {
// 代理,把data的属性一个一个添加Vue中
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
// 获取this.xx的值 --> 获取this.$data.xx的值
return this.$data[key];
},
set(newValue) {
// 设置this.xx=val --> 设置this.$data.xx=val
this.$data[key] = newValue;
}
})
})
}
}
// 响应式系统
class Observer {
constructor(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, value) {
// 为每个vm.$data中的属性创建一个依赖对象dep,用于管理vm.$el模板中使用了该属性的订阅对象
// 1.将使用该属性的所有订阅对象添加到依赖对象dep中的订阅对象数组中
// 2.该属性的值改变,将通知订阅对象数组中的所有成员更新数据
// 监听子属性
const dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 添加订阅对象
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (value === newValue) return;
value = newValue;
// 数据发生改变,通知订阅对象更新数据
dep.notify();
}
})
}
}
// 依赖
class Dep {
constructor() {
// 订阅对象数组
this.subs = [];
}
addSub(sub) {
// 添加订阅者
this.subs.push(sub);
}
notify() {
// 遍历订阅对象,更新视图
this.subs.forEach(sub => {
sub.update();
})
}
}
// 订阅
// 首先设定whacher为订阅者,要想往dep里添加订阅者,由于在defineReactive方法里操作,需要使用闭包
class Watcher {
constructor(node, name, data) {
//node节点
this.node = node;
//node节点使用vm.$data中的属性名称
this.name = name;
this.data = data;
//临时保存订阅对象
//Dep.target指向Watcher本身
Dep.target = this;
// 更新视图,并将订阅对象添加到订阅对象数组中
this.update();
Dep.target = null;
}
// 更新视图
update() {
if (this.node.nodeType === 1) { //标签节点
this.node.value = this.data[this.name];
} else if (this.node.nodeType === 3) { //文本节点
this.node.nodeValue = this.data[this.name];
}
}
}
const reg = new RegExp(/\{\{(.+)\}\}/);
// 解析模板
class Compiler {
constructor(el, data) {
// 获取this.el模板元素节点
this.el = document.querySelector(el);
// data数据
this.data = data;
// 创建虚拟dom节点,并将模板解析后的内容插入到虚拟dom节点内
// 使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。
const frag = this.createFragment();
// 将虚拟dom节点内容插入到this.el节点内,插入的是虚拟dom节点的子孙节点,而虚拟dom节点自身不会插入
// 注:模板解析完后,this.el节点内为空
this.el.appendChild(frag);
}
createFragment() {
let child;
// 创建虚拟dom节点
let frag = document.createDocumentFragment();
// 遍历this.el子节点
// 获取第一个节点
while (child = this.el.firstChild) {
// 解析this.el子节点child
this._compiler(child);
// 将解析后的child节点插入到frag虚拟dom节点内,child节点会从this.el中移除,
// this.el.firstChild就变成了child的下一个兄弟节点,直到this.el中没有子节点,跳出循环
frag.appendChild(child);
}
return frag;
}
// 解析节点
_compiler(node) {
// 标签节点
if (node.nodeType === 1) {
const attrs = node.attributes;
if (attrs.hasOwnProperty('v-model')) {
const name = attrs['v-model'].nodeValue;
// v-model双向数据绑定
// 创建订阅对象,并初始化视图
new Watcher(node, name, this.data);
// 给节点绑定input事件
node.addEventListener('input', (e) => {
this.data[name] = e.target.value;
})
}
// 遍历标签节点的子节点
node.childNodes.forEach(item => {
// 递归解析子节点
this._compiler(item);
})
}
// 文本节点
else if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
const name = RegExp.$1.trim();
// 创建订阅对象,并初始化视图
new Watcher(node, name, this.data);
}
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YY</title>
</head>
<body>
<div id="app">
<h3>注:代码是简单实现,因此一对标签内只能解析一个数据</h3>
<p>{{ num }}</p>
<input type="submit" onclick="btn()" value="添加">
<p>{{ message }}</p>
<input type="text" v-model="message">
</div>
<script src="./demo-vue2.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
message: '鱼鱼想上岸',
num: 1
}
})
function btn() {
vm.num++
}
</script>
</body>
</html>