前言
- 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段】达到本文目标,若使诸君稍有启发,不枉此文心力^-^
- 文分【思路篇】和【实现篇】,本文是思路篇,建议看两个窗口同步阅读-》Vue-ast|实现篇第一版|实现render函数
目标
第一阶段:实现template写法兼容
先看使用
在Vue中 对于html模板存在如下三种写法
默认执行渲染逻辑:优先级 1. 找el属性 2. 先找render方法 3. 再找template
# html (找el)
<div id="app">
<div style="color:yellow">姓名 {{name}}</div> <span>111</span>
</div>
let vm = new Vue({
el: '#app',
data(){
return {
name: '王志远',
age: 23
}
}
})
// 或者 $mount
// 此方法是针对是否传el属性的情况 即可以实现控制vue挂载时机
let vm = new Vue({
data(){
return {
name: '王志远',
age: 23
}
}
})
vm.$mount('#app')
# template ==================================
let vm = new Vue({
el: '#app',
data(){
return {
name: '王志远',
age: 23
}
},
template: `<div id="app">
<div>姓名 {{name}}</div> <span>111</span>
</div>`
})
# render ==================================
let vm = new Vue({
el: '#app',
data(){
return {
name: '王志远',
age: 23
}
},
render(_c){
return _c('div',{
id: 'app'
},[_c('div',{},`姓名 ${this.name}`),_c('span',{},'111')])
}
})
核心问题
- 三种写法如何兼容
解决思路
兼容问题其实写法不同但目的相同,即通过html字符串生成ast树,也就是render的功能,也就意味着,我们的焦点在于将所有template写法转为render函数即可(注意优先级);
- 先判断用户有无定义render,如定义直接使用此函数
- 无定义则判断有无template,如果无则根据el获取html字符串赋值给template,根据template生成render函数
第二阶段:实现render函数
先看使用
return _c('div',[_c('div',{style: 'color:yellow'},`姓名 ${this.name}`),_c('span',{},'111')])
=>
<div><div style="color: yellow;">姓名 王志远</div><span>111</span></div>
核心问题
- 实现_c函数==功能是将用户传递信息转成vdom,即虚拟节点
- 实现_update方法==功能是通过vdom渲染页面
解决思路
- 定义vdom结构
{tag,data,key,children,text }及根据信息创建vdom的函数_c - 将_c传递给render,然后获取render函数返回值,是一个vdom
- 通过vdom渲染页面可以采用深度遍历+处理dom的方式
- 通过vnode生成对应html结构,注意处理属性
- 通过el找到挂载点,替换即可
此处是vue的domdiff核心,在此进行patch,即新老虚拟节点比对后再挂载,因篇幅问题,此处只实现第一次渲染,即根据vnode渲染真实dom逻辑,后期单独写文
待解决问题:将html转为render函数
思路:1. 将html用ast语法树去描述,这样js就可以处理了 2. 通过ast生成render函数 这样就实现了将不同的问题转为已处理过的问题
第三阶段:通过template获取信息
核心问题
- 如何获取template(html结构)字符串中html对应的信息
解决思路
-
正则提取有效信息,获取后对原html字符串进行截取然后继续循环处理,直到html为空
- 处理起始标签
<div id="11">- 处理标签名
- 处理属性
- 处理结尾标签
- 处理文本
- 处理起始标签
第四阶段:处理获取信息,生成ast树
先看使用
用户写的代码
<div id="app">
11
<div style="color:yellow">姓名 {{name}}</div>
</div>
我们需要转成的结构
{
parent: null
tag: "div"
type: 1,
attrs:[
{name: "id", value: "app"}
],
children: [{
text: "11"
type: 3
},{
tag: 'span',
parent: 父div,
attrs: [{name: "style",value: {color: "yellow"}}],
children: [{
type: 3, text: "姓名{{name}}"
}]
}]
}
核心问题
- 获取到信息采用什么数据结构去描述?
- 怎么校验html结构是否正确(即
<div></span></div>这种闭合错误的情况),即构建父子结构
解决思路
- 注意到这种场景很适合一种数据结构--树,即由一个根节点不断发散,且子规模(树叶结构)相同;我们就可以采用树形结构对获取的信息描述,即:根据模板生成ast树
- 定义树单元结构:标签对应
{ tag: tagName, //标签名type: 1, // 标签类型children:[], // 孩子列表 attrs, // 属性集合parent:null // 父级元素};文本对应{type:3,text:text} - 根据获取信息生成对应树单元
- 定义树单元结构:标签对应
- 采用栈结构记录处理标签过程,处理开始标签时将标签信息对象入栈,在处理闭合标签时
- 首先 对栈进行出栈操作,同时记录标签的父级信息对象(即出栈元素的parent属性指向栈顶元素)
- 其次 将出栈元素存入栈顶元素的children属性中,从而构建了父子结构
- 最后,处理结束时判断栈是否为空,不为空说明匹配异常
第五阶段:根据ast生成render函数,函数执行时会返回对应的vnode
先看使用
// ast 结构
{
parent: null
tag: "div"
type: 1,
attrs:[
{name: "id", value: "app"}
],
children: [{
text: "11"
type: 3
},{
tag: 'span',
parent: 父div,
attrs: [{name: "style",value: {color: "yellow"}}],
children: [{
type: 3, text: "姓名{{name}}"
}]
}]
}
// 需要被转成
render(){
return _c('div',{id:'app'},_v('11'),_c('span',{style:"color:yellow"},"姓名"+_s(name)))
}
其中,_v用于生成文本虚拟节点 _c 用于生成标签节点 _s用于将对象转为字符串形式,避免字符串转函数时会将之误认为是变量从而报错
核心问题
- ast树结构如何转函数?
- 内部变量应该在vm上取值,如何做到?
解决思路
- ast通过深度遍历然后字符串拼接可以实现转字符串,然后通过new Function可以实现字符串转函数
- 通过with可以实现指定函数内的全局变量
// with 实例
let obj = {
a: 1
}
with(obj){
console.log(a);
}
最后
在用户未传render时,将此render挂载上去,复用之前渲染(patch)逻辑
至此,ast完成(源码可见实现篇文末仓库链接)