Java 模拟实现 Vue

文章目录

    • 前言
    • 整体结构
    • [模拟 html 中的 div 元素](#模拟 html 中的 div 元素)
    • [模拟 vue.js 中的 构造参数 options](#模拟 vue.js 中的 构造参数 options)
    • [模拟 Object.defineProperty](#模拟 Object.defineProperty)
    • [模拟主类 Vue](#模拟主类 Vue)
    • [模拟 Observer](#模拟 Observer)
    • [模拟 Watcher,Dep](#模拟 Watcher,Dep)
    • [模拟 Compiler](#模拟 Compiler)
    • 模拟运行
    • 总结

前言

前面我们用 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 语言进行重写实现,其结构原理对我来说又清楚了一些,所以掌握原理才是最重要的,不管用什么语言都能实现同样的功能。

这里也提及一下其中用到的设计模式,主要有:

  1. 观察者模式:通过 Observer 组织观察者 Watcher,在数据发生变化(调用了 set 方法)时,就通知观察者更新 token 流进行页面的渲染
  2. 代理模式:Vue 类中有代理 get,set 方法,简化对象调用
  3. 装饰器模式:我认为这才是 Vue 实现的核心,通过装饰器,将一个普通的对象比如字符串,一下子变成了一个有 get 与 set 方法的对象DataWrapper,在 get,set 方法中再塞些逻辑私货
相关推荐
九天轩辕1 小时前
基于 Qt 和 libimobiledevice 的跨平台 iOS 设备管理工具开发实践
开发语言·qt·ios
淘源码d1 小时前
智慧工地企项一体化平台,Spring Cloud +UniApp 智慧工地源码,BIM+AI+物联网,施工全过程数字化智慧工地管理平台
java·人工智能·物联网·智慧工地·智慧工地源码·智慧工地app·数字工地
程序喵大人1 小时前
C++ MCP 服务器实现
开发语言·c++·项目·mcp服务器
谷粒.1 小时前
API测试全解析:从基础到性能压测
java·运维·网络·人工智能·python·测试工具·自动化
小尧嵌入式1 小时前
QT软件开发知识点流程及文本转语音工具
开发语言·c++·qt
月亮!1 小时前
敏捷开发中测试左移的5个关键实践
java·人工智能·python·selenium·测试工具·测试用例·敏捷流程
雨季6661 小时前
Flutter 智慧金融零售服务平台:跨端协同升级金融便民体验
开发语言·javascript·ecmascript
掘根1 小时前
【消息队列项目】SQLite简单介绍
java·oracle·sqlite
专业开发者1 小时前
MTK GNSS 可见性控制指南
开发语言·python·物联网