wwwzne-ui
v1.0.7
Published
一个类似`VUE3`的`UI`框架,`api`相似,内部实现存在差异,一定程度上是`mini vue`+`web component`的组合。
Readme
wwwzen-ui
一个类似VUE3的UI框架,api相似,内部实现存在差异,一定程度上是mini vue+web component的组合。
| 目录 | | :-------------------------------------------------- | | 第一部分:基础 | | 分节1:前置知识 | | —— 1.1:最长上升子序列算法 | | —— 1.1.1:暴力版本 | | —— 1.1.2:优化版本 | | —— 1.1.3:最优版本 | | ——1.2:前端语言基础 | | ——1.3:编译原理 | | 分节2:数据响应式 | | —— 2.1:基础结构 | | —— 2.2:响应式代理 | | —— 2.3:虚拟dom | | ———— 2.3.1:虚拟dom的类型 | | ———— 2.3.2:虚拟dom的渲染过程 | | ———— 2.3.3:广义diff算法 | | ———— 2.3.4:核心diff算法 | | ———— 2.3.5:组件类型节点处理 | | —— 2.4:抽象语法树 | | ———— 2.4.1:astnode的类型 | | ———— 2.4.2:上下文 | | —— 2.5:模板编译 | | —— 2.6:指令 | | —— 2.6:响应式api | | 分节3:页面响应式 | | 第二部分:UI组件 | | 分节1:图标组件 | | 分节2:灯箱组件 | | 分节3:弹性组件 | | 分节4:网格组件 | | 分节2:按钮组件 | | 分节3:链接组件 | | 分节4:分割线组件 | | 分节4:markdown解析器 | | 分节2:数据响应式 | | 分节3:页面响应式 | | 第三部分:业务组件 | | 分节1:前端路由控制 | | 分节2:数据响应式 | | 分节3:页面响应式 | | 第四部分:简化操作 | | 章节3:简化设置 |
前置知识
最长上升子序列算法
leetcode链接:最长递增子序列
暴力版本
循环数组里的元素计算所有以其开头的递增的序列取最大值即为最长子序列,采用dfs递归思路编写。
function lengthOfLIS(nums: number[]): number {
const dfs = (i: number, last: number): number => {
if (i === nums.length) return 0;
let len = dfs(i + 1, last);
if (nums[i] > last) len = Math.max(len, dfs(i + 1, nums[i]) + 1);
return len;
}
return dfs(0, -Infinity);
}优化版本
记忆化搜索
对于暴力搜索情况下,存在重复计算,如[0,0,1,2],前两个元素一致那么dfs(2, 0)就计算了两次,提前去重也无法解决,反例可为[0,1,0,1,2],去重后dfs(2, 0)也计算了两次。那么考虑记录dp[(i,last)]。
function lengthOfLIS(nums: number[]): number {
const dp: { [key: number]: number }[] = new Array(nums.length)
.fill(0)
.map(() => ({}))
const dfs = (i: number, last: number): number => {
if (i === nums.length) return 0;
if (dp[i][last]) return dp[i][last];
let len = dfs(i + 1, last);
if (nums[i] > last) len = Math.max(len, dfs(i + 1, nums[i]) + 1);
dp[i][last] = len;
return len;
}
return dfs(0, -Infinity);
}动态规划
转移方程为nums[i]>nums[j],dp[i]=max(dp[i],dp[j] + 1)。
const lengthOfLIS = (nums: number[]): number => {
const dp = new Array(nums.length).fill(1);
for (let i = 0; i < nums.length; i++) {
for (let j = i - 1; j >= 0; j--) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
}贪心算法
维护一个上升序列数组
const lengthOfLIS = (nums: number[]): number => {
const num: number[] = [nums[0]];
for (let i = 1; i < nums.length; i++) {
if (nums[i] > num[num.length - 1]) {
num.push(nums[i]);
} else {
for (let j = 0; j < num.length; j++) {
if (num[j] >= nums[i]) {
num[j] = nums[i];
break;
}
}
}
}
return num.length;
}最优版本
二分优化贪心算法
维护上升序列数组时,二分查找插入位置
const LIS = (nums: Array<any>): any[] => {
const result = [];
const position = [];
for (let i = 0; i < nums.length; i++) {
if (nums[i] === -1) continue;
if (nums[i]! > result[result.length - 1]!) {
result.push(nums[i]);
position.push(result.length - 1);
} else {
let l = 0, r = result.length - 1;
while (l <= r) {
const mid = ~~((l + r) / 2);
if (nums[i]! > result[mid]!) {
l = mid + 1;
} else if (nums[i]! < result[mid]!) {
r = mid - 1;
} else {
l = mid;
break;
}
}
result[l] = nums[i];
position.push(l);
}
}
let cur = result.length - 1;
for (let i = position.length - 1; i >= 0 && cur >= 0; i--) {
if (position[i] === cur) result[cur--] = i;
}
return result;
}前端语言基础
- HTML
- CSS
- javascript
- typescript
- php
- svg
- markdown
编译原理
数据响应式
数据响应式:数据对象是可观察的,当数据发生变化时,会触发依赖它的视图或逻辑更新。
基础结构
类似vue: runtime(运行时)、compiler(编译模板)、reactive(响应式)模块
<div id="app">
<div>{{counter.num}}</div>
<button @click="add">click</div>
</div>
<script>
const { reactive, createApp } = Vue;
createApp({
setup(){
const counter = reactive({num: 0});
const add = ()=> {
counter.num++;
};
return {
counter,
add
}
}
}).mount("#app")
</script>响应式代理
1.对象代理(reactive),通过Proxy创建代理实现,其中get拦截执行完track(追踪)函数再执行获取对象属性,同样要设置只执行track一次的相关逻辑,set则拦截执行trigger(触发)函数引发依赖函数的自动执行,track与trigger其实就是维护一个WeakMap<any, Map<string | symbol, Set<() => any>>>,它是依赖存储的核心数据结构,track实现自动依赖收集收集effect注册的影响函数,trigger则实现依赖函数的自动执行。
2.非对象代理(ref),通过构建RefImpl类转成对象再Proxy创建代理实现,之后的过程reactive一致。
3.依赖收集(track),也就是track函数其具体流程为
4.依赖执行(trigger),也就是trigger函数其具体流程为
5.效应函数(effect),也称为影响函数,副作用函数
6.特例处理,嵌套情况reactive(reactive(obj))不应该多次代理,重复情况let a=reactive(obj),b=reactive(obj)不应该多次代理,发生变更时hasChanged才触发副作用函数,数组代理存在length属性也要特殊处理,嵌套effect情况逻辑处理。
7.计算属性(computed),
graph LR
A(普通对象) --reactive--> B(响应式对象)
C(track函数)<--get-->D(reactive函数)<--set-->E(trigger函数)虚拟dom
VNode,本质就是一个js对象,里面有规定的属性。一般有五个属性type、props、children、shapeFlag、el、anchor、key,特殊的el就是挂载的具体dom节点元素,当挂载并创建真实dom过程结束后将其保存在el属性里面,其未挂载时为undefined。
虚拟dom的类型
虚拟dom分为四个类型,Element、Text、Fragment、Component。ShapeFlags用于辅助类型判断。
Element
普通元素节点,document.createElement创建。type指标签名,props指元素属性,children指子元素。
{
type: string,
props: Object,
children: string | VNode[]
}Text
文本节点,document.createTextNode创建。type固定为Symbol('Text'),props为空,children为文本内容字符串。
{
type: Symbol,
props: null,
children: string
}Fragment
无渲染过程的容器节点,相当于template。type固定为Symbol('Fragment'),props为空,children为子节点数组,意为其子节点渲染完成时挂载到Fragment的父节点上。
{
type: Symbol,
props: null,
children: []
}Component
组件节点,执行内部的渲染方法,产出上面三种虚拟DOM的集合。组件的type是定义组件的对象,props是外部传入的属性数据,children为null实际也可为undefined。
{
type: Object,
props: Object,
children: null
}
{
type: {
template: `{{msg}}{{name}}`,
props: ['name'],
setup(){
return {
msg: 'hello'
}
}
}
props: { name: 'world' }
}ShapeFlags是一组标记,用于快速辨别出VNode的类型和其children的类型,依赖位运算。四个类型ELEMENT、TEXT、FRAGMENT、COMPONENT。ELEMENT为常见HTML标签对应的虚拟DOM类型,TEXT为文本标签对应的虚拟DOM类型,FRAGMENT为无渲染过程的容器VNode节点类型,COMPONENT为组件VNode节点类型。三个辅助判断类型TEXT_CHILDREN、ARRAY_CHILDREN、CHILDREN,用于区分子节点类型。TEXT_CHILDREN表明子结点为文本VNode节点,ARRAY_CHILDREN表明子结点为VNode节点混合数组,CHILDREN表明存在子节点。
enum ShapeFlags {
ELEMENT = 1,// 00000001
TEXT = 1 << 1,// 00000010
FRAGMENT = 1 << 2,// 00000100
COMPONENT = 1 << 3,// 00001000
TEXT_CHILDREN = 1 << 4,// 00010000
ARRAY_CHILDREN = 1 << 5,// 00100000
CHILDREN = (1 << 4) || (1 << 5)// 00110000
}h函数
h(),别名createVNode(),是创建VNode的函数,接收三个参数,type,props,children,返回VNode。VNode为{ type,props,children,shapeflag,el,anchor,key,component},创建的VNode的el、anchor、component为null,key为props.key,所以可能为undefine。
虚拟dom的渲染过程
render函数
render(),渲染函数,将虚拟DOM转为真实DOM。使用时传入两个参数要挂载的虚拟DOM,以及挂载的位置。挂载位置具体是一个真实DOM,虚拟DOM就挂载上此真实DOM,其可能隐含记录属性,是之前挂载时存储下来的_vnode,如果第一个参数虚拟DOM为null,则表明真实DOM更新后为空,那就要按照_vnode进行unmount操作,进入unmount函数,如果第一个参数虚拟DOM非null,即可进行patch操作,进入patch函数。最后更新_vnode为传入虚拟DOM。
graph TD
A(n1:prevVNode<br/>n2:nextVNode)-->B(render)-->C{n2是否存在}
C --否--> D(unmount) --> E{是否为组件}
E --是--> F(unmountComponent)
E --否--> G{是否为Fragment}
G --是--> H(unmountFragment)
G --否--> I[对Text,Element<br/>执行removeChild]
C --是--> J(patch)-->K{n1与n2<br/>类型是否一致}
K --否--> L[n1执行unmount]
L --n1--> D
L --n2--> M{是否为Component}
K --是--> M
M --是--> N(processComponent)
M --否--> O{是否为Text}
O --是--> P(processText)
P --> Q{n1是否存在}
Q --是--> R[更新el.textContent]
Q --否--> S(mountTextNode)
O --否--> T{是否为Fragment}
T --是--> U(processFragment)
U --> V{n1是否存在}
V --否--> W(mountChildren)
V --是--> X(patchChildren)
T --否--> Y(processElement)--> Z{n1是否存在}
Z --否--> a(mountElement)
Z --是--> b(patchElement)-->c(patchProps)-->X
X --递归--> bpatch函数
patch(),本质是diff过程+mount过程入口。接收四个参数,n1、n2、container、anchor,n1为原始VNode,n2为新的VNode,container为父容器真实DOM,anchor为添加锚点,其非必须参数。其操作比较两个虚拟DOM的差异,方便改变的内容挂载更新。对比过程:n1,n2为新旧节点,首先对比类型(注意并非对比标记),如果不一致则先unmount(n1)同时更新anchor为n1的anchor或n1的下相邻真实DOM元素,之后按照n2的类型判断分流执行processElement、processText、processFragment、processComponent。
processElement函数
processElement(),过程函数,接收四个参数,n1、n2、anchor,n1为原始VNode,n2为新的VNode,anchor为添加锚点,其非必须参数。如果n1不存在,也无需对比直接挂载n2即可,进入mountElement函数,n1存在,则进入patchElement函数。
patchElement函数
patchElement(),对比函数,接收三个参数,n1、n2、container、anchor,n1为原始VNode,n2为新的VNode,container为父容器真实DOM,anchor为添加锚点,其非必须参数。因为之前对比类型时不一致就卸载n1,那么执行到函数时他们类型一定一致,存在属性复用n2.el=n1.el,执行patchProps与patchChildren。
patchProps函数
patchProps(),对比函数,接收三个参数oldP,newP,el,oldP是旧属性集合,newP是新属性集合,el为被作用DOM元素。循环处理即可传入patchDomProp来更新节点DOM属性。
patchDomProp函数
patchDomProp(),对比更新DOM属性函数。其接收四个参数prev、next、key、el,prev为原始值,next为新值,key为属性键,el为被作用DOM元素。内部使用switch判断区分不同键执行不同更新操作。
patchChildren函数
patchChildren(),对比孩子节点函数。其接收四个参数n1、n2、container、anchor,n1为旧VNode节点,n2为新VNode节点,container为父容器真实DOM,anchor为添加锚点,其非必须参数。其过程会对比n1的子节点与n2的子节点,n1与n2都有三种类型ARRAY_CHILDREN、TEXT_CHILDREN、NULL,所以会产生九种情况。情况一n1与n2均为TEXT_CHILDREN,直接更新el的textContent即可,情况二n1为ARRAY_CHILDREN,n2为TEXT_CHILDREN,那要执行nmountChildren(c1)更新el的textContent即可,情况三n1为NULL,n2为TEXT_CHILDREN,直接更新el的textContent即可。上面三种情况相对简单,情况四n1为TEXT_CHILDREN,n2为ARRAY_CHILDREN,先令el的textContent为空字符串,再进入mountChildren挂载即可。情况五n1为ARRAY_CHILDREN,n2为ARRAY_CHILDREN,进入核心diff算法,精细更新操作。情况六n1为NULL,n2为ARRAY_CHILDREN,直接mountChildren挂载即可。最后三种情况,n2都为null,n1为ARRAY_CHILDREN要执行卸载unmountChildren,n1为TEXT_CHILDREN直接将el的textContent改为空字符串即可,n1为null则不操作,
mountChildren函数
mountChildren(),挂载子节点函数。其接收三个参数children、container、anchor,children为待挂载的VNode节点数组,contrainer为父组件容器,anchor为添加锚点,其非必须参数。循环操作执行path过程,为递归操作。
unmountChildren函数
unmountChildren(),卸载子节点函数。其接收一个参数children,执行到此函数children一定为待卸载的VNode节点数组,循环执行unmount操作。
processText函数
processText(),过程函数。其接收四个参数n1、n2、container、anchor,n1为VNode节点,n2为文本VNode节点,container为父组件容器真实DOM,anchor为添加锚点,其非必须参数。如果n1为null,直接挂载n2即可。如果n1非null,直接节点复用更新textContent。
mountTextNode函数
mountTextNode(),挂载文本节点函数。其接收四个参数n1,n2,container,anchor,n1为VNode节点,n2为VNode_TEXT节点,container为父组件容器真实DOM,anchor为添加锚点,其非必须参数。通过insertBefore挂载节点。
processFragment函数
processFragment(),挂载容器节点函数。其接收四个参数n1,n2,container,anchor,n1为VNode节点,n2为VNode_FRAGMENT节点,container为父组件容器真实DOM,anchor为添加锚点,其非必须参数。如果n1存在递归执行patchChildren过程,n1为null增加首尾anchor,进行mountChildren过程。
processComponent函数
unmount函数
卸载函数,一个参数虚拟DOM,如果虚拟DOM为null,意思是需要卸载但没有东西可以卸载无需任何操作,如果虚拟DOM非null,则分流执行unmountComponent、unmountFragment、removeChild,Element和Text都会进入removeChild分支。
unmountComponent函数
广义diff算法
patchUnkeyedChildren函数
核心diff算法
核心diff算法指的就是对两个存在key的arrayChildren做diff操作。
patchkeyedChildren函数
patchkeyedChildren(),
组件类型节点处理
实现组件的挂载与响应式更新,setup定义了组件内部属性,render定义了组件ui渲染结构,内部本质就是那render的返回值进行patch操作实现挂载。基础结构如下。
const Comp = {
setup() {
const count = ref(0);
const add = () => {
count.value += 3;
count.value += 3;
console.log(count.value);
}
return {
count,
add,
}
},
render(ctx) {
return [
h('div', null, ctx.count.value),
h(
'button', {
onClick: ctx.add,
},
'add',
)
]
}
}
const vnode = h(Comp);
render(vnode, document.body);processComponent
processComponent(),执行组件patch过程函数,其接收四个参数n1,n2,container,anchor,n1为VNode节点,n2为VNode_TEXT节点,container为父组件容器真实DOM,anchor为添加锚点,其非必须参数。如果n1为null那直接进行n2的挂载mountComponent,如果n1非null则进行updateComponent操作。
mountComponent
normalizeVNode
updateComponent
抽象语法树
抽象语法树,Abstract Syntax Tree,简称 AST)是一种用于表示源代码结构的树状表达方式,其由模板代码(也称源代码)经过parse解析成一种树型结构。模板代码本质就是一段字符串,parse就是初步分析,ast是一种辅助分析的中间产物。
astnode的类型
<div id="foo" v-if="ok">hello {{name}}</div> enum ASTNodeTypes {
ROOT = 'ROOT',
ELEMENT = 'ELEMENT',
TEXT = 'TEXT',
EXPRESSION = 'EXPRESSION',
INTERPOLATION = 'INTERPOLATION',
ATTRIBUTE = 'ATTRIBUTE',
DIRECTIVE = 'DIRECTIVE'
}ROOT
根节点,在最外层包裹整个节点树。
type AST_ROOT = {
type: ASTNodeTypes.ROOT,
children: AST_CHILDREN
}ELEMENT
普通元素节点
type AST_ELEMENT = {
type: ASTNodeTypes.ELEMENT,
tag: string,
tagType: 'ELEMENT' | 'COMPONENT',
props: AST_ATTRIBUTE[],
directives: AST_DIRECTIVE[],
isSelfClosing: boolean,
children: AST_CHILDREN
}Text
文本节点
type AST_TEXT = {
type: ASTNodeTypes.TEXT,
content: string
}EXPRESSION
表达式节点
type AST_EXPRESSION = {
type: ASTNodeTypes.EXPRESSION,
content: string,
isStatic: boolean
}ATTRIBUTE
属性节点
type AST_ATTRIBUTE = {
type: ASTNodeTypes.ATTRIBUTE,
name: string,
value: undefined | AST_TEXT
}INTERPOLATION
插值节点
type AST_INTERPOLATION = {
type: ASTNodeTypes.INTERPOLATION,
content: AST_EXPRESSION
}DIRECTIVE
指令节点
type AST_DIRECTIVE = {
type: ASTNodeTypes.DIRECTIVE,
name: string,
exp: undefined | AST_EXPRESSION,
arg: undefined | AST_EXPRESSION
}上下文
context对象
context用于在函数调用中传递处理进程,option用于操作可选配置,delimiters为插值符号,isVoidTag为自闭合标签,isNativeTag为非自闭合标签,source存储当前操作进度。
type context = {
option: {
delimiters: [string, string],
isVoidTag: (i: string) => boolean,
isNativeTag: (i: string) => boolean,
},
source: string
}createParseContext函数
解析过程
parse函数
advanceBy函数
advanceSpaces函数
adbanceSpaces函数,用于去除空格字符。
isEnd函数
parseChildren函数
模板编译
graph LR
A(模板代码) -->|parse| B(AST)
B-->|transform|C(codegenNode)
C-->|codegen|D(渲染函数代码)模板代码产生的ast经过进一步分析(transform)产生codegenNode。codegenNode是生成目标渲染函数的中间结构。codegen(即code generate)过程,遍历codegenNode,递归生成最终的渲染函数代码。
traversNode
traverseNode函数,遍历AST语法树,存在四个分支遍历到root节点的情况则递归,将孩子节点传入traverseNode函数;遍历到interpolation节点的情况进入createTextVnode函数,遍历到text节点的情况存在复用同样传入createTextVnode函数;遍历到element节点的情况;表达式节点则依附在其他节点内部不在此函数里处理,属性节点与指令节点会在元素节点内,所以也不再此函数中处理。
createTextVnode
createTextVnode函数,生成text或者interpolation节点的渲染函数片段。这里{{a}}a会被渲染为[h(Text_,null,a),h(Text_,null,"a")],一个带引号,一个不带引号存在代码复用,额外设置一个函数createText函数处理两种情况,返回第三部分合并再返回即可。
createText
指令
本框架支持的指令有v-on、v-bind、v-html、v-if、v-for,其中v-on,v-bind、v-html,本质是属性,在编译过程中会是一个单独的过程从头到尾处理,特殊的v-html、v-model与v-if实现较为复杂,但他们仅仅存在于元素节点中它们的处理逻辑也存放在resolveElementASTNode过程中。
pluck
pluck函数,
响应式api
const {
createApp,
render,
h,
Text_,
Fragment_,
renderList,
resolveComponent,
nextTick,
reactive,
effect,
ref,
computed,
compile,
config,
} = window.wwwzne;前端路由控制
markdown解析器
简要原理
graph TB
subgraph A[预处理:标准化]
direction LR
G[统一换行符]
H[转义字符]
end
subgraph B[词法分析:划分token]
direction LR
标题token --- 段落token --- 引用token --- 列表token --- 代码token --- 表格token
end
subgraph C[语法分析]
direction LR
AST根 --- AST标题 --- AST段落 --- AST代码
end
subgraph D[渲染输出]
direction LR
end
markdown片段 --> A -->|标准md文本| B -->|token流| C -->|AST| D --> F[html片段]用法
页面响应式
图标组件
<wz-icon type="" view></wz-icon>图标组件内置一些小型SVG图标,type可为wz-icon.typeList列表的内容或者路径,view存在设置与不设置两种状态对应开启点击预览功能,测试文件为./test/wz-icon.html。
灯箱组件
<wz-lightbox src="" open></wz-lightbox>灯箱组件用于图片放大预览,src可为图片路径,open设置与不设置两种状态对应显示与不显示,测试文件为./test/wz-lightbox.html。
弹性组件
<wz-flex type="" jc="" ai=""></wz-flex>
<wz-f type="" jc="" ai=""></wz-f>本质就是div的flex布局容器,type为row|col|row-|col-,jc为justify-content值为start|center|end|left|right|space-around|space-between|space-evenly默认为center,ai为align-items值为start|center|end默认为center,测试文件为./test/wz-flex.html。
网格组件
<wz-grid col="" row="" gap=""></wz-grid>按钮组件
<wz-button type=""></wz-button>
<wz-bt type=""></wz-bt>链接组件
分割线组件
<wz-hr position="" type="" size="" color="">wwwzne</wz-hr>- position: left | center | right
- type: solid | dashed | dotted | double
- size: 尺寸字符串
- color: 颜色字符串
二维码渲染器组件
轮播图组件
测试
单元测试
- reactive: /test/reactive.test.ts
- ref
集成测试
简化设置
config对象
| 函数 | 说明 | 参数 | 返回值 | | :------------- | :-------------- | :------------------------------ | :--------- | | initDefault | 设置默认配置 | 无参数 | config对象 | | setTitle | 设置页面标题 | 无参数 | config对象 | | setTheme | 设置风格字段 | 一个参数(字符串) | config对象 | | getTheme | 获取风格字段 | 无参数 | config对象 | | htmlLang | 设置页面语言 | 一个参数(字符串默认cmn-hans) | config对象 | | setMeta | 设置页面元数据 | (对象字面量或字面量数组) | config对象 | | setLogo | 设置页面图标 | 一个参数(路径或svg字符串) | config对象 | | setDirection | 设置文本方向 | 一个参数(字符串ltr与rtl默认ltr) | config对象 | | addCssFile | 追加CSS样式文件 | 一个参数(文件路径字符串) | config对象 | | setBackground | 设置背景图片 | 一个参数(文件路径字符串) | config对象 | | setDefaultFont | 设置默认字体 | | |
