今天在复盘的时候,发现上一节还是有些仓促,本节开始稍微细致一些。
然后发现上一节漏掉了一个attrParser()
方法,它的作用主要是将收集到的字符串属性转换成Map
结构,常规的JSON
格式也是可以的,保证属性名称不重复,后声明的覆盖先声明的即可。
那么我们今天就先从attrParser()
开始吧。
一、补充attrParser方法
首先还是让我们先看看使用场景,比如我们可以按如下方法声明属性:
- style=“color: pink;”
- class=“todo todo1 todo2”
- x-for=“item in list” 循环方式 后期会用到
- :todo=“todo” 向下传递props 后期会用到
- x-if=“true”
- x-show=“true”
- key=“key”
然后呢,我会可能会得到一个组合后的属性字符串,如:
const attrStr = 'style="color: pink;" class="todo todo1 todo2" x-for="item in list" :todo="todo" x-if="true" key="key"'
接下来我们开始拆解字符串,大家应该也可以注意到,style
和class
属性中均存在空格,所以单纯的通过空格或者引号split
是行不通的。
使用双指针+词法分析可以很方便的将属性截取出来,此例我们偷个懒,还是上正则:
const attrReg = /(:?x?-?\w+)=?\s?(\"(.*?)\")?/g
使用正则的方式可以一次性将属性分割开来,然后基于等号split
即可拿到所有属性,下面让我们用上边的attrStr
来测试下效果
const attrStr = 'style="color: pink;" class="todo todo1 todo2" x-for="item in list" :todo="todo" x-if="true" key="key"'
const attrReg = /(:?x?-?\w+)=?\s?(\"(.*?)\")?/g
console.log(attrStr.match(attrReg))
// 操作后,我们将得到数据
// ['style="color: pink;"', 'class="todo todo1 todo2"', 'x-for="item in list"', ':todo="todo"', 'x-if="true"', 'key="key"']
通过上边测试,我们发现基础的用例是能跑通的,那剩下的问题就简单了,直接编码吧
// shared/index.ts
export const attrParser = (attrStr: string) => {
if (!attrStr) return attrStr
let ret: Map<string, any> = new Map()
attrStr = attrStr.trim()
// 上边介绍过的分割方法
let attrs = attrStr.match(attrReg)
if (attrs && attrs.length) {
const quot = /["']/g
for (const iterator of attrs) {
let attr = iterator.split('=')
// 将value多余的引号去掉
// 将没有value值的属性的value置为true
ret.set(attr[0], attr[1] ? attr[1].replace(quot, '') : true)
}
}
return ret
}
通过该方法,我们就得到了一个存放属性的Map
结构,它看起来像下图一样:
到此,我们完成了属性的基础切割。接下来让我们用已经介绍过的内容,生成带层级的虚拟dom结构。
二、虚拟dom结构
进行到此,我们可能需要引入虚拟dom的概念了,但是上一期已经说好这期生成目标结构,所以虚拟dom相关的具体内容我们放到后边的章节来详细说明,本期先弱化虚拟dom概念,暂时还是称作目标结构
吧。
还是一样,我们上来先思考我们需要的目标结构应该长什么样子的,或者说我们如何用最少的属性描述一个dom节点。
用最少的属性描述,是因为后期虚拟dom会常驻内存,较为精简的结构能节省内存,也可以提升
diff
的效率。
通过参考(抄袭)其他作者的结构,我们暂定我们的目标结构
为:
interface VNode = {
tag: string // 存放标签名称
data: any // 用来存放标签的所有属性
children: Array<VNode>
elm: Node // 后期用来存放真实dom
text: string // 若标签只有一个文本节点,则存放在此
key: string | symbol | undefined
}
OK,有了目标结构,那我们还需要一个(不是)创建目标结构的方法,该方法主要负责精简结构,返回VNode
// virtual-dom/src/vnode.ts
export function vnode(
tag: string | undefined,
data: VNodeData | undefined,
children: Array<VNode | string> | undefined,
elm: Node | undefined,
text: string | undefined
): VNode {
let key = data === undefined ? undefined : data.key
let ret: Record<string, any> = {
tag,
data,
children,
elm,
text,
key,
}
// 精简结构,去除空属性
for (let k in ret) {
if (ret.hasOwnProperty(k)) {
if (ret[k] === undefined) Reflect.deleteProperty(ret, k)
}
}
return ret
}
至此,准备工作已经做完,让我们开始生成目标结构吧
三、生成目标结构
首先呢我们先引出来一个概念,分析有开启有关闭的结构时(xml、html等), 如果需要生成带层级的嵌套结构,一般都是通过词法分析+栈
实现的,实际上我们通过上一节,已经完成了词法分析的步骤,接下来,我们一起看看如何通过栈来收集结构。
3.1 栈的操作逻辑
3.1 代码实现
逻辑理清之后,编码就比较简单了,篇幅有限,我们本期先实现一个简单版的,下一期引入h
函数,也就是react中的React.createElement
// x-template/src/createVNode.ts
/**
* 根据模版字符串,创建虚拟节点
* @param htmlStr 模版字符串
* @param data 数据
*/
export const createVNode = (htmlStr: string, data: any) => {
// 使用栈收集标签层级
// 我们先在其中加入一个跟节点作为所有节点的父级,收集完成后,其下的children就是结果
let stack: Array<VNode> = [vnode('root', undefined, [], undefined, undefined)]
parser(htmlStr, {
onStartTag(name: string, attrs: Map<string, any>) {
let parent = stack[stack.length - 1]
let _vnode = vnode(
name,
getTagData(attrs), // 获取tagdata的结构 可以先忽略
undefined,
undefined,
undefined
)
parent.children = parent.children ? parent.children : []
// 添加子集
parent.children.push(_vnode)
// 进栈
stack.push(_vnode)
},
onText(text: string) {
let parent = stack[stack.length - 1]
parent.children = parent.children ? parent.children : []
// 将文本标签作为当前标签的子集
parent.children.push(
vnode(undefined, undefined, undefined, undefined, text)
)
},
onEndTag(name: string) {
let parent = stack[stack.length - 1]
if (parent.tag === name) {
// 若标签名一致,则出栈
stack.pop()
} else {
throw new Error('not found close tag: ' + parent.tag)
}
},
})
let vnodes = stack[0].children
// 拿到收集后的虚拟节点
console.log(vnodes)
return vnodes
}
本期完!
下期预告,下期我们引入h
函数的概念,方便我们后期通过ts
或者babel
解析jsx
生成虚拟dom使用。
然后若时间充裕,再加上简单的数据绑定、指令解析(x-for、x-if、x-show)等内容。
循序渐进,不忘初心,我们下期见~
有问题请留言 或发邮件: liujax@126.com