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


    好家伙,

     

    Vue源码学习(三):渲染第二步,创建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完整代码

    复制代码
    /**
     * 
    Hello{{msg}}
    * * _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)作为上下文绑定到函数中,这样在渲染函数中就可以访问组件实例的属性和方法,例如访问组件的数据、计算属性或方法

     

     

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

     

     

     

     

  • 相关阅读:
    [每周一更]-(第63期):Linux-nsenter命令使用说明
    resultMap结果映射
    优思学院|六西格玛黑带的个人成功特质
    Spring中Bean的作用域
    TSINGSEE视频汇聚管理与AI算法视频质量检测方案
    Opengl之帧缓冲
    Android垂直跑马灯
    (Linux学习七)进程介绍
    无胁科技-TVD每日漏洞情报-2022-11-23
    【路径规划-TSP问题】基于遗传算法求解多起点多TSP问题附matlab代码
  • 原文地址:https://www.cnblogs.com/FatTiger4399/p/17696212.html