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

vue,源码,学习,templete,渲染,第三步,ast,语法,转换,函数 · 浏览次数 : 179

小编点评

**方法解释:** 1. **`genChildren()`** 函数递归处理子节点获取所有元素节点的子节点,并拼接子节点代码的字符串。 2. **`gen()`** 函数根据节点类型生成代码: - 如果节点是元素节点,递归调用 **`generate()`** 函数处理该节点。 - 如果节点是文本节点,返回处理后的代码。 3. **`genProps()`** 函数解析属性,遍历元素节点的所有属性,拼接属性名和属性值。如果属性名是 `style`,则将属性值解析为对象形式。 4. **`render()`** 函数创建一个新函数并赋值其代码,然后执行该函数并返回结果。 **方法注释:** - `with(this)` 方法用于将当前组件实例(即 Vue 组件的 `this`)作为上下文绑定到函数中。 - `code` 是在 `render()` 方法中生成代码的字符串。 - `return` 语句用于将字符串转换为函数并赋值给 `render` 变量。 **调试部分:** - 代码中没有提供调试部分,无法查看代码执行过程。 **最终结果输出:** 代码会创建一个新的函数,该函数将字符串变成一个可执行的函数,并返回该函数。

正文

好家伙,

 

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

 

 

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

 

 

 

 

与Vue源码学习(四):渲染第三步,将ast语法树转换为渲染函数相似的内容:

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

好家伙, Vue源码学习(三):渲染第二步,创建ast语法树, 在上一篇,我们已经成功将 我们的模板 转换为ast语法树 接下来我们继续进行操作 1.方法封装 由于代码太多,为了增加代码的可阅读性 我们先将代码进行封装 index.js import { generate } f

Vue源码学习(五):渲染第四步,生成虚拟dom并将其转换为真实dom

好家伙, 前情提要: 在上一篇我们已经成功将ast语法树转换为渲染函数 现在我们继续 1.项目目录 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我解释给你听一万遍 新增文件:vnode/index.js vnode/p

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

好家伙,书接上回 在上一篇Vue源码学习(二):渲染第一步,模板解析中,我们完成了模板解析 现在我们继续,将模板解析的转换为ast语法树 1.前情提要 代码已开源https://github.com/Fattiger4399/analytic-vue.git手动调试一遍, 胜过我

Vue源码学习(二):渲染第一步,模板解析

好家伙, 1.去哪了 在正式内容之前,我们来思考一个问题, 当我们使用vue开发页面时,中的内容是如何变成我们网页中的内容的? 它会经历四步: 解析模板:Vue会解析中的内容,识别出其中的指令、插值表达式({{}}),以及其他元素和属性。

Vue源码学习(七):合并生命周期(混入Vue.Mixin)

好家伙, 1.使用场景 现在来,来想一下,作为一个使用Vue的开发者,假设现在我们要使用created(),我们会如何使用 1.1. .vue文件中使用 {{ message }}

Vue源码学习(十一):计算属性computed初步学习

好家伙, 1.Computed实现原理 if (opts.computed) { initComputed(vm,opts.computed); } function initComputed(vm, computed) { // 存放计算属性的watcher const watchers = vm

Vue源码学习(一):数据劫持(对象类型)

好家伙,了解一下Vue如何实现数据劫持 1.Vue中data的使用 首先,我得搞清楚这玩意的概念,我们先从vue的使用开始吧 想想看,我们平时是如何使用vue的data部分的? 无非是这两种情况 (你可千万不要带着惊讶的表情说"啊!原来有两种写法的吗") //函数写法 data() { return

Vue源码学习(六):(支线)渲染函数中with(),call()的使用以及一些思考

好家伙, 昨天,在学习vue源码的过程中,看到了这个玩意 嘶,看不太懂,研究一下 1.上下文 这段出现vue模板编译的虚拟node部分 export function renderMixin(Vue) { Vue.prototype._c = function () { //创建标签 return

Vue源码学习(十):关于dep和watcher使用的一些思考

好家伙, 前面想了好久,都没想明白为什么要dep和watcher打配合才能实现数据-视图同步 为什么要多一个依赖管理这样的东西 给每个数据绑个watcher(xxfunction),然后,数据变了,调set,然后调xxfunction,不就行了, 然后今天突然想明白了,不是为什么要这么干,而是必须这

Vue源码学习(九):响应式前置:实现对象的依赖收集(dep和watcher)

好家伙,这是目前为止最绕的一章,也是十分抽象的一章 由于实在太过抽象,我只能用一个不那么抽象的实例去说服我自己 完整代码已开源https://github.com/Fattiger4399/analytic-vue.git 1.我们要做什么? 来看这个例子, index.html setTimeou