如今这个世道,作为一个有几年工作经验的前端,不学点框架源码都感觉要被抛弃了,react或vue要能吹吹牛吧,最好能造个轮子,听说vue3源码好学点,那么学学vue3,但是学起来还是那么费劲,感觉快放弃了,就在这个时候出现了petite-vue,害,这家伙比vue简单啊,拿它来重拾学习源码的信心岂不更好,能自己写一个petite-vue再学习vue3岂不是事半功倍。说了这么多,今天就开始迈出第一步吧。注意,本文是学习petite-vue源码系列的第一篇文章,先打个广告,github项目地址,欢迎点个星星喔,现在进入正题吧。
petite-vue还算是比较新的一个框架,尤雨溪2021年6月30号才初始化项目,经过几天密集的代码提交后,有二十多天已经没有更新了,看得出已经比较稳定了,本文不打算详细介绍petite-vue是干嘛的,有啥优势,关于这些可以查看官方介绍,首先来看看怎么跑一个hello world吧。
如果你熟悉vue,那么对petite-vue的用法就很熟悉了,毕竟师出同门,当然还有一些个性化的语法,如上面的v-scope;对petite-vue有了简单的认识后,我们就模仿上面的示例,来实现一个看起来一样的代码吧,其中我们要实现如下几个关键部分:
PetiteVuePetiteVue是一个全局对象,包含createApp这个重要的API,因此可以像下面这样声明:
const PetiteVue = { createApp(scope) { ... } }; createAppcreateApp是一个函数,入参可以接收一个表示组件数据值的对象,同时需要返回一个包含mount函数的对象,我们在上一步的基础上接着丰富createApp函数吧:
const PetiteVue = { createApp(scope) { const appContent = { scope: scope, }; const app = { context: appContent, mount() { ... } }; return app; } }; mountmount根据字面意思,就是挂载我们的组件了,这里我们只是简单的将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-scopev-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的例子看起来差不多了,到这里我们就基本达成了最初的目标了,实现了一版很简陋的看起来差不多的框架。
继续完善