基本结构

这里我根据自己的理解模仿了Vue的单文件写法,通过给Vue.createApp传入参数再挂载元素来实现页面与数据的互动。

其中理解不免有错,希望大佬轻喷。

收集数据

这里将Vue.createApp()里的参数叫做options

data可以是一个对象或者函数,在是函数的时候必须ruturn出一个对象,该对象里的数据会被vm直接调用。

可以直接先获取options,然后将里面的data函数执行一次再把结果挂载到实例上,methods等对象也可以直接挂载:(这里忽略了data是对象的情况,只按照是函数来处理)

  1. class Vue{
  2. constructor() {
  3. this.datas = Object.create(null);
  4. }
  5. static createApp(options){
  6. const vm = new Vue();
  7. vm.datas = options.data?.call(vm);
  8. for (const key in options.methouds) {
  9. vm.methouds[key] = options.methouds[key].bind(vm);
  10. }
  11. return vm;
  12. }
  13. }

当然这样只是会获得一个Vue实例,上面有输入的数据,这些数据还不会与页面发生互动。

Vue 的响应式数据

Vue的数据双向绑定是通过代理注入来实现的,在vue2中使用Object.defineProperty而到了vue3使用的是ProxyAPI。虽然用的方法不同,但核心思想是一样的:截获数据的改变,然后进行页面更新。

这样就可以试着写出获得代理数据的方法:

  1. class Vue{
  2. constructor() {}
  3. static createApp(options){
  4. const vm = new Vue();
  5. const data = options.data?.call(vm);
  6. for (const key in data) {
  7. vm.datas[key] = vm.ref(data[key]);
  8. }
  9. return vm;
  10. }
  11. reactive(data) {
  12. const vm = this; //! 固定VUE实例,不然下面的notify无法使用
  13. return new Proxy(data, {
  14. //todo 修改对象属性后修改Vnode
  15. set(target, p, value) {
  16. target._isref
  17. ? Reflect.set(target, "value", value)
  18. : Reflect.set(target, p, value);
  19. //todo 在这里通知,然后修改页面
  20. dep.notify(vm);
  21. return true;
  22. },
  23. });
  24. }
  25. ref(data) {
  26. //? 基本数据类型会被包装为对象再进行代理
  27. if (typeof data != "object") {
  28. data = {
  29. value: data,
  30. _isref: true,
  31. toSting() {
  32. return this.value;
  33. },
  34. };
  35. }
  36. return this.reactive(data);
  37. }
  38. }

现在如果data中设置的数据发生了改变,那么就会调用dep.notify来改变页面内容。

vm代理datas等数据

因为再模板里是不会写this.datas.xxx来调用数据的,这里也可以使用代理来把datas中的数据放到vm上:

  1. class Vue {
  2. constructor() {
  3. //! 因为vm代理了datas 以后在vm上添加新属性会被移动到datas中,所以如果是实例上的属性要像el一样占位
  4. this.el = "document";
  5. this.mountHTML = "mountHTML";
  6. this.datas = Object.create(null);
  7. this.methouds = Object.create(null);
  8. }
  9. static createApp(options) {
  10. //? 将data代理到vm上
  11. const vm = new Proxy(new Vue(), {
  12. get(target, p) {
  13. if (Reflect.get(target, p)) {
  14. return Reflect.get(target, p);
  15. } else {
  16. return target.datas[p]._isref ? target.datas[p].value : target.datas[p];
  17. }
  18. },
  19. set(target, p, value) {
  20. if (target[p]) {
  21. Reflect.set(target, p, value);
  22. } else if (target.datas[p]?._isref) {
  23. Reflect.set(target.datas[p], "value", value);
  24. } else {
  25. Reflect.set(target.datas, p, value);
  26. }
  27. return true;
  28. },
  29. });
  30. //? onBeforeCreate
  31. options.onBeforCreate?.call(vm);
  32. const data = options.data?.call(vm);
  33. for (const key in data) {
  34. vm.datas[key] = vm.ref(data[key]);
  35. }
  36. for (const key in options.methouds) {
  37. vm.methouds[key] = options.methouds[key].bind(vm);
  38. }
  39. //? onCreated
  40. options.onCreated?.call(vm);
  41. return vm;
  42. }
  43. }

这样通过createApp获得的Vue实例直接访问并修改收集到的datas里的数据。

挂载

通过Vue.createApp可以获得一个Vue实例,这样只需要调用实例中的mount方法就可以进行挂载了,在挂载后就马上进行数据的渲染。

vm.mount接收一个参数,可以是css选择器的字符串,也可以直接是html节点:

  1. class Vue{
  2. constructor() {}
  3. mount(el) {
  4. //todo 初始化
  5. this.init(el);
  6. //todo 渲染数据
  7. render(this);
  8. return this;
  9. }
  10. init(el) {
  11. this.el = this.getEl(el);
  12. this.mountHTML = this.el.innerHTML; //? 获得挂载时元素的模板
  13. }
  14. getEl(el) {
  15. if (!(el instanceof Element)) {
  16. try {
  17. return document.querySelector(el);
  18. } catch {
  19. throw "没有选中挂载元素";
  20. }
  21. } else return el;
  22. }
  23. }

渲染页面

Vue渲染页面使用了VNode来记录并按照它进行页面的渲染,在每次更新数据时获得数据更新的地方并通过diff算法来比较旧VNode和更新数据后VNode的不同来对页面进行渲染。

这里不做太复杂处理,直接把挂载节点的innerHTML作为模板,通过正则进行捕获并修改,然后渲染到页面上,同时如果有通过@ 或 v-on绑定的事件,则按照情况进行处理:

  • 如果是原生的事件,则直接添加进去;
  • 如果是非原生的事件,则通过on来记录,以后用emit来进行触发。
  1. export default function render(vm) {
  2. const regexp =
  3. /(?<tag>(?<=<)[^\/]+?(?=(>|\s)))|\{\{(\s*)(?<data>.+?)(\s*)\}\}|(?<text>(?<=>)\S+?(?=<))|(?<eName>(?<=@|(v-on:))\S+?)(=")(?<event>\S+?(?="))/g;
  4. const fragment = document.createDocumentFragment();
  5. let ele = {};
  6. //? 每次匹配到tag就把获得的信息转成标签
  7. for (const result of vm.mountHTML.matchAll(regexp)) {
  8. if (result.groups.tag && ele.tag) {
  9. fragment.appendChild(createEle(vm, ele));
  10. ele = {};
  11. }
  12. Object.assign(ele, JSON.parse(JSON.stringify(result.groups)));
  13. }
  14. fragment.appendChild(createEle(vm, ele)); //? 最后这里再执行一次把最后的一个元素也渲染
  15. ele = null;
  16. //? 清空原来的DOM
  17. vm.el.innerHTML = "";
  18. vm.el.appendChild(fragment);
  19. }
  20. //? 放入原生事件,用字典储存,这里只记录了click
  21. const OrangeEvents = { click: Symbol() };
  22. /**
  23. * 根据解析的数据创建放入文档碎片的元素
  24. */
  25. function createEle(vm, options) {
  26. const { tag, text, data, eName, event } = options;
  27. if (tag) {
  28. const ele = document.createElement(tag);
  29. if (data) {
  30. ele.innerText = getByPath(vm, data);
  31. }
  32. if (text) {
  33. ele.innerText = text;
  34. }
  35. if (event) {
  36. //todo 先判断是不是原生事件,是就直接绑定,不然用eventBinder来注册
  37. if (OrangeEvents[eName]) {
  38. ele.addEventListener(eName, vm.methouds[event]);
  39. } else {
  40. eventBinder.off(eName); //? 因为这里render的实现是重新全部渲染,所以要清空对应的事件缓存
  41. eventBinder.on(eName, vm.methouds[event].bind(vm));
  42. }
  43. }
  44. return ele;
  45. }
  46. }
  47. /**
  48. * 通过字符串来访问对象中的属性
  49. */
  50. function getByPath(obj, path) {
  51. const pathArr = path.split(".");
  52. return pathArr.reduce((result, curr) => {
  53. return result[curr];
  54. }, obj);
  55. }

这里的正则用了具名组匹配符,可以通过我的这篇博客来了解。

这里渲染函数只是进行简单渲染,没有考虑到字符和数据同时出现的情况,也没有考虑标签嵌套的问题,只能平铺标签。。。

注册事件

事件注册就是一个标准的发布订阅者模式的实现了,可以看看我的这篇博客(讲的并不详细)

这里对事件绑定进行了简化,只保留了on off emit三个方法:

  1. class Event {
  2. constructor() {
  3. this.collector = Object.create(null);
  4. }
  5. on(eName, cb) {
  6. this.collector[eName] ? this.collector[eName].push(cb) : (this.collector[eName] = [cb]);
  7. }
  8. off(eName, cb) {
  9. if (!(eName && cb)) {
  10. this.collector = Object.create(null);
  11. } else if (eName && !cb) {
  12. delete this.collector[eName];
  13. } else {
  14. this.collector[eName].splice(this.collector[eName].indexOf(cb), 0);
  15. }
  16. return this;
  17. }
  18. emit(eName, ...arg) {
  19. for (const cb of this.collector[eName]) {
  20. cb(...arg);
  21. }
  22. }
  23. }
  24. const eventBinder = new Event();
  25. export { eventBinder };
  26. export default eventBinder.emit.bind(eventBinder); //! emit会被注册到vm上,让它的this始终指向eventBinder

更新页面

有了渲染函数就可以根据数据的变化来渲染页面了,如果一次有多个数据进行修改,那么会触发多次渲染函数,这是明显的性能浪费,所以引用任务队列的概念来保证一次操作只会重新渲染一次页面:

  1. // Dep.js
  2. export default class Dep {
  3. constructor() {
  4. this.lock = true;
  5. }
  6. notify(vm) {
  7. //? onBeforeUpdate
  8. //! 把更新视图放到微任务队列,即使多个数据改变也只渲染一次
  9. if (this.lock) {
  10. this.lock = false;
  11. //! 应该在这里运用diff算法更新DOM树 这里只是重新渲染一次页面
  12. nextTick(render, vm);
  13. nextTick(() => (this.lock = true)); //? onUpdated
  14. }
  15. }
  16. }
  17. // nextTick.js
  18. export default function nextTick(cb, ...arg) {
  19. Promise.resolve().then(() => {
  20. cb(...arg);
  21. });
  22. }

结语

代码地址

说不定还会试着加入其它功能。

手写一个超简单的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. jmeter--JSON Extractor 用法

    JMeter处理大部分请求返回的结果,都是json.对于请求返回的结果,处理以后作为其他请求的参数,有一个方便使用的插件:JSON Extractor JSON Extractor中文叫做json提取 ...

  2. 【工具解析】瑞士军刀bettercap2.X解析_第一期_编写HTTP代理注入模块_http(s).proxy.script

    /文章作者:Kali_MG1937 CNBLOG博客号:ALDYS4 QQ:3496925334/ 前言 bettercap已经从1.6更新至2.0版本 语言也从ruby改为了go 编写注入模块指定的 ...

  3. java笔试题(二)

    1.写出一维数组初始化的两种方式 int[] arr={1,2,3}; String[] str=new String[2]; str[1]="23"; 2.写出二维数组初始化的两 ...

  4. Java程序安装失败

      检查文件路径,应该不含中文汉字,空格以及特殊字符.应将jdk的安装目录设置为纯英文路径. 是否有多个安装程序同时运行,若多点安装程序则会安装失败,打开任务管理器,查看是否有多个安装程序运行 注册表 ...

  5. c#json将字符串反序列化成对象时不新建类的做法

    在服务端代码文件中加上struct结构体就能解决 struct LocationInfo { public string LocationID { get; set; } public string ...

  6. 熬夜总结vue3中setUp函数的2个参数详解

    1.setUp函数的第1个参数props setup(props,context){} 第一个参数props: props是一个对象,包含父组件传递给子组件的所有数据. 在子组件中使用props进行接 ...

  7. k8s 1.12 环境部署及学习笔记

    1.K8S概述 1.Kubernetes是什么 2.Kubernetes特性 3.Kubernetes集群架构与组件 4.Kubernetes核心概念 1.1 Kubernetes是什么 • Kube ...

  8. Java基础篇(JVM)——字节码详解

    这是Java基础篇(JVM)的第一篇文章,本来想先说说Java类加载机制的,后来想想,JVM的作用是加载编译器编译好的字节码,并解释成机器码,那么首先应该了解字节码,然后再谈加载字节码的类加载机制似乎 ...

  9. 7、linux快捷键

    ctrl +a:切换到命令行开始 ctrl+e:切换到命令行结尾 ctrl+c:终止当前命令或脚本 ctrl+d:退出当前shell,相当于exit ctrl+l:清除当前屏幕的内容,相当于clear ...

  10. 9、解决mstsc卡顿的问题:

    1.同时按住"win+r"键调出"运行",在方框内输入"cmd"后点击"确定"打开dos窗口: 2.在dos中输入&qu ...