vlambda博客
学习文章列表

petite-vue:【源码解析】回归原生,无虚拟DOM的极简体验

调研原因

近期,petite-vue以极简、去虚拟DOM的特点令大家眼前一亮。大家都以为它是不是尤大大推出vue3mini版(可能看到名字会被诱导),但其实它的存在,也是有它一个独特的意义。它也许会引领我们关于原生dom渲染和借助虚拟dom的思考。而且最近随着SolidJSSvelte两个框架的大火,它们都是编译时通过将状态更新编译为独立的DOM操作方法,省去了虚拟DOM比较这一步所消耗的时间。无论在编译-运行时-响应原理相对于“御三家”都有了不一样的体验,所以更值得我们去探究一下。


截止发文前,petite-vuestar数已经3.8k,尤大大的魅力可不一般哦

初体验

你可以通过script的形式直接引入到html文件中【建议配上init和defer】

<script init defer src="https://unpkg.com/petite-vue">

</script>


【defer】

不去阻塞dom的渲染和其他资源加载,同时保证dom解析完成后执行

【init】

自动初始化petite-vue,可以直接在全局中获取PetiteVue全局变量


【API使用】(以官网例子为准)

<div v-scope="{ str: 'Hello World' }">

{{ str }}

</div>

通过v-scope标识出需要解析的区域,并且通过指定str作为一个变量的形式,再通过经典的vue插槽表达式形式,去展示渲染,就可以完成初体验了。

目前,petite-vue是基于vite和vue3的新玩法进行项目的书写。
生命周期:目前仅支持两个【mounted和unmounted】
模板:模板组件$template和非模板组件,灵活书写小组件
指令和响应式:你可以灵活去使用vue3中部分的响应式API和指令
缺陷:目前还是存在多个特性不能完全兼容vue3,包括computed等

兼容与不兼容的特性

这个地方参考了【掘金读者:陌小路】进行补充和总结

兼容特性:

{{ }} 插值表达式

v-bind

(including : shorthand and class/style special handling)

v-on (including @ shorthand and all modifiers)

v-model (all input types + non-string :value bindings)

v-if / v-else / v-else-if

v-for

v-show

v-html

v-text

v-pre

v-once

v-cloak

reactive()

nextTick()


不兼容的特性:

ref()、computed()等

template仅支持选择器

不支持render function,因为petite-vue没有虚拟DOM

v-on="object"

v-is & <component :is="xxx">

v-bind:style自动添加前缀不支持

Transition, KeepAlive, Teleport, Suspense

v-for 深层解构

不支持的响应式类型(Set、Map等)

petite-vue项目启动

第一步:项目拉取

git clone https://github.com/vuejs/petite-vue.git


第二步:安装依赖

yarn


第三步:启动项目

npm run dev


第四步:访问页面

http://localhost:3000/

以上步骤进入完成之后,当你访问3000端口,就会显示很多vue基本api
这个时候,你肯定很想打debug调试,然后就立刻点击了对应源码的小红点或者直接键入debugger,发现没有反应。肯定啦,你当前就压根还没调用到功能API和服务,那我们应该如何调试呢?

debugger调试petite-vue

第一步:项目启动

yarn run dev


第二步:访问页面

http://localhost:3000/


第三步:根据你想要了解的vue-API特性选择一个点击a标签link跳转


第四步:观测主入口src/index.ts

重点关注createApp().mount()


然后可以结合debugger一边看源码一边分析

源码解读

createApp

首先来到src/index.ts,我们可以看到这么一段代码

if ((s = document.currentScript) && s.hasAttribute('init')) {

createApp().mount()

}

这里大家会注意到document.currentScript这个属性
document.currentScript属性返回当前正在运行的脚本所属的<script>元素这里是通过创建s变量记录当前运行的脚本元素,如果存在制定属性init,那么就调用createApp和mount方法。 

然后我们看看createApp做了什么操作,来到src/app.ts中,可以看到createApp是接收一个可选的initialData初始化数据 

一、创建根ctx上下文【赋予了很多属性和方法】

const ctx = createContext()


二、利用reactive将初始化数据代理成响应式

if (initialData) {

ctx.scope = reactive(initialData)

}


三、创建全局helpers,绑定在scope中

ctx.scope.$s = toDisplayString

ctx.scope.$nextTick = nextTick

ctx.scope.$refs = Object.create(null)


四、最终返回一个对象

对象中包含三个方法:directive、mount、unmount

directive、mount、unmount

【directive】

注册自定义指令。实际上是向ctx的dirs添加一个属性,当调用applyDirective时,就可以得到对应的处理函数。

【mount】

处理el参数,找到应用挂载的根DOM节点,然后执行初始化流程

具体过程:

1)首先判断传入的el是不是string类型,如果是直接通过document.querySelector去取;否则,就会取document.documentElement

2)初始化roots【节点数组】

let roots: Element[]

3)判断是否存在v-scope,有就将其放入root数组;否则就要去这个el下面找到所以的带v-scope属性的节点,然后筛选出这些带v-scope属性下面的不带v-scope属性的节点,塞入roots数组

4)判断这时roots数组依然为空,如果是就将el放入roots

(这时如果在开发环境甚至就会出现警告)

5)然后基于root进行map对于roots中每个el进行Block实例化

rootBlocks = roots.map((el) => new Block(el, ctx, true))

这里的Block是个重点概念,它的作用主要是用于统一DOM节点渲染、插入、移除和销毁等操作。

6)最后再进行对元素包含'v-cloak'属性的进行移除该属性,然后返回this实例本身

【unmount】

销毁的过程,基于每个block实例化调用teardown方法进行销毁

teardown() {

this.ctx.blocks.forEach((child) => {

child.teardown()

})

this.ctx.effects.forEach(stop)

this.ctx.cleanups.forEach((fn) => fn())

}

从这里开始,我们就要重点关注Block的处理。那我们去src/block.ts中进行分析

Block

(注意:Block是一个类)

初始化constructor

主要接收三个元素elctxisRoot

- 第一步:初始化template

首先会判断isRoot是否是根元素

如果是就是直接取el传入的元素作为template

否则就会基于template调用cloneNode方法进行获取作为template

(注意:为什么这里会使用cloneNode这个方法,是因为queryselector之后我们拿到的是一个obj


- 第二步:初始化ctx

如果是根节点,就直接用传入的ctx;如果不是根节点,就递归的继承ctx


- 第三步:调用walk方法构建应用

walk(this.template, this.ctx)


另外,Block还包含了其他的方法

insert新增,主要负责对节点的插入控制

remove移除,主要负责对节点的移除处理

teardown销毁,主要负责对整体的销毁清空

Block初始化的过程主要是处理template和ctx,最终传递给walk进行构建,那这个时候,我们就去重点观察walk的工作,那我们就要来到src/walk.ts中去分析

walk

walk主要接收两个参数node: Node, ctx: Context


- 第一步:首先拿到节点的nodeType

const type = node.nodeType


- 第二步:根据nodeType进行不同的处理,这里主要分为了三种

第一种:nodeType === 1 代表元素

1)先去判断是否具有v-if、v-for、v-scope、v-once、ref属性,分别对应有不同的内置方法进行处理

2)walkChildren(el, ctx)先处理子节点,在处理节点自身的属性

walkChildren其实就是获取首个字节点,不断遍历去递归字节点去调用walk方法对于子节点进行处理

3)最后处理节点属性相关的指令,包括内置指令和自定义指定


第二种:nodeType === 3 元素或者属性中的文本内容

1)先拿到node的data

2)通过匹配include方法,然后正则匹配需要替换的文本内容

3)applyDirective(node, text, segments.join('+'), ctx)

(通过调用该方法最终返回一个文本字符串)


第三种:nodeType === 11 轻量级的document对象

直接调用walkChildren进行遍历递归子节点进行walk方法调用

nodeType

此属性只读且传回一个数值。

有效的数值符合以下的型别:

1-ELEMENT

2-ATTRIBUTE

3-TEXT

4-CDATA

5-ENTITY REFERENCE

6-ENTITY

7-PI (processing instruction)

8-COMMENT

9-DOCUMENT

10-DOCUMENT TYPE

11-DOCUMENT FRAGMENT

12-NOTATION

总结

以上就是整体从创建-解析-渲染的整个流程,通过源码分析,可以更加明确得看出来,petite-vue没有依赖虚拟dom,没有虚拟DOM,就无需通过template构建render函数进行渲染,而是直接递归遍历DOM节点,借助正则去匹配hasAttribute或者checkAttr进行解析各种指令。并基于@vue/reactivity,完美继承和使用vue3的响应式API特性进行操作和使用,但也离不开通过ctx.effect()收集依赖。个人觉得,petite-vue还是维持着革新的亮点,从体积和复古原生dom的形式就可以映射出来。其无需考虑虚拟DOM跨平台的功能,在源码中直接使用浏览器相关API操作DOM,减少了框架runtime运行时的成本。(开箱即用、对于Jquery这类型的项目,可以直接迁移转换到petite-vue也是非常好的替代哦)


前端良文
关注我,定期分享前端开发中的干货知识。每一篇文章都让你有所收获。
13篇原创内容
Official Account