手写一个超简单的Vue
基本结构
这里我根据自己的理解模仿了Vue的单文件写法,通过给Vue.createApp传入参数再挂载元素来实现页面与数据的互动。
其中理解不免有错,希望大佬轻喷。
收集数据
这里将
Vue.createApp()里的参数叫做options
data可以是一个对象或者函数,在是函数的时候必须ruturn出一个对象,该对象里的数据会被vm直接调用。
可以直接先获取options,然后将里面的data函数执行一次再把结果挂载到实例上,methods等对象也可以直接挂载:(这里忽略了data是对象的情况,只按照是函数来处理)
class Vue{
    constructor() {
        this.datas = Object.create(null);
    }
    static createApp(options){
        const vm = new Vue();
        vm.datas = options.data?.call(vm);
        for (const key in options.methouds) {
            vm.methouds[key] = options.methouds[key].bind(vm);
        }
        return vm;
    }
}
当然这样只是会获得一个Vue实例,上面有输入的数据,这些数据还不会与页面发生互动。
Vue 的响应式数据
Vue的数据双向绑定是通过代理注入来实现的,在vue2中使用Object.defineProperty而到了vue3使用的是ProxyAPI。虽然用的方法不同,但核心思想是一样的:截获数据的改变,然后进行页面更新。
这样就可以试着写出获得代理数据的方法:
class Vue{
    constructor() {}
    static createApp(options){
        const vm = new Vue();
        const data = options.data?.call(vm);
        for (const key in data) {
            vm.datas[key] = vm.ref(data[key]);
        }
        return vm;
    }
    reactive(data) {
        const vm = this; //! 固定VUE实例,不然下面的notify无法使用
        return new Proxy(data, {
            //todo 修改对象属性后修改Vnode
            set(target, p, value) {
                target._isref
                    ? Reflect.set(target, "value", value)
                    : Reflect.set(target, p, value);
                //todo 在这里通知,然后修改页面
                dep.notify(vm);
                return true;
            },
        });
    }
    ref(data) {
        //? 基本数据类型会被包装为对象再进行代理
        if (typeof data != "object") {
            data = {
                value: data,
                _isref: true,
                toSting() {
                    return this.value;
                },
            };
        }
        return this.reactive(data);
    }
}
现在如果data中设置的数据发生了改变,那么就会调用dep.notify来改变页面内容。
vm代理datas等数据
因为再模板里是不会写this.datas.xxx来调用数据的,这里也可以使用代理来把datas中的数据放到vm上:
class Vue {
    constructor() {
        //! 因为vm代理了datas 以后在vm上添加新属性会被移动到datas中,所以如果是实例上的属性要像el一样占位
        this.el = "document";
        this.mountHTML = "mountHTML";
        this.datas = Object.create(null);
        this.methouds = Object.create(null);
    }
    static createApp(options) {
        //? 将data代理到vm上
        const vm = new Proxy(new Vue(), {
            get(target, p) {
                if (Reflect.get(target, p)) {
                    return Reflect.get(target, p);
                } else {
                    return target.datas[p]._isref ? target.datas[p].value : target.datas[p];
                }
            },
            set(target, p, value) {
                if (target[p]) {
                    Reflect.set(target, p, value);
                } else if (target.datas[p]?._isref) {
                    Reflect.set(target.datas[p], "value", value);
                } else {
                    Reflect.set(target.datas, p, value);
                }
                return true;
            },
        });
        //? onBeforeCreate
        options.onBeforCreate?.call(vm);
        const data = options.data?.call(vm);
        for (const key in data) {
            vm.datas[key] = vm.ref(data[key]);
        }
        for (const key in options.methouds) {
            vm.methouds[key] = options.methouds[key].bind(vm);
        }
        //? onCreated
        options.onCreated?.call(vm);
        return vm;
    }
}
这样通过createApp获得的Vue实例直接访问并修改收集到的datas里的数据。
挂载
通过Vue.createApp可以获得一个Vue实例,这样只需要调用实例中的mount方法就可以进行挂载了,在挂载后就马上进行数据的渲染。
vm.mount接收一个参数,可以是css选择器的字符串,也可以直接是html节点:
class Vue{
    constructor() {}
    mount(el) {
        //todo 初始化
        this.init(el);
        //todo 渲染数据
        render(this);
        return this;
    }
    init(el) {
        this.el = this.getEl(el);
        this.mountHTML = this.el.innerHTML; //? 获得挂载时元素的模板
    }
    getEl(el) {
        if (!(el instanceof Element)) {
            try {
                return document.querySelector(el);
            } catch {
                throw "没有选中挂载元素";
            }
        } else return el;
    }
}
渲染页面
Vue渲染页面使用了VNode来记录并按照它进行页面的渲染,在每次更新数据时获得数据更新的地方并通过diff算法来比较旧VNode和更新数据后VNode的不同来对页面进行渲染。
这里不做太复杂处理,直接把挂载节点的innerHTML作为模板,通过正则进行捕获并修改,然后渲染到页面上,同时如果有通过@ 或 v-on绑定的事件,则按照情况进行处理:
- 如果是原生的事件,则直接添加进去;
- 如果是非原生的事件,则通过on来记录,以后用emit来进行触发。
export default function render(vm) {
    const regexp =
        /(?<tag>(?<=<)[^\/]+?(?=(>|\s)))|\{\{(\s*)(?<data>.+?)(\s*)\}\}|(?<text>(?<=>)\S+?(?=<))|(?<eName>(?<=@|(v-on:))\S+?)(=")(?<event>\S+?(?="))/g;
    const fragment = document.createDocumentFragment();
    let ele = {};
    //? 每次匹配到tag就把获得的信息转成标签
    for (const result of vm.mountHTML.matchAll(regexp)) {
        if (result.groups.tag && ele.tag) {
            fragment.appendChild(createEle(vm, ele));
            ele = {};
        }
        Object.assign(ele, JSON.parse(JSON.stringify(result.groups)));
    }
    fragment.appendChild(createEle(vm, ele)); //? 最后这里再执行一次把最后的一个元素也渲染
    ele = null;
    //? 清空原来的DOM
    vm.el.innerHTML = "";
    vm.el.appendChild(fragment);
}
//? 放入原生事件,用字典储存,这里只记录了click
const OrangeEvents = { click: Symbol() };
/**
 * 根据解析的数据创建放入文档碎片的元素
 */
function createEle(vm, options) {
    const { tag, text, data, eName, event } = options;
    if (tag) {
        const ele = document.createElement(tag);
        if (data) {
            ele.innerText = getByPath(vm, data);
        }
        if (text) {
            ele.innerText = text;
        }
        if (event) {
            //todo 先判断是不是原生事件,是就直接绑定,不然用eventBinder来注册
            if (OrangeEvents[eName]) {
                ele.addEventListener(eName, vm.methouds[event]);
            } else {
                eventBinder.off(eName); //? 因为这里render的实现是重新全部渲染,所以要清空对应的事件缓存
                eventBinder.on(eName, vm.methouds[event].bind(vm));
            }
        }
        return ele;
    }
}
/**
 * 通过字符串来访问对象中的属性
 */
function getByPath(obj, path) {
    const pathArr = path.split(".");
    return pathArr.reduce((result, curr) => {
        return result[curr];
    }, obj);
}
这里的正则用了具名组匹配符,可以通过我的这篇博客来了解。
这里渲染函数只是进行简单渲染,没有考虑到字符和数据同时出现的情况,也没有考虑标签嵌套的问题,只能平铺标签。。。
注册事件
事件注册就是一个标准的发布订阅者模式的实现了,可以看看我的这篇博客(讲的并不详细)
这里对事件绑定进行了简化,只保留了on off emit三个方法:
class Event {
    constructor() {
        this.collector = Object.create(null);
    }
    on(eName, cb) {
        this.collector[eName] ? this.collector[eName].push(cb) : (this.collector[eName] = [cb]);
    }
    off(eName, cb) {
        if (!(eName && cb)) {
            this.collector = Object.create(null);
        } else if (eName && !cb) {
            delete this.collector[eName];
        } else {
            this.collector[eName].splice(this.collector[eName].indexOf(cb), 0);
        }
        return this;
    }
    emit(eName, ...arg) {
        for (const cb of this.collector[eName]) {
            cb(...arg);
        }
    }
}
const eventBinder = new Event();
export { eventBinder };
export default eventBinder.emit.bind(eventBinder); //! emit会被注册到vm上,让它的this始终指向eventBinder
更新页面
有了渲染函数就可以根据数据的变化来渲染页面了,如果一次有多个数据进行修改,那么会触发多次渲染函数,这是明显的性能浪费,所以引用任务队列和锁的概念来保证一次操作只会重新渲染一次页面:
// Dep.js
export default class Dep {
    constructor() {
        this.lock = true;
    }
    notify(vm) {
        //? onBeforeUpdate
        //! 把更新视图放到微任务队列,即使多个数据改变也只渲染一次
        if (this.lock) {
            this.lock = false;
            //! 应该在这里运用diff算法更新DOM树 这里只是重新渲染一次页面
            nextTick(render, vm);
            nextTick(() => (this.lock = true)); //? onUpdated
        }
    }
}
// nextTick.js
export default function nextTick(cb, ...arg) {
    Promise.resolve().then(() => {
        cb(...arg);
    });
}
结语
说不定还会试着加入其它功能。
手写一个超简单的Vue的更多相关文章
- 【spring】--  手写一个最简单的IOC框架
		1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ... 
- 手把手教你手写一个最简单的 Spring Boot Starter
		欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ... 
- 手写一个最简单的IOC容器,从而了解spring的核心原理
		从事开发工作多年,spring源码没有特意去看过.但是相关技术原理倒是背了不少,毕竟面试的那关还是得过啊! 正所谓面试造火箭,工作拧螺丝.下面实现一个最简单的ioc容器,供大家参考. 1.最终结果 2 ... 
- 手写一个最迷你的Web服务器
		今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码. 2. 代码介绍: ( ... 
- 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理
		摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ... 
- 利用SpringBoot+Logback手写一个简单的链路追踪
		目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ... 
- 手写一个简单的ElasticSearch SQL转换器(一)
		一.前言 之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql 插件, ... 
- 剖析手写Vue,你也可以手写一个MVVM框架
		剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ... 
- 浅析MyBatis(二):手写一个自己的MyBatis简单框架
		在上一篇文章中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBa ... 
随机推荐
- 女朋友看了也懂的Kafka(下篇)
			前言: 在上篇中我们了解了Kafka是什么,为什么需要Kafka,以及Kafka的基本架构和各自的作用是什么,这篇文章中我们将从kafka内部每一个组成部分去看kafka 是如何保证数据的可靠性以及工 ... 
- 使用NDepend衡量代码的SOLID程度
			SOLID是面向对象的软件开发中的5条准则,也是开发人员可以提升自己代码质量的准则.那么如何衡量自己的代码是否符合SOLID准则呢?NDepend这款工具也许可以帮得上忙.本文将介绍一些NDepend ... 
- 【C++】秒级时间戳,毫秒级时间戳
			时间戳,秒级 测试代码: #include <iostream> #include <time.h> #include <windows.h> using name ... 
- 【NX二次开发】Block UI 截面构建器
			属性说明 属性 类型 描述 常规 BlockID String 控件ID Enable Logical 是否可操作 Group ... 
- 【NX二次开发】Block UI   选择小平面区域
			属性说明 属性 类型 描述 常规 BlockID String 控件ID Enable Logical 是否可操作 Group ... 
- 别再说Java对象都是在堆内存上分配空间的了!
			Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ... 
- [翻译]Go与C#对比 第三篇:编译、运行时、类型系统、模块和其它的一切
			Go vs C#, Part 3: Compiler, Runtime, Type System, Modules, and Everything Else | by Alex Yakunin | S ... 
- Oracle数据泵导出数据库
			Oracle数据泵导出数据库 特别注意:如果后续要导入的数据库版本低,所有导出命令就需要在后面加一个version=指定版本. 例如从11g导出数据导入到10g,假设10g具体版本为10.2.0.1, ... 
- ES6学习笔记之字符串新增方法
			1.字符串的子串识别 传统上,Javascript 只有indexof 方法,用来确定一个字符串是否包含在另一个字符串中.如: //indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的 ... 
- 五、JavaSE语言基础之流程控制
			流程控制的简单概念 流程控制:指通过关键字控制代码的执行流程; 一.顺序结构:执行顺序从左到右从上到下; 顺序结构无关键字,一般而言大多数代码都是顺序结构; 二.选择结构:根据条件判断是否执行 选择结 ... 
