vue3源码难学,先从petite-vue开始吧
如今这个世道,作为一个有几年工作经验的前端,不学点框架源码都感觉要被抛弃了,react或vue要能吹吹牛吧,最好能造个轮子,听说vue3源码好学点,那么学学vue3,但是学起来还是那么费劲,感觉快放弃了,就在这个时候出现了petite-vue,害,这家伙比vue简单啊,拿它来重拾学习源码的信心岂不更好,能自己写一个petite-vue再学习vue3岂不是事半功倍。说了这么多,今天就开始迈出第一步吧。注意,本文是学习petite-vue源码系列的第一篇文章,先打个广告,github项目地址,欢迎点个星星喔,现在进入正题吧。
petite-vue还算是比较新的一个框架,尤雨溪2021年6月30号才初始化项目,经过几天密集的代码提交后,有二十多天已经没有更新了,看得出已经比较稳定了,本文不打算详细介绍petite-vue是干嘛的,有啥优势,关于这些可以查看官方介绍,首先来看看怎么跑一个hello world吧。
<div v-scope>{{msg}}</div>
<script src="https://unpkg.com/petite-vue"></script>
<script>
PetiteVue.createApp({ msg: 'hello world!' }).mount()
</script>
如果你熟悉vue,那么对petite-vue的用法就很熟悉了,毕竟师出同门,当然还有一些个性化的语法,如上面的v-scope;对petite-vue有了简单的认识后,我们就模仿上面的示例,来实现一个看起来一样的代码吧,其中我们要实现如下几个关键部分:
PetiteVue
PetiteVue是一个全局对象,包含createApp这个重要的API,因此可以像下面这样声明:
const PetiteVue = {
createApp(scope) {
...
}
};
createApp
createApp是一个函数,入参可以接收一个表示组件数据值的对象,同时需要返回一个包含mount函数的对象,我们在上一步的基础上接着丰富createApp函数吧:
const PetiteVue = {
createApp(scope) {
const appContent = {
scope: scope,
};
const app = {
context: appContent,
mount() {
...
}
};
return app;
}
};
mount
mount根据字面意思,就是挂载我们的组件了,这里我们只是简单的将msg渲染到页面上,要实现这一目标,我们要遍历div的DOM结构,找到{{插值}}的地方,然后用scope的值去填充文本,说完了思路,接下来就实现吧,这里我们新增两个遍历DOM的函数walk和walkChildren:
function walk(node, context) {
const { nodeType } = node;
if (nodeType === 1) { // Element
return walkChildren(node, context);
}
if (nodeType === 3) { // Text
...
}
}
function walkChildren(node) {
let child = node.firstChild;
while(!child) {
walk(child);
child = child.nextSibling;
}
}
const PetiteVue = {
createApp(scope) {
const appContent = {
scope: scope,
};
const app = {
context: appContent,
mount() {
const root = document.querySelector('[v-scope]');
if (!root) {
console.warn('请提供有v-scope属性的html标签');
return;
}
walk(root, appContent);
root.removeAttribute('v-scope');
}
};
return app;
}
};
通过walk和walkChildren递归,可以遍历所有DOM节点,这里我们只关心Text节点,上面的代码还没实现具体逻辑,先不急,把架子搭起来,后面再实现。
v-scope
v-scope是标记根组件的自定义属性,petite-vue支持多个根组件节点,在本篇实现中就先实现一个吧,尽量保持简单些;通过document.querySelector获取到根节点引用,它就作为遍历DOM的起点,当然最后要把v-scope属性删除,上面的代码已经实现了,这里多废话几句。
{{}}
{{}}是我们自定义的插值语法,因此需要在walk遍历过程中去识别和解析出来,识别还是很简单的,就判断文本是不是{{xx}}格式的,通过一个简单的正则/{{([^]+?)}}/就可以判断,这里简单说一下正则表达式吧,[^]+?表示匹配任意字符,但是尽量少匹配,外面的括号是一个分组,会提取出{{}}里面的表达式,最后前后需要有{{}}包裹住,还是比较好理解的,现在动手实现具体的逻辑吧:
const RE = /{{([^]+?)}}/;
function walk(node, context) {
const { nodeType } = node;
if (nodeType === 1) { // Element
return walkChildren(node, context);
}
if (nodeType === 3) { // Text
const text = node.textContent;
const match = text.match(RE);
if (match) {
const exp = match[1].trim(); // 删除表达式前后的空白字符
node.textContent = context.scope[exp];
}
}
}
function walkChildren(node) {
let child = node.firstChild;
while(!child) {
walk(child);
child = child.nextSibling;
}
}
const PetiteVue = {
createApp(scope) {
const appContent = {
scope: scope,
};
const app = {
context: appContent,
mount() {
const root = document.querySelector('[v-scope]');
if (!root) {
console.warn('请提供有v-scope属性的html标签');
return;
}
walk(root, appContent);
root.removeAttribute('v-scope');
}
};
return app;
}
};
现在可以在浏览器里面跑起来了,看下效果吧,嗯,跟petite-vue的例子看起来差不多了,到这里我们就基本达成了最初的目标了,实现了一版很简陋的看起来差不多的框架。
继续完善
从实现来看当匹配到插值语法的时候,我们直接把文本节点的内容全部替换了,如果我们的文本是这样的格式呢:"this is content: {{msg}} is't over",那么最终渲染的还是只有msg的状态值,其他都丢失了,这样显得有点糟糕,我们就乘胜追击,再完善一下吧。首先分析一下为了实现文本完整的渲染,我们要将静态的文本和插值文本提取出来,然后再拼接起来才是最终符合预期的结果,从左到右依次解析文本,"this is content: {{msg}} is't over"需要分成三部分,分别是["this is content: ", "{{msg}}", " is't over"],msg经过转换后变成["this is content: ", "{hello world!", "is't over"],最后拼接起来回填到文本节点就可以了:
const RE = /{{([^]+?)}}/g;
function walk(node, context) {
const { nodeType } = node;
if (nodeType === 1) { // Element
return walkChildren(node, context);
}
if (nodeType === 3) { // Text
const text = node.textContent;
let i = 0; // 保存上一个匹配{{}}格式的字符结束索引
if (text.includes('{{')) { // 先判断是否有"{{"字符,有才进行下面的判断
let match = null;
const segments = []; // 保存所有截断的文本
while ((match = RE.exec(text))) {
segments.push(text.slice(i, match.index)); // {{之前的字符
i = match.index + match[0].length;
const exp = match[1].trim(); // 删除表达式前后的空白字符
segments.push(context.scope[exp]); // msg的值求得之后,放入数组中便于后面拼接
}
segments.push(text.slice(i)); // 最后一个}}后面的字符
node.textContent = segments.join('');
}
}
}
支持表达式
通过拼接字符串的方式我们完成了渲染的基本要求,但是熟悉vue语法的同学会说,双花括号内部是支持js表达式的,既然实现到这里了,我们就支持一下表达式吧,首先分析一下,表达式里面的标识符指向scope对象的属性值,一个还好说,那么两个怎么通过简单的方式去实现呢,挨个挨个去把标识符提取出来,然后计算再合并么,想想都麻烦,那有没有简单的方式呢,我都这么说了,当然是有的,先看下实现原理吧:
function createFunc(exp) {
return new Function(`scope`, `with(scope) { return (${exp}) }`)
}
const f = foo('a + b');
f({ a: 1, b: 2 });
通过createFunc创建一个新的函数,with将exp表达式的作用域限定在scope中,这样当执行a+b的时候,相当于scope.a + scope.b,最后将结果返回,最终执行的函数如下所示:
(function(scope) {
with(scope) {
return (a + b);
}
})({a: 1, b: 2})
知晓了原理之后,我们就补齐表达式的计算吧:
function createFunc(exp) {
return new Function(`scope`, `with(scope) { return (${exp}) }`);
}
...
function walk(node, context) {
const { nodeType } = node;
if (nodeType === 1) { // Element
return walkChildren(node, context);
}
if (nodeType === 3) { // Text
const text = node.textContent;
let i = 0;
if (text.includes('{{')) {
let match = null;
const segments = []; // 保存所有截断的文本
while ((match = RE.exec(text))) {
segments.push(text.slice(i, match.index));
i = match.index + match[0].length;
const exp = match[1].trim(); // 删除表达式前后的空白字符
segments.push(createFunc(exp)(context.scope)); // createFunc(exp)生成函数,再将scope传入执行
}
segments.push(text.slice(i));
node.textContent = segments.join('');
}
}
}
...
现在我们写的第一版框架就完成啦,完整的v1版本代码可点击这里,当然现在功能十分有限,没有其他指令集,没有响应式,不过作为学习petite-vue的第一步,已经迈出去啦,给自己一个赞吧,持之以恒,终会有收获的。这里预告一下第二篇的内容,我们将分析和实现响应式方面的内容。
福袋
vue3源码难学,先从petite-vue开始吧的更多相关文章
- 边看MHA源码边学Perl语言之一开篇
边看MHA源码边学Perl语言之一开篇 自我简介 先简单介绍一下自己,到目前为此我已经做了7年左右的JAVA和3年左右php开发与管理,做java时主要开发物流行业的相关软件,对台湾快递,国际快递,国 ...
- Typescript | Vue3源码系列
TypeScript 是开源的,TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript.编译出来的 JavaScript 可以运行在任何浏览器上.TypeS ...
- vue3源码node的问题
下载vue3源码后,下载依赖时,node的版本需要在10.0.0以上,并且不同的vue3里面的插件的配置对版本依赖还不同,14.0.0以上的版本基本都不支持win7了, win7系统可以安装12.0. ...
- 【译】Vue源码学习(一):Vue对象构造函数
本系列文章详细深入Vue.js的源代码,以此来说明JavaScript的基本概念,尝试将这些概念分解到JavaScript初学者可以理解的水平.有关本系列的一些后续的计划和轨迹的更多信息,请参阅此文章 ...
- Redux 源码解读 —— 从源码开始学 Redux
已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下:捡起旧知识的同时又有了一些新的收获,在这里作文以记之. 在阅读文章之前,最好已经知道如何使用 ...
- 在阅读sqlmap源码时学到的知识--检查运行环境
最近在读sqlmap的源码,懵懵懂懂中页大约学到了一些知识(说给自己听的话:由此可见,所谓的能够解决所有遇到问题的python水平,只能说明你遇见的都是简单的需求....),老规矩,在这里写一下,一则 ...
- Vue3源码分析之 Ref 与 ReactiveEffect
Vue3中的响应式实现原理 完整 js版本简易源码 在最底部 ref 与 reactive 是Vue3中的两个定义响应式对象的API,其中reactive是通过 Proxy 来实现的,它返回对象的响应 ...
- Vue3源码分析之Diff算法
Diff 算法源码(结合源码写的简易版本) 备注:文章后面有详细解析,先简单浏览一遍整体代码,更容易阅读 // Vue3 中的 diff 算法 // 模拟节点 const { oldVirtualDo ...
- Vue.js源码解析-从scripts脚本看vue构建
目录 1. scripts 脚本构建 1.1 dev 开发环境构建过程 1.1.1 配置文件代码 1.1.2 如何进行代码调试? 1.2 build 生产环境构建过程 1.2.1 scripts/bu ...
随机推荐
- 练习(time.tick定时器监控数据库)
设立个定时器,监控数据库里fb_state(发布状态),并将数据库中一条记录的开始时间与截止时间和当前时间进行比对.若当前时间在开始时间与结束时间之间,则将发布状态设为1,否则为0. 同时,将此段代码 ...
- SpringCloud(1)生态与简绍
一:微服务架构简绍学习目标 1.技术架构的演变,怎么一步步到微服务的:2.什么是微服务,优点与缺点 :3.SOA(面向服务)与MicroServices(微服务)的区别 :4.Dubbo 与Spri ...
- SpringBoot实现通用的接口参数校验
本文介绍基于Spring Boot和JDK8编写一个AOP,结合自定义注解实现通用的接口参数校验. 缘由 目前参数校验常用的方法是在实体类上添加注解,但对于不同的方法,所应用的校验规则也是不一样的,例 ...
- 【题解】[LuoguP3503]「BZOJ2086」[POI2010] Blocks
题目描述 给出N个正整数a[1..N],再给出一个正整数k,现在可以进行如下操作:每次选择一个大于k的正整数a[i],将a[i]减去1,选择a[i-1]或a[i+1]中的一个加上1.经过一定次数的操作 ...
- MySQL:一条更新语句是如何执行的
目录 引言 更新流程图 更新流程说明 第一步:更新数据 数据页内存 Change Buffer 第二步:缓存日志内容 redo log buffer binlog cache 第三步:日志写入磁盘 两 ...
- 项目启动报错:Redis health check failed
最近是重新开发整个项目,在上线测试的时候发现这个问题. 项目环境:SpringBoot2.x+Consul+Redission+Maven 报错的信息如下: o.s.b.a.redis.RedisHe ...
- 五、JavaSE语言基础之流程控制
流程控制的简单概念 流程控制:指通过关键字控制代码的执行流程; 一.顺序结构:执行顺序从左到右从上到下; 顺序结构无关键字,一般而言大多数代码都是顺序结构; 二.选择结构:根据条件判断是否执行 选择结 ...
- Kubernetes自动横向伸缩集群节点以及介绍PDB资源
在kubernetes中,有HPA在需要的时候创建更多的pod实例.但万一所有的节点都满了,放不下更多pod了,怎么办?显然这个问题并不局限于Autoscaler创建新pod实例的场景.即便是手动创建 ...
- Docker单机网络上
前言 Docker系列文章: 此篇是Docker系列的第六篇,大家一定要按照我做的Demo都手敲一遍,印象会更加深刻的,加油! 为什么要学习Docker Docker基本概念 Docker镜像基本原理 ...
- WEB与游戏开发的一些区别
WEB与游戏开发的一些区别 前言 最近由于在准备期末考,以及准备实习.其实都没好好写过博客,但今天由于个人身边的一些事,所以对做web和做游戏开发的区别做个记录,以下都是从网上搜索到的资料文章,感 ...