Vue源码学习(四):<templete>渲染第三步,将ast语法树转换为渲染函数

好家伙,

Vue源码学习(三):<templete>渲染第二步,创建ast语法树,

在上一篇,我们已经成功将

我们的模板

转换为ast语法树

接下来我们继续进行操作

1.方法封装

由于代码太多,为了增加代码的可阅读性

我们先将代码进行封装

index.js

复制代码
import { generate } from "./generate"
import { parseHTML } from "./parseAst"

export function compileToFunction(el) {

    //1. 将html元素变为ast语法树
    let ast = parseHTML(el)
    //2. ast语法树变成render函数
    //(1) ast语法树变成字符串
    //(2) 字符串变成函数
    let code = generate(ast) // _c _v _s
    console.log(code)
    //3.将render字符串变成函数
    let render = new Function(`with(this){return ${code}}`)
    console.log(render,'this is render')
    return render
}

今天我们将注意力集中在generate方法,

ast参数长什么样子?

2.渲染函数generate()作用

首先我们确认,这个generare()方法是干什么的?

将ast语法树变成一个渲染函数

于是,我们来思考几个问题--

问一:为什么要使用generare()方法将ast语法树转换为渲染函数?

答一:在上图我们看见了,这个所谓的语法树其实更像是一个json而不是一个js,

而我们要将模板转换为可执行的JavaScript代码,才能跑

答一(官方一点的版本):

首先,将AST转换为渲染函数可以消除模板的解析和编译的开销。

在Vue的运行时版本中,没有编译器,所以将模板转换为渲染函数能够提高运行时的性能。

其次,渲染函数可以更高效地处理动态渲染。由于AST在编译时已经进行了一些静态分析,因此在渲染函数中可以更好地优化动态渲染和响应式更新的逻辑,减少运行时的开销。

最后,渲染函数的生成也是为了更好地支持vue组件的复用。

通过将模板转换为渲染函数,Vue可以更方便地缓存和复用这些函数,进一步提高组件的渲染性能。

总而言之,使用generate()方法将AST转换为渲染函数是为了提高Vue应用的性能和效率,优化动态渲染和支持组件的复用。

3.generate()方法得到结果

我们想象一下

复制代码
let code = generate(ast)

code会是什么样子的?

如果我们去翻源码,大概可以看到这步的结果长这样

然而实际上,我们的四步走: 模板解析 =》AST =》生成渲染函数 =》渲染到真实DOM

渲染函数的下一步是渲染到真实DOM

也就是要预留一个标记给"渲染到真实DOM"这一步做处理

_c 标签

_v 文本

_s 符号

4.generate.js代码解析

3.1.generate() 函数是入口函数,接受一个 AST 语法书作为参数:

复制代码
export function generate(el) {
    console.log(el,'|this is el')
    let children = genChildren(el)
    console.log(children, "|this is children")
    let code = `_c('${el.tag}',${el.attrs.length?`${genPorps(el.attrs)}`:'undefined'},${
        children?`${children}`:''
    })`
    console.log(code, '|this is code')
    return code
}
  • (1) 调用 genChildren() 函数获取子节点代码字符串。
  • (2) 拼接元素节点的标签名( genPoros()方法 )、属性和子节点代码,并返回生成的渲染函数代码。

generate.js完整代码

复制代码
/**
 * <div id="app">Hello{{msg}}</div>
 * 
 * _c 解析标签
 * _v 解析字符串
 * 
 * render(){
 *  return _c('div',{id:app},_v('hello'+_s(msg)),_c)
 * }
 *  
 */
//处理属性
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

//genPorps()方法解析属性
function genPorps(attrs) {
    // console.log(attrs)
    let str = '';
    //对象
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i]
        if (attr.name === 'style') { // 
            let obj = {}
            attr.value.split(';').forEach(item => {
                let [key, val] = item.split(':')
                // console.log(key, val, "//this is [key,val]")
                obj[key] = val
            })
            attr.value = obj
        }
        //拼接
        str += `${attr.name}:${JSON.stringify(attr.value)},`
        // console.log(str, '|this is str')
        // console.log(`{${str.slice(0,-1)}}`)
    }
    //首字符到倒数第二个字符,即去掉标点符号
    return `{${str.slice(0,-1)}}`
}

//处理子节点
function genChildren(el) {
    let children = el.children //获取元素节点的子节点
    //如果存在子节点,则递归调用 gen() 函数处理每个子节点,并用逗号拼接子节点的代码。
    if (children) {
        //返回子节点代码的字符串。
        return children.map(child => gen(child)).join(',')
    }
}
//
function gen(node) { //1.元素  2.div  tip:_v表示文本
    // console.log(node, "this is node")
    //如果节点是元素节点,递归调用 generate() 函数处理该节点,并返回结果。
    if (node.type === 1) {
        return generate(node)
    } else { //文本 
        //(1) 只是文本 hello  (2){{}}
        let text = node.text //获取文本
        //转化
        if (!defaultTagRE.test(text)) {
            return `_v(${JSON.stringify(text)})`
        }
        //(2)带插值表达式{{}}
        //文本包含插值表达式,使用正则表达式 defaultTagRE 
        //查找所有 {{}} 形式的插值表达式,并解析成可执行的代码片段。
        let tokens = []
        //lastIndex 需要清零 否则test匹配会失败
        let lastindex = defaultTagRE.lastIndex = 0
        //match保存获取结果
        let match
        while (match = defaultTagRE.exec(text)) {
            console.log(match, "|this is match")
            let index = match.index
            if (index > lastindex) {
                tokens.push(JSON.stringify(text.slice(lastindex, index))) //内容
            }
            tokens.push(`_s(${match[1].trim()})`)
            //lastindex处理文本长度
            lastindex = index + match[0].length
        }
        //此处if用于处理`Hello{{msg}} xxx`中的xxx
        if (lastindex < text.slice(lastindex)) {
            tokens.push(JSON.stringify(text.slice(lastindex, index))) //内容
        }
        return `_v(${tokens.join('+')})`
    }
}

export function generate(el) {
    console.log(el,'|this is el')
    let children = genChildren(el)
    console.log(children, "|this is children")
    let code = `_c('${el.tag}',${el.attrs.length?`${genPorps(el.attrs)}`:'undefined'},${
        children?`${children}`:''
    })`
    console.log(code, '|this is code')
    return code
}

(代码注释已十分完善)

方法解释(简化版本):

3.2.genChildren() 函数 处理子节点

  • 获取元素节点的子节点。
  • 如果存在子节点,则递归调用 gen() 函数处理每个子节点,并用逗号拼接子节点的代码。
  • 返回子节点代码的字符串。

3.3. gen() 函数 根据节点类型生成代码:

  • 如果节点是元素节点,递归调用 generate() 函数处理该节点,并返回结果。
  • 如果节点是文本节点:返回处理后的节点代码。
    • 如果文本不包含插值表达式 {{}},则使用 _v() 方法将文本转换为可执行的字符串形式。
    • 如果文本包含插值表达式,使用正则表达式 defaultTagRE 查找所有 {{}} 形式的插值表达式,并解析成可执行的代码片段。
  • 返回处理后的节点代码。

3.4. genProps() 函数 解析属性:

  • 遍历元素节点的所有属性,拼接属性名和属性值。
  • 如果属性名是 "style",则将属性值解析为对象形式。
  • 返回拼接后的属性字符串。

5.render字符串变成函数

上述操作结束后,我们得到还是字符串,现在我们将其变成一个函数

复制代码
let render = new Function(`with(this){return ${code}}`)

这里为什么要用with(this) ??

答:而with(this) 是将当前组件实例(即Vue组件的this)作为上下文绑定到函数中,这样在渲染函数中就可以访问组件实例的属性和方法,例如访问组件的数据、计算属性或方法。

调试部分以及最终结果输出