概述
-
Vue
响应式数据实现:vue.js
则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。 - 实现思路:首先通过
compile
类,对处在根元素中的子节点进行扫描、编译,通过key
从data
中获取对应的值追加到页面上。实现一个observe
类,getdata
中的数据设置getter
和setter
方法,数据劫持data
中数据的变化做对应的更新操作。页面中每一个属性对应一个watcher
,通过watcher
连接compile
和observe
,监听页面的变化来改变数据。
image.png
实现
Compile
compile
主要是编译页面中的指令模板,将模板中的变量替换成数据,然后初次渲染视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
注意事项:
- 由于页面的DOM操作非常耗性能,所以我们将DOM转换成fragment文档碎片,解析完成后再讲fragment添加会DOM,提高性能和效率。
-
对编译的每一个属性,需要指定一个watcher,页面上的每一个属性对应一个watcher。
image.png
// 编译工具类
const compileUtil = {
// 从 data:{} 中获取值
getValue(expr, vm) {
// [person,name] 通过这个方法可以获取对象中的值 data:{obj:{name:'jack'}}
// console.log(expr.trim().split(".")); ["msg"] ["person","name"]
return expr
.trim()
.split(".")
.reduce((data, currentValue) => {
return data[currentValue];
}, vm.$data);
},
setVal(expr, vm, value) {
return expr
.trim()
.split(".")
.reduce((data, currentValue) => {
data[currentValue] = value;
}, vm.$data);
},
// 处理这种情况{{ person.name }} {{ person.age }}
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1].trim(), vm);
});
},
text(node, expr, vm) {
// console.log(expr)
//exp:msg exp:person.name
let value;
if (expr.indexOf("{{") !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], newVal => {
// console.log(this.getContentValue(expr, vm))
// this.getContentValue(expr, vm) => jack===6666
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
// new Watcher(vm, args[1], newVal => {
// newVal:6666
// this.updater.textUpdater(node, newVal);
// });
return this.getValue(args[1].trim(), vm);
});
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, newVal => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新视图
new Watcher(vm, expr, newVal => {
this.updater.modelUpdater(node, newVal);
});
// 视图更新数据
node.addEventListener("input", e => {
this.setVal(expr, vm, e.target.value);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
//expr:'click'
const fn = vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm));
},
updater: {
htmlUpdater(node, value) {
node.innerHTML = value;
},
textUpdater(node, value) {
node.textContent = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
};
// 编译类 编译指令模板 v-text {{ msg }} v-on:click
class Compile {
constructor(el, vm) {
// 判断传过来是节点还是选择器
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 获取文档碎片对象 存入内存中 操作DOM时减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 编译模板
this.compile(fragment);
// 追加到根元素上
this.el.appendChild(fragment);
}
compile(fragment) {
// 获取所有的子节点
const childNodes = fragment.childNodes;
// 遍历所有的子节点 针对不同的节点 作不同的编译工作
[...childNodes].forEach(child => {
// 元素节点 <p></p> <h1></h1>
if (this.isElementNode(child)) {
// console.log(child)
// console.log('元素节点',child)
this.compileElement(child);
} else {
// 文本节点 {{ person.name }} {{ msg }}
// console.log('文本节点',child)
this.compileText(child);
}
// 递归调用子节点
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
});
}
compileElement(node) {
// console.log(node) v-text v-html v-model
const attributes = node.attributes;
// console.log(attributes);
[...attributes].forEach(attr => {
// 获取属性和值
const { name, value } = attr;
// 判断是否是指令
if (this.isDirective(name)) {
// 获取对应的指令的名字 html text model
const [, directive] = name.split("-"); //text html model on:click
// 对于是v-on:click这种情况需要做进一步处理 不
const [dirName, eventName] = directive.split(":"); //text html model (on,eventName)
// console.log(dirName,eventName) 是事件的返回 text:undefined
// 数据驱动视图 node:节点 value:指令的名称
compileUtil[dirName](node, value, this.vm, eventName);
// 删除V-text等指令属性
node.removeAttribute(`v-${directive}`);
} else if (this.isEventName(name)) {
let [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
}
});
}
compileText(node) {
const content = node.textContent;
if (/\{\{(.*)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
node2Fragment(el) {
// 创建文档碎片
const fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild);
}
return fragment;
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
isElementNode(el) {
return el.nodeType === 1;
}
}
class KVue {
constructor(options) {
// 全局保存数据
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1、实现一个数据观察者
new Observer(this.$data);
// 2、实现一个编译器
new Compile(this.$el, this);
// 3、将data数据代理到当前实例上
this.proxyData(this.$data);
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
});
}
}
Observe
Observe
主要使用Obeject.defineProperty()
是对data
做监听,给data
中的每个属性加上get
、set
方法,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。
// 观察者 页面上 每一个属性都对应一个watcher 通知页面更新 存UI中的依赖,实现update函数更新页面
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
// 通过回调去更新页面
this.cb = cb;
//初始化的保存旧值 便于后面对比 并且把当前实例指向Dep.target静态对象
this.oldVal = this.getOldValue();
}
getOldValue() {
Dep.target = this;
const oldVal = compileUtil.getValue(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
const newVal = compileUtil.getValue(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
// 存储watcher的容器
class Dep {
constructor() {
this.deps = [];
}
addDep(watcher) {
this.deps.push(watcher);
}
notify() { //通知watcher刦更新页面
this.deps.forEach(watcher=>watcher.update())
}
}
// 监听data中各个属性的变化 给data中的数据添加getter setter方法
class Observer {
constructor(data) {
this.observe(data);
}
// 监听data中的数据
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach(key => {
// 定义响应式
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
if (typeof value === "object") {
this.observe(value);
}
// 创建Dep实例,Dep和key是一一对应的
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
set: newVal => {
// 解决由于新增对象而无法监听问题 vm.$data.person = {a:1}
this.observe(newVal);
if (newVal !== value) {
value = newVal;
dep.notify()
}
},
get() {
// 页面中每使用一次data中的属性 都需要使用watcher监视 并添加到dep中
Dep.target && dep.addDep(Dep.target);
return value;
}
});
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MVVM</title>
</head>
<body>
<div id="app">
<p>{{ msg }}</p>
<p>{{ person.name }}=={{ person.age }}</p>
<p v-text="msg"></p>
<p v-text="person.age"></p>
<p v-text="htmlStr"></p>
<p v-html="htmlStr"></p>
<ul>
<li>{{msg}}</li>
<li>{{htmlStr}}</li>
</ul>
<input type="text" v-model="msg" />
<button v-on:click="clickHandle">click</button>
<button @click="clickHandle">click</button>
</div>
</body>
<script src="./observer.js"></script>
<script src="./kVue.js"></script>
<script>
let app = new KVue({
el: '#app',
data: {
msg: 'this is msg',
person: {
name: 'jack',
age: 21
},
htmlStr: '<b>this is html string</b>'
},
methods: {
clickHandle(){
console.log(this.$data.msg)
}
}
})
</script>
</html>
效果图
image.png