基本结构

这里我根据自己的理解模仿了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的更多相关文章

  1. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  2. 手把手教你手写一个最简单的 Spring Boot Starter

    欢迎关注微信公众号:「Java之言」技术文章持续更新,请持续关注...... 第一时间学习最新技术文章 领取最新技术学习资料视频 最新互联网资讯和面试经验 何为 Starter ? 想必大家都使用过 ...

  3. 手写一个最简单的IOC容器,从而了解spring的核心原理

    从事开发工作多年,spring源码没有特意去看过.但是相关技术原理倒是背了不少,毕竟面试的那关还是得过啊! 正所谓面试造火箭,工作拧螺丝.下面实现一个最简单的ioc容器,供大家参考. 1.最终结果 2 ...

  4. 手写一个最迷你的Web服务器

    今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码.  2. 代码介绍: ( ...

  5. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  6. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  7. 手写一个简单的ElasticSearch SQL转换器(一)

    一.前言 之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql 插件, ...

  8. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  9. 浅析MyBatis(二):手写一个自己的MyBatis简单框架

    在上一篇文章中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBa ...

随机推荐

  1. git介绍及使用

    一.架构 版本库(仓库):工作区中有一个隐藏目录.git,这个目录不属于工作区,而是git的版本库,是git管理的所有内容. 暂存区:版本库中包含一个临时区域,保存下一步要提交的文件. 分支:版本库中 ...

  2. UF_DRF 制图符号标注尺寸

    Open C uc5530uc5531uc5532uc5533uc5534uc5540uc5541uc5542uc5543uc5550uc5551uc5563uc5566uf5505uf5506uf5 ...

  3. 使用 Hexo 搭建静态博客

    目录 Hexo 简介 什么是 Hexo? Hexo 安装 Hexo 建站 Hexo 配置 Hexo 自定义主题 Hexo 写作 Hexo 服务器 Hexo 生成与发布 一键部署 Hexo 站点到 gi ...

  4. 「模拟8.18」字符串(卡特兰数)·乌鸦喝水(树状数组,二分)·所驼门王的宝藏(tarjan,拓扑)

    最近好颓啊,所以啥都做不出来 简单说一下这次考试,分机房了,还分不同考卷,果然我还是留在二机房的蒟蒻, 大概也只有这样的简单题,才能勉强水个rank 3吧........ 其实不必管在哪个机房,努力便 ...

  5. DOS命令行(3)——Windows运行窗口打开特定功能

    cmd 开启命令解释权或启用Windows中的某程序 cmd /c 执行完命令后关闭命令窗口 cmd /k 执行完命令后不关闭命令窗口   winver 查看Windows版本信息版权信息授权信息   ...

  6. [.NET Core知识点回顾]-自动内存管理

    自动内存管理是公共语言运行时在托管执行过程中提供的服务之一.公共语言运行时的垃圾回收器为应用程序管理内存 的分配和释放.对开发人员而言,在开发托管应用程序时不必编写执行内存管理任务代码. 分配内存 初 ...

  7. Linux中useradd的用法

    语法:useradd [选项] 用户名 选项: -d<登陆目录> 指定新用户登陆的起始目录,默认为/home -e<有效期限> 指定用户的有效期限,格式为 YYYY-MM-DD ...

  8. vue cli3 创建项目

    1.确认是否由安装由vue 命令提示符 执行 vue -V 如果没有则执行 npm uninstall vue-cli 2.创建项目 vue create demo1 具体操作如下: (1)执行以上命 ...

  9. solidity 小案例 收费站

    solidity IDE下载地址 https://pan.baidu.com/s/1cY8VgDqB9Wt9VzK-Nocbyw 代码案例: pragma solidity ^0.4.0; //创建合 ...

  10. inux下查看最消耗CPU、内存的进程

    1.CPU占用最多的前10个进程: ps auxw|head -1;ps auxw|sort -rn -k3|head -10 2.内存消耗最多的前10个进程 ps auxw|head -1;ps a ...