前言
双向数据绑定人人都会背了,已经没什么新奇了。
但是如果遇到XX喜欢问源码之类的,或者问你设计思路你又该如何应对呢,所以下面这篇文章主要是为了记录双向数据绑定的一个实现,采用了类的方式,积极向面向对象编程靠拢。
这里采用的是vue2的数据劫持方式,vue3可以参考:
此处。
难点
1. Dep跟Watcher分别对应什么呢
一个Dep对应一个数据劫持属性,一个Watcher对应模板一个双向绑定的变量或变量属性--> {{xxx.xxx}}或者v-model。
- Dep是发布者,从Observer类中可以看出,Dep对应的劫持到的data或者data的某一个属性。即,如果该值发生变化,就会触发数据劫持
set操作
,从而执行通知操作dep.notify()
,遍历执行watcher.update()
,从而更新视图。 - Watcher是观察者,从Compiler类中可以看出,解析对应的模板会读取数据,触发数据劫持
get操作
从而触发dep.addSub(watcher)
。
源码
<!--
* @Author: Penk
* @LastEditors: Penk
* @LastEditTime: 2021-07-12 00:23:30
* @FilePath: \temp\myVue.html
-->
<!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>Document</title>
<style>
#app {
text-align: center;
margin: 100px auto auto auto;
}
</style>
</head>
<body>
<div id="app">
<div v-html="msg"></div>
<input v-model="author.name" style="margin-bottom: 20px" />
<br />
姓名:{{author.name}}
<br />
计算属性变大写:{{toUpperCaseName}}
<br />
<br />
<button v-on:click='change(author.name,"自定义参数")'>test</button>
</div>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
<!-- <script src="./script.js"></script> -->
<script>
class Penk {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let methods = options.methods;
let computed = options.computed;
if (this.$el) {
// 数据劫持,初次劫持并没有触发new Dep()!!!
new Observer(this.$data);
// 设置代理,过滤$data,可直接访问data() 中的数据=> this.xxx
this.proxyData(this.$data);
// 设置methods,同上
this.proxyMethods(methods);
// 设置computed,同上
this.proxyComputed(computed);
// 将模板转化成对象,进行解析
// 默认会执行数据的get操作,触发数据劫持,并设置发布订阅模式
new Compiler(this.$el, this);
// 执行挂载mounted,并且作用域指向data
options.mounted.call(this.$data);
}
}
proxyData(data) {
for (let key in data) {
Object.defineProperty(this, key, {
enumerable: true,
get() {
return this.$data[key];
},
set(newVal) {
if (this.$data[key] != newVal) {
this.$data[key] = newVal;
}
}
});
}
}
proxyMethods(methods) {
for (let key in methods) {
this.$data[key] = methods[key];
}
}
proxyComputed(computed) {
for (let key in computed) {
// this.$data[key] = computed[key].call(this);
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this);
}
});
}
}
}
// 观察者
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldValue = this.get();
}
get() {
Dep.target = this;
let val = CompileUtils.getVal(this.expr, this.vm);
Dep.target = null;
return val;
}
update() {
let newVal = CompileUtils.getVal(this.expr, this.vm);
if (this.oldValue !== newVal) {
this.cb(newVal);
}
}
}
// 订阅者
class Dep {
constructor() {
this.subs = [];
}
// 订阅
addSub(watcher) {
this.subs.push(watcher);
}
// 发布
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
// 编译者
class Compiler {
constructor(el, vm) {
this.el = this.getElementByEl(el);
this.vm = vm;
// 获取dom节点
let fragment = this.node2fragment(this.el);
// 编译模板 用数据编译
this.compile(fragment);
// 把内容塞到页面中
this.el.appendChild(fragment);
}
// 核心编译方法
compile(node) {
let childNodes = node.childNodes;
[...childNodes].forEach((e) => {
if (e.nodeType == 1) {
this.compileElement(e);
} else if (e.nodeType == 3) {
this.compileText(e);
}
});
}
// 编译文本
compileText(node) {
let text = node.textContent;
if (/\{\{(.*)\}\}/.test(text)) CompileUtils.text(node, text, this.vm);
}
// 编译元素
compileElement(node) {
this.compile(node);
let attributes = node.attributes;
[...attributes].forEach((attr) => {
let { name, value } = attr;
if (this.isDirective(name)) {
let [, directive] = name.split('-');
let [directiveName, eventName] = directive.split(':');
CompileUtils[directiveName](node, value, this.vm, eventName);
}
});
}
// 判断是否指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 节点转片段
node2fragment(el) {
let fragment = document.createDocumentFragment();
let node;
while ((node = el.firstChild)) {
fragment.appendChild(node);
}
return fragment;
}
// 获取元素
getElementByEl(el) {
if (el.nodeType === 1) return el;
return document.querySelector(el);
}
}
// 编译工具
var CompileUtils = {
getVal(expr, vm) {
let data = vm.$data;
expr.split('.').forEach((e) => {
data = data[e];
});
return data;
},
setVal(expr, vm, val) {
let data = vm.$data;
expr.split('.').reduce((total, currentValue, index, arr) => {
if (index == arr.length - 1) {
total[currentValue] = val;
return;
}
return total[currentValue];
}, data);
},
getContentValue(expr, vm) {
let value = expr.replace(/\{\{(.*)\}\}/g, (...args) => {
return this.getVal(args[1], vm);
});
return value;
},
getMethodObj(expr, vm) {
console.log(expr);
let leftIndex = expr.indexOf('(');
let method = expr.slice(0, leftIndex);
let params = expr.slice(leftIndex + 1, expr.length - 1).split(',');
vm.$data[method]().call(this, ...params);
return {
method
};
},
// 指令
model(node, expr, vm) {
let value = this.getVal(expr, vm);
let fn = this.update.modelUpdater;
fn(node, value);
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
node.addEventListener('input', (e) => {
let val = e.target.value;
this.setVal(expr, vm, val);
});
},
html(node, expr, vm) {
let value = this.getVal(expr, vm);
let fn = this.update.htmlUpdater;
fn(node, value);
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
},
// 事件绑定
on(node, expr, vm, eventName) {
node.addEventListener(eventName, () => {
let leftIndex = expr.indexOf('(');
let method = expr.slice(0, leftIndex);
let params = expr.slice(leftIndex + 1, expr.length - 1).split(',');
let temParams = [];
params.forEach((param) => {
if (param.indexOf("'") == 0 || param.indexOf('"') == 0) {
param;
temParams.push(param.slice(1, param.length - 1));
} else {
temParams.push(this.getVal(param, vm));
}
});
vm.$data[method].call(this, ...temParams);
});
},
text(node, expr, vm) {
let fn = this.update.textUpdater;
let value = expr.replace(/\{\{(.*)\}\}/g, (...args) => {
new Watcher(vm, args[1], (newVal) => {
fn(node, this.getContentValue(expr, vm));
});
return this.getVal(args[1], vm);
});
fn(node, value);
},
// 更新视图方法
update: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
textUpdater(node, value) {
node.textContent = value;
}
}
};
// 数据劫持
class Observer {
constructor(data) {
//初始化时候劫持数据
this.observer(data);
}
observer(data) {
if (data && typeof data == 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key]);
}
}
}
defineReactive(obj, key, val) {
this.observer(val);
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set: (newVal) => {
if (val == newVal) return;
val = newVal;
// 重新赋值的时候劫持数据
this.observer(newVal);
dep.notify();
}
});
}
}
</script>
<script>
let vm = new Penk({
el: '#app',
data: {
author: {
name: 'penk',
age: 18,
a: { aa: 1 }
},
msg: '<h1>v-html</h1>'
},
methods: {
change(...data) {
alert('method,带参~' + data);
}
},
mounted() {
// this.change('mounted');
},
computed: {
toUpperCaseName() {
return this.author.name.toUpperCase();
}
}
});
</script>
</body>
</html>
效果
待发…