文章目录
前言
前面我们用 js 写了一个 zvue.js(简版的 vue.js),可以再回顾一下 深入理解 Vue.js 原理,其原理架构图为:

并且实现了 Vue 中的插值功能 {``{xxx}},但 js 给我的感觉没有那么好,没有 java 面象对象结构清楚,因此想弄清 vue 的原理反正我是挺费劲的,所以我打算用 java 重写一遍,来深刻理解其逻辑原理。最近也在重温设计模式,Vue 不就是观察者模式吗?来吧!
整体结构
下面就一步步用代码来实现简单的 Vue 吧,代码已上传 gitee,整个文件目录结构如下:
bash
vue
├── Compiler.java
├── Dep.java
├── Observer.java
├── Vue.java
├── Watcher.java
└── bean
├── DataWrapper.java
├── Div.java
└── Options.java

我是照着我的简版 zvue.js 来改的,所以 Vue,Observer,Compiler,Dep,Watcher 几个类都是老熟人了。
它们结构也都类似 zvue.js 中的结构,例如 Dep 类:
java
@Data
public class Dep {
private List<Watcher> subs = new ArrayList<>(10);
public static Watcher target = null;
//将观察者添加进观察者列表
public void subsAdd(Watcher watcher) {
this.subs.add(watcher);
}
// 通知观察者进行更新
public void subsNotify() {
subs.forEach(Watcher::update);
}
}
模拟 html 中的 div 元素
本篇不做那么复杂,只要实现 vue 中的插值就行了,先来一个 Div 类
java
@Data
public class Div {
private String id;
private String textContent = "{{msg}}-{{msg}}";
public Div(String id) {
this.id = id;
}
}
模拟 vue.js 中的 构造参数 options
还记得大明湖畔的 new Vue(options) 吗?不记得就再看一下:
js
const app = new Vue({
el: '#app',
data: {
message: 'Hello world!'
}
});
需要传参给 Vue 的就是 options ,一切皆对象,我们就再来一个类 Options:
java
@Data
@AllArgsConstructor
public class Options {
private String el;
private Map<String, Object> data;
}
模拟 Object.defineProperty
来看一个 zvue.js 中 js 代码片段:
js
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
//目的:this.xx==this.$data.xx
get() {
console.log("proxy -1- get data[key]:", data[key]);
return data[key];
},
set(newVal) {
console.log("proxy -2- set data[key]=", newVal);
data[key] = newVal;
},
});
Object.defineProperty 其实用到了装饰器模式,就是为了把一个对象(比如符串)包装一下变成另一个有 get 和 set 方法 的对象,在 get 和 set 方法里面还可以夹带私货
那就来一个 DataWrapper 类,因为后面有用到 Dep,不同于 js 直接用,java 这里要 传入参数 Dep:
java
public class DataWrapper {
private Object val;
private final Dep dep;
public DataWrapper(Object val, Dep dep) {
this.val = val;
this.dep = dep;
}
public Object get() {
if (Dep.target != null) { dep.subsAdd(Dep.target); }
return this.val;
}
public void set(Object newVal) {
if (val == newVal) { return; }
this.val = newVal;
if (val != null && val instanceof Map) { new Observer((Map<String, Object>) val); }
dep.subsNotify();
}
}
使用示例如下,目的就是让字符串有了 get 与 set 的方法:
java
public class Main{
public static void main(String[] args) {
DataWrapper w = new DataWrapper("123", new Dep());
System.out.println(w.get());
w.set("12345");
System.out.println(w.get());
}
}
模拟主类 Vue
Vue 类也就是主入口,从这里开始执行初始化,解析模板
java
@Data
public class Vue {
private Options options;
private Map<String, Object> data;
private String el;
private Div divEl;
public Vue(Options options) {
this.options = options;
this.data = options.getData();
// el: 字符串,div 的 id
this.el = options.getEl();
// 模拟 vue.js 中的 $el
this.divEl = new Div(el);
System.out.println("Vue -1- div text: " + divEl.getTextContent());
new Observer(this.data);
new Compiler(this, this.divEl);
}
// 代理data取值,data 为 key-val 形式,返回 data 中的 val
public Object proxyGet(String key) {
Object val = this.data.get(key);
if (val instanceof DataWrapper) {
return ((DataWrapper) val).get();
}
return val;
}
// 代理data设值,data 为 key-val 形式,newVal 新值
public void proxySet(String key, Object newVal) {
Object val = this.data.get(key);
if (val instanceof DataWrapper) {
DataWrapper obj = (DataWrapper) val;
obj.set(newVal);
}
this.data.put(key, val);
}
}
模拟 Observer
Observer 观察者模式工具类,用于组织观察者模式相应动作,将 data 中的每个 value 数据都包装一下
java
@Data
public class Observer {
// 数据map,模拟的是 vue.$data
private Map<String, Object> data;
// 观察者管理器
private Dep dep;
// 观察者模式工具类,用于组织观察者模式相应动作,data 为数据 map
public Observer(Map<String, Object> data) {
this.data = data;
this.dep = new Dep();
this.walk(data, dep);
}
// 遍历数据进行装操作,data 为数据 map,dep 为观察者管理器
private void walk(Map<String, Object> data, Dep dep) {
data.forEach((k, v) -> this.doDefProp(data, k, dep));
}
// 包装数据为一个DataWrapper对象,key为键值,dep为观察者管理器
private void doDefProp(Map<String, Object> data, String key, Dep dep) {
Object val = data.get(key);
DataWrapper wrapper = new DataWrapper(val, dep);
data.put(key, wrapper);
}
}
模拟 Watcher,Dep
观察者 Watcher,主要功能绑定key,监测并更新旧值
java
@Data
public class Watcher {
private Vue vm;
private String key;
private List<String> tokens;
private int pos;
private Object oldVal;
// 构造函数
public Watcher(Vue vm, String key, List<String> tokens, int pos) {
this.vm = vm;
this.key = key;
this.tokens = tokens;
this.pos = pos;
Dep.target = this;
this.oldVal = vm.proxyGet(key);
Dep.target = null;
}
// 更新逻辑
public void update() {
Object newVal = this.vm.proxyGet(this.key);
if (newVal == this.oldVal) { return; }
this.callback(newVal);
this.oldVal = newVal;
}
// 将旧插值更新,newVal 新值
private void callback(Object newVal) {
this.tokens.set(pos, newVal.toString());
String content = String.join("", tokens);
System.out.println("callback -- div content:" + content);
this.vm.getDivEl().setTextContent(content);
}
}
观察者管理器 Dep 是把各观察者放到一个列表中,有数据变化就通知到各个观察者进行更新动作
java
// 观察者管理器
@Data
public class Dep {
private List<Watcher> subs = new ArrayList<>(10);
// 全局共享的对象,
public static Watcher target = null;
// 将观察者添加进观察者列表
public void subsAdd(Watcher watcher) {
this.subs.add(watcher);
}
// 通知观察者进行更新
public void subsNotify() {
subs.forEach(Watcher::update);
}
}
模拟 Compiler
Compiler 模板解析类,用途是解析模板,将模板变成一个 token 流,记录 Vue 插值,指令相关的位置,当有数据变化时,能够找到位置并进行旧值替代,从而实现页面的渲染更新。
java
public class Compiler {
private final Vue vm;
private final Div el;
private static final Pattern TEXT_PATTERN =
Pattern.compile("\\{\\{(.+?)}}");
public Compiler(Vue vm, Div el) {
this.vm = vm;
this.el = el;
this.compileText(this.el);
}
private void compileText(Div el) {
if (this.el == null) {
return;
}
String textContent = el.getTextContent();
Matcher divMatcher = TEXT_PATTERN.matcher(textContent);
List<String> tokens = new ArrayList<>();
int lastEnd = 0;
while (divMatcher.find()) {
int start = divMatcher.start();
int end = divMatcher.end();
String group = divMatcher.group(1);
if (lastEnd < start) {
tokens.add(textContent.substring(lastEnd, start));
}
tokens.add(this.vm.proxyGet(group).toString());
lastEnd = end;
int pos = tokens.size() - 1;
new Watcher(this.vm, group, tokens, pos);
}
if (CollUtil.isNotEmpty(tokens)) {
String content = String.join("", tokens);
this.vm.getDivEl().setTextContent(content);
}
}
}
使用示例程序:
java
public class Main{
public static void main(String[] args) {
String textContent = "{{msg}}-{{msg1}}-{{msg2}}";
Matcher divMatcher = TEXT_PATTERN.matcher(textContent);
List<String> tokens = new ArrayList<>();
int lastEnd = 0;
while (divMatcher.find()) {
int start = divMatcher.start();
int end = divMatcher.end();
String group = divMatcher.group(1);
System.out.println("整个匹配: " + divMatcher.group(0));
System.out.println("第一个分组: " + divMatcher.group(1));
System.out.println("start:" + divMatcher.start());
System.out.println("end: " + divMatcher.end());
if (lastEnd < start) {
tokens.add(textContent.substring(lastEnd, start));
}
tokens.add(group);
lastEnd = end;
}
System.out.println(tokens);
}
}
模拟运行
java
public class Main{
public static void main(String[] args) {
// el
String divId = "app";
Map<String, Object> data = new HashMap<>(4);
data.put("msg", "hello vue");
System.out.println("初始 msg 为 hello vue:");
Options options = new Options(divId, data);
Vue app = new Vue(options);
System.out.println("\nhtml 页面结果:");
System.out.println(app.divEl.getTextContent());
System.out.println("\n变更 msg 为 1234:");
// 更新 app.data.msg 的值,div 的 textContent 也会相应更新
app.proxySet("msg", 1234);
System.out.println("\nhtml 页面结果:");
System.out.println(app.divEl.getTextContent());
}
}
运行日志如下:
log
初始 msg 为 hello vue:
Vue -1- div text: {{msg}}-{{msg}}
html 页面结果:
hello vue-hello vue
变更 msg 为 1234:
callback -- div content:1234-hello vue
callback -- div content:1234-1234
html 页面结果:
1234-1234
总结
本文基于前面写过的简版 vue.js 用 java 语言进行重写实现,其结构原理对我来说又清楚了一些,所以掌握原理才是最重要的,不管用什么语言都能实现同样的功能。
这里也提及一下其中用到的设计模式,主要有:
观察者模式:通过 Observer 组织观察者 Watcher,在数据发生变化(调用了 set 方法)时,就通知观察者更新 token 流进行页面的渲染代理模式:Vue 类中有代理 get,set 方法,简化对象调用装饰器模式:我认为这才是 Vue 实现的核心,通过装饰器,将一个普通的对象比如字符串,一下子变成了一个有 get 与 set方法的对象DataWrapper,在 get,set 方法中再塞些逻辑私货