思路
思想就是:双向绑定 就是 数据 更改会响应到视图上,视图更改会响应回数据
具体到代码上就是:
- 通过 Object.defineProperty 监听到数据 value 的变化。
- 再 get 中 使Dep收集对应的wather并放入缓存列表。
- 再 set 中传递新值并触发Dep遍历缓存列表,将新值传递到wather更新函数并执行。
其中核心就是观察者模式的运用、递归的运用、模版正则匹配,除此之外都是为了迎合业务进行的代码。
如果你此时没有深入了解 观察者模式,不懂用递归, 建议先学学这两个,之后你再看就会觉得特别简单。
代码实现myVue
点击下面 — 查看简略版代码
点击查看 简略版代码
class myVue {
constructor(options) {
this.$data = options.data();
Observe(this.$data);
Compile(options.el, this);
}
}
// 数据劫持
function Observe(obj) {
if (!obj || typeof obj !== "object") return;
Object.keys(obj).forEach((key) => {
const dep = new Dep(); // 创建发布者
let value = obj[key];
Observe(value);// 递归掉用(深层调用)
// 数据劫持
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set(newValue) {
Observe(newValue); // 递归掉用 (当重新赋值是新对象的时候)
value = newValue;
dep.notify(); // 发布通知
},
get() {
console.log("数据劫持 get:" + key, value);
Dep.target && dep.addSub(Dep.target); // 收集订阅者
return value;
},
});
});
}
// 因为模版编译其实就是业务上处理 重点是遍历所有节点 和 正则匹配
// 我们想的简单一点 假设是对<div id='app'> {{ aaa }} </div>这个特定的就一个div 来进行响应式
function Compile(option) {
let html_ = document.querySelector(option.el)
// todo: 模版解析
html_.innerText = option.data.aaa
new watcher(option, "aaa", (data) => {
html_.innerText = data // 将更新方法传递
})
}
// ==== 观察者 订阅者 ===
class Dep {
constructor() {
this.list = []
}
notify(newValue) {
this.list.forEach(wather => {
wather.update(newValue)
})
}
push(watcher){
this.list.push(watcher)
}
}
class watcher {
constructor(option, key, updateFunc) {
Dep.tag = this;
option.data[key]; // 进行取值操作 来触发 get 使Dep来收集watcher
this.updateFunc = updateFunc;
Dep.tag = null
}
update(v){
this.updateFunc(v);
console.log('执行更新函数');
}
}
下面的详细的代码
class MyVue {
constructor(options) {
this.$data = options.data();
console.log("this.$data: ", this.$data);
// console.log("dep: ", dep);
// 数据劫持
Observe(this.$data);
// 属性代理 将vue上定义的属性之间设置到data上
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
set(newValue) {
this.$data[key] = newValue;
},
get() {
return this.$data[key];
},
});
});
// 对html进行模板编译
Compile(options.el, this);
}
}
// 处理指令操作
function Compile(el, vm) {
// 获取对应的文档结构
vm.$el = document.querySelector(el);
// 创建文档碎片(创建一块内存,放到内存中,页面上没有,防止重排重绘)
const fragment = document.createDocumentFragment();
while ((children = vm.$el.firstChild)) {
fragment.appendChild(children);
}
replace(fragment);
vm.$el.appendChild(fragment);
// 文档编译
function replace(node) {
// 匹配{{}}
const regMustaChe = /\{\{\s*(\S+)\s*\}\}/;
// 是文本节点
if (node.nodeType == 3) {
const text = node.textContent;
let execResult = regMustaChe.exec(text);
// console.log("text: ", text);
// console.log("execResult: ", execResult);
if (execResult) {
let value = execResult[1]
.split(".")
.reduce((newObj, k) => newObj[k], vm);
node.textContent = text.replace(regMustaChe, value);
// 创建watcher实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustaChe, newValue);
});
}
return;
}
// input v-model 属性
if (node.nodeType == 1 && node.tagName.toUpperCase() == "INPUT") {
// 得到当前元素所有属性节点
const attr = Array.from(node.attributes);
let isHasVModel = attr.find((k) => k.name == "v-model");
if (isHasVModel) {
let newValue = isHasVModel.nodeValue
.split(".")
.reduce((newObj, k) => newObj[k], vm);
node.value = newValue;
new Watcher(vm, isHasVModel.nodeValue, (newValue) => {
node.value = newValue;
});
// 监听数据改变
node.addEventListener("input", (e) => {
let newValue = e.target.value;
let arr = isHasVModel.nodeValue.split(".");
let obj = arr
.slice(0, arr.length - 1)
.reduce((newObj, k) => newObj[k], vm);
obj[arr[arr.length - 1]] = newValue;
});
}
}
node.childNodes.forEach((e) => replace(e));
}
}
function Observe(obj) {
if (!obj || typeof obj !== "object") return;
Object.keys(obj).forEach((key) => {
const dep = new Dep();
let value = obj[key];
// 递归掉用(深层调用)
Observe(value);
// 数据劫持
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set(newValue) {
// 递归掉用 (当重新赋值时新对象的时候)
Observe(newValue);
console.log("数据劫持 set: " + key, newValue);
value = newValue;
dep.notify();
},
get() {
console.log("数据劫持 get:" + key, value);
Dep.target && dep.addSub(Dep.target);
return value;
},
});
});
}
// 订阅者
class Dep {
constructor() {
this.sub = [];
}
addSub(watcher) {
this.sub.push(watcher);
}
// 通告
notify() {
console.log("Dep notify 发布订阅");
this.sub.forEach((watcher) => {
watcher.upData();
});
}
}
// 发布者
class Watcher {
/**
*
* @param {*} vm vue实例
* @param {*} key 属性
* @param {*} cb 更新自己
*/
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 存入
Dep.target = this;
key.split(".").reduce((newObj, k) => newObj[k], vm);
Dep.target = null;
}
// 修改dom
upData() {
let newValue = this.key
.split(".")
.reduce((newObj, k) => newObj[k], this.vm);
console.log("newValue: ", newValue);
this.cb(newValue);
}
}
收获
个人感觉死记这个实现原理没有啥用,还是推荐学设计模式,总之对我来说收获是:
- 它巧用wather 来触发get进而 Dep收集自己; 这种思维很好;
- 对于字符串 "user.name" 可以使用
key.split(".").reduce((newObj, k) => newObj[k], vm)> 按照常规方法是不是 split之后通过 forEach 来取值,但也可以通过累加方法来取值 这样更高级好看,还是要熟悉数组的常用方法。
- 这个递归有意思
这个递归有意思
vm.$el = document.querySelector(el);
const fragment = document.createDocumentFragment();
while ((children = vm.$el.firstChild)) {
fragment.appendChild(children);
}