手写vue(三)模板渲染解析

news2024/11/17 21:50:09

一、目标

        创建一个Vue实例时,我们可以传入el配置项,去指定一个DOM元素作为Vue容器,而这个Vue容器中,可以使用例如插值表达式等Vue框架提供的语法,并且能够渲染到浏览器页面上。

        而浏览器并不能解析这些Vue语法,因此,Vue框架是通过获取到Vue容器,然后对容器内容进行解析,重新生成DOM元素,去替换掉容器内容。

二、执行流程

        Vue是如何重新解析,并且生成新的DOM对象的呢?

1、拿到模板字符串,然后解析为语法树,语法树节点描述了这个DOM的父节点、属性、子节点、

节点类型等。

{
        type,
        tag,
        attrs,
        parent,
        children: []
}

2、使用语法树生成虚拟DOM,虚拟DOM节点与语法树节点内容类似,不过虚拟DOM上面我们可以去挂一些自定义的属性,方便生成真实dom等等,而语法树节点,则是直接描述HTML内容,因此解析语法树,用第三方提供的包,也是可以完成的。

3、通过虚拟DOM生成真实DOM,然后替换Vue容器

三、模板解析

我们拿到HTML字符串后,可以通过正则去解析HTML内容,主要是要解析出标签、标签属性、文本内容。

解析HTML入口方法:parseHTML()

通过循环匹配,不断地按顺序解析HTML

例如:

下面这段html代码的解析流程:

初始状态

<div id="app">test<span></span></div>

树节点栈:【】

1、匹配到开始标签,生成一个树节点,类型为节点元素,设置tag和属性,然后截去解析完的内容,并把生成的节点放入栈中:

test<span></span></div>

树节点栈:【node{tag:'div', {id:'app'}, children: [], type: 'element'}】

2、匹配到文本内容,生成一个树节点,类型为文本元素,放入到栈顶元素的children数组中

<span></span></div>

树节点栈:【node{tag:'div', {id:'app'}, children: [node(text:'test', type: 'text')], type: 'element'}】

3、匹配到span的开始标签,生成节点,放入到栈顶元素的children数组中,并将自身放入栈中

</span></div>

树节点栈:【node{tag:'div', {id:'app'}, children: [node(text:'test', type: 'text'), node(span...)], type: 'element'}, node(span.....)】

4、匹配到span结束标签,栈中弹出span

</div>

树节点栈:【node{tag:'div', {id:'app'}, children: [node(text:'test', type: 'text'), node(span...)], type: 'element'}】

5、匹配到div结束标签,栈中弹出div

''

树节点栈:【】

最后得到的节点则是整颗语法树的根节点,顺着children往下找,可以生成整颗语法树

// Non-Colonized Name,xml元素和属性的名称
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配开始标签, 分组1为标签名
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 标签属性,分组1:key,value: 分组3/分组4/分组5
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 闭合标签
const startTagClose = /^\s*(\/?)>/;

export const AstNodeType = {
    ELEMENT_NODE: 1,
    TEXT_NODE: 3
}

/**
 * 创建AST语法树节点
 */
function createASTElement(tag, attrs, parent) {
    return {
        type: AstNodeType.ELEMENT_NODE,
        tag,
        attrs,
        parent,
        children: []
    }
}

/**
 * 解析HTML字符串为抽象语法树
 * @param {String} html Html字符串
 * @returns 抽象语法树
 */
export function parseHTML(html) {
    // 使用栈存储结构,逐层解析HTML
    const stack = []
    // 栈顶指针
    let topNode = null
    // 树根
    let root = null

    // 处理开始标签
    function start(tag, attrs) {
        const node = createASTElement(tag, attrs, topNode)
        if (topNode) {
            topNode.children.push(node)
        } else {
            root = node
        }
        if (tag != 'br') {
            stack.push(node)
            topNode = node
        }
    }

    // 处理文本内容
    function content(text) {
        topNode.children.push({
            type: AstNodeType.TEXT_NODE,
            text,
            parent: topNode
        })
    }

    // 处理结束标签
    function end(tag) {
        if (topNode.tag === tag) {
            stack.pop()
            topNode = stack[stack.length - 1]
        } else {
            topNode.children.push(createASTElement(tag, null, topNode))
        }
    }

    // html解析前进,截掉已解析内容
    function advance(length) {
        html = html.substring(length)
    }

    // 尝试解析开始标签,返回一个标签节点,包含标签名、属性等信息
    function parseStartTag() {
        let startTag = null
        const match = html.match(startTagOpen)
        if (match) {
            startTag = {
                tag: match[1],
                attrs: {}
            }
            advance(match[0].length)

            while (true) {
                const attrMatch = html.match(attribute)
                if (attrMatch) {
                    // 获取正则匹配到的分组值
                    startTag.attrs[[attrMatch[1]]] = attrMatch[3] || attrMatch[4] || attrMatch[5]
                    advance(attrMatch[0].length)
                } else {
                    break
                }
            }
            let end = html.match(startTagClose)
            advance(end[0].length)
        }
        return startTag
    }

    // 解析完成的html代码片段会被截掉,一直循环到html所有内容都解析完毕
    while (html) {
        // 匹配到首个<,可能时开始标签,可能是结束标签,文本内容是不会包含的,需要被转义
        const endIdx = html.indexOf('<')
        // 如果当前已经解析到标签
        if (endIdx === 0) {
            // 尝试作为开始标签去解析
            const startTag = parseStartTag()
            if (startTag) {
                start(startTag.tag, startTag.attrs)
                // 如果是开始标签,则continue, 因为开始标签在parseStartTag中已经被截掉了
                continue
            }
            // 如果走到这,证明是结束标签,则处理结束标签
            const endTagMatch = html.match(endTag)
            if (endTagMatch) {
                end(endTagMatch[1])
                advance(endTagMatch[0].length)
            }
        } else {
            // 处理文本标签
            const text = html.substring(0, endIdx).trim()
            if (text) {
                content(text)
            }
            advance(endIdx)
        }
    }
    return root
}

四、生成渲染函数代码

渲染函数可以生成虚拟dom,渲染函数的格式:h(【tag标签名】,【属性节点】,【子节点】,【子节点】....... )

可以发现,上一步生成的抽象语法树,包含了渲染函数所需要的全部信息,而我们生成渲染函数代码,可以避免每次更新页面时,需要重新解析一遍html。

渲染函数示例:

模板:

<div id="app">test{{value}}<span></span></div>

解析为语法树后,根据语法树,生成的渲染函数代码为:

_c('div', {attrs:{id: 'app'}}, _v('test' + _s(value)), _c('span'))

// _c 创建虚拟dom节点

// _v 创建文本虚拟dom节点

// _s 包裹插值语法内容,将插值表达式的值转化为字符串

此时,如果实现了_c、_v、_s, 并且将这个表达式放入vm上下文中运行(_s中的value,是直接取值的,不是一个字符串),就可以获得一个虚拟节点。

生成表达式:

import { AstNodeType } from "../parser";

// 匹配 插值语法
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
/**
 * 
 * @param {Object} ast ast语法树
 * 
 * @returns 渲染函数字符串
 * 例如_c('div', {id: 'app'}, _v('age:' + _s(age)), _c(......))
 * _c 创建虚拟dom节点
 * _v 创建文本虚拟dom节点
 * _s 包裹插值语法内容,将插值表达式的值转化为字符串
 */
export function generate(ast) {
    if (!ast) {
        return null;
    }
    // 生成子节点
    const childrenCodeList = []
    if (ast.children) {
        ast.children.forEach((child) => {
            childrenCodeList.push(generate(child))
        })
    }
    const childrenCode = childrenCodeList.join(',') || null
    let code = ""
    // 根据节点类型,生成对应虚拟DOM
    switch (ast.type) {
        case AstNodeType.ELEMENT_NODE:
            code = `_c('${ast.tag}',${JSON.stringify(ast.attrs)}, ${childrenCode})`
            break
        case AstNodeType.TEXT_NODE:
            let text = ast.text
            let match
            let tokens = []

            // 匹配插值表达式,用_s包裹起来
            while (match = defaultTagRE.exec(text)) {
                if (match.index !== 0) {
                    const t = text.substring(0, match.index)
                    tokens.push(`'${t}'`)
                }
                text = text.substring(match.index + match[0].length)
                tokens.push(`_s(${match[1].trim()})`)
            }
            if (text) {
                tokens.push(`'${text}'`)
            }
            // 拼接文本块
            code = `_v(${tokens.join('+')})`
        default:
            break;
    }
    return code
}

 将字符串表达式生成渲染函数:

import { generate } from "./codegen";
import { parseHTML } from "./parser";

/**
 * 解析模板,生成渲染函数
 * @param {String} template 模板
 * @returns 渲染函数
 */
export function compileToFunctions(template) {
    // 1、解析模板,生成抽象语法树
    const ast = parseHTML(template)
    // 2、通过抽象语法树,生成渲染函数表达式,表达式执行的值为虚拟dom
    const exp = generate(ast)
    // 3、添加绑定作用域,添加返回语句,最后生成函数
    // (调用这个函数的时候,需要使用call(this), 表达式中的响应式数据可以直接取到,不用通过this.XXX)
    const code = `with(this){return ${exp}}`
    return new Function(code)
}

五、执行渲染函数

执行渲染函数,需要实现c\v\s方法, 并挂到vue原型对象中去

$option._render 就是刚刚生成的渲染函数

    // 生成虚拟DOM
    Vue.prototype._render = function () {
        return this.$options.render.call(this)
    }
    /**
     * 给定标签名称、属性、子节点,生成虚拟DOM
     * @param {String} tag 标签名
     * @param {Object} attrs 属性集合
     * @param  {Array} children 子节点
     * @returns 虚拟dom
     */
    Vue.prototype._c = function (tag, attrs, ...children) {
        return new VNode(tag, { attrs }, children, undefined, undefined, this)
    }
    // 生成文本虚拟节点
    Vue.prototype._v = function (text) {
        return new VNode(undefined, undefined, undefined, text)
    }
    /**
     * 将插值表达式的值转化为字符串
     * @param {any} value 任意值
     * @returns 字符串
     */
    Vue.prototype._s = function (value) {
        let result = null
        if (typeof value === 'object') {
            result = JSON.stringify(value)
        } else {
            result = value
        }
        return result
    }

/***   vnode.js  *****/

// 虚拟节点定义
export class VNode {
    tag
    data
    children
    text
    elm
    context
    constructor(tag, data, children, text, elm, context) {
        this.tag = tag
        this.data = data
        this.children = children
        this.text = text
        this.elm = elm
        this.context = context
    }
}

六、生成真实DOM

根据虚拟DOM生成真实节点

/**
 * 根据虚拟节点创建真实节点
 * @param {vnode} vnode 虚拟节点
 * @returns 真实节点
 */
export function createElm(vnode) {
    const { children, data: { attrs } = {}, tag, text } = vnode || {}
    let elm = null
    if (tag) {
        elm = document.createElement(tag)
        Object.keys(attrs).forEach(key => {
            elm.setAttribute(key, attrs[key])
        })
        children && children.forEach(child => {
            elm.appendChild(createElm(child))
        })
    } else {
        elm = document.createTextNode(text)
    }
    return elm
}

gitee提交:

登录 - Gitee.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/438139.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu20.04软件安装大全

目录 Ubuntu20.04 软件安装大全前言1. Windows和Ubuntu双系统安装1.1 下载Ubuntu系统镜像1.2 磁盘分区1.3 GPT分区安装Ubuntu1.4 系统完成后的一些设置1.5 遇到的一些小bug 2. 换源2.1 apt换源2.2 pip换源 3. 显卡驱动安装3.1 卸载显卡驱动3.2 准备工作3.3 驱动安装3.4 验证 4.…

分享5款小软件,让你打造更舒适的办公电脑

每次发现实用的小工具,都会有种小小的成就感&#xff0c;这也是我喜欢收集和分享高效工具的原因。 图标定制软件——CustomizerGod CustomizerGod是一款强大的电脑图标定制软件&#xff0c;可以让你随心所欲地改变系统中的任何图标。你可以使用CustomizerGod来修改桌面、任务…

Node 01-Buffer

Buffer&#xff08;缓冲器&#xff09; 概念 Buffer 是一个类似于数组的 对象 &#xff0c;用于表示固定长度的字节序列 Buffer 本质是一段内存空间&#xff0c;专门用来处理 二进制数据 。 特点 Buffer 大小固定且无法调整Buffer 性能较好&#xff0c;可以直接对计算机内存…

AI智能课程第一讲:chatgpt介绍

AI应用现状 用AI艺术创作 一个小女孩打折手电筒在侏罗世纪公园找恐龙。 AI用于医疗行业 AI辅助驾驶 AI广告投放上的应用 什么是chatgpt&#xff1f; chatgpt相关技术的发展 为什么用chatgpt写代码会特别的快呢&#xff1f; 因为它集成了GitHub上所有开发者的库公用资源&…

战胜儿童乙肝,早治是关键

在我国实施新生儿乙型肝炎疫苗免疫规划后&#xff0c;母婴传播量明显减少。2014年&#xff0c;中国疾病预防控制中心对全国1-29岁人群乙型肝炎血清流行病学调查结果显示&#xff0c;1-4岁人群HBSAg流行率仅为0.32%&#xff0c;较1992年下降96.7%。然而&#xff0c;仍有6%的新生…

X509证书中的Signature Algorithm

Signature Algorithm在X509中的结构 分别在整个证书结构中Certificate的第二项和 TBSCertificate结构中的第三项&#xff1a; 对于ECC和国密算法 The AlgorithmIdentifier parameters field MUST be absent. 各种签名算法的OID: ECC的: ecdsa-with-SHA256 OBJECT IDENTIFI…

计网第五章.运输层—TCP的三次握手与四次挥手

以下来自湖科大计算机网络公开课笔记及个人所搜集资料 目录 一、TCP三次握手建立连接为什么TCP客户进程最后还要发送一个普通的TCP确认报文段呢&#xff1f;能不能两次握手&#xff1f;总结&#xff1a; 二、TCP四次挥手释放连接四次挥手过程问题1&#xff1a;TCP客户进程在发送…

【推荐】1657- 灵活可扩展,2023年值得尝试的13款富文本编辑器

作为前端开发人员&#xff0c;我们经常需要为网站和应用程序添加文本内容。与传统的文本编辑器不同&#xff0c;富文本编辑器可让您轻松创建各种类型的文本内容&#xff0c;包括加粗字体、斜体字、框架、列表、图片和视频等。 本文我将向大家推荐 13 款开源的灵活可拓展的富文本…

HTML5的新特性,CSS3的新特性

1.HTML5的新特性 HTML5的新特性主要针对以前的不足&#xff0c;增加了一些新的标签&#xff0c;新的表单&#xff0c;新的表单属性等。 这些新特性都有兼容性问题&#xff0c;基本是IE9以上版本的浏览器才支持&#xff0c;如果不考虑兼容性问题&#xff0c;可以大量使用这些新…

Docker之Dockerfile

Dockerfile 1. Dockerfile 简介2. Dockerfile 构建过程3. Dockerfile 常用指令4. 实战测试 centos5. 实战测试 Tomcat镜像6. 发布自己的镜像6.1 DockerHub6.2 阿里云镜像服务 7. 小结 1. Dockerfile 简介 Dockerfile 是用来构建docker 镜像的文件&#xff01;是一个命令参数脚本…

MySQL 窗口函数

MySQL的窗口函数是一种特殊类型的聚合函数&#xff0c;使用窗口函数可以使查询更高效&#xff0c;因为它们可以避免在多个聚合阶段中重复扫描相同的行。还可以使用窗口函数来计算一些有趣的结果&#xff0c;例如排名、百分比和移动平均值等。 目录 一、认识窗口函数1、窗口函数…

重入的问题搞清楚

很久很久之前&#xff0c;写入重入问题的文章 如果你在笔试的实际&#xff0c;面试官问——下面这个代码有什么问题&#xff1f; #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #incl…

代码随想录算法训练营第三十四天|1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果

文章目录 1005.K次取反后最大化的数组和134. 加油站:star:135. 分发糖果:star::star::star: 1005.K次取反后最大化的数组和 链接:代码随想录解题思路&#xff1a; 遇到负数就去反,k– 当k能遇到正数的是否&#xff0c;如果k % 2 1 必须取一个最小的正数进行取反再加才行 pub…

Vue 计算属性

文章目录 Vue 计算属性computed vs methodscomputed setter Vue 计算属性 计算属性关键词: computed。 计算属性在处理一些复杂逻辑时是很有用的。 可以看下以下反转字符串的例子&#xff1a; 实例 1 <div id"app">{{ message.split().reverse().join() }}…

【SpringBoot2】SpringBoot开发实用篇

SpringBoot开发实用篇 KF-1.热部署 ​ 什么是热部署&#xff1f;简单说就是你程序改了&#xff0c;现在要重新启动服务器&#xff0c;嫌麻烦&#xff1f;不用重启&#xff0c;服务器会自己悄悄的把更新后的程序给重新加载一遍&#xff0c;这就是热部署。 ​ 热部署的功能是如…

使用CDN的好处

基于移动和应用程序的互联网迫使越来越多的内容提供商、内容出版商和在线供应商简化导航并改善用户体验&#xff0c;主要是网站的页面加载时间。 以下是您必须考虑在业务生态系统中实施CDN的8个原因&#xff1a; 全球可访问性&#xff1a;内容交付网络有助于使内容在全球范围内…

图像分类识别(方向/重点指引)

1、继YOLO之后的高效目标检测算法&#xff1a; CenterNet 继YOLO之后的高效目标检测算法&#xff1a; CenterNet 2、百度飞浆面向 AI 行业应用场景的开源项目&#xff1a;GitHub - PaddlePaddle/PaddleX: PaddlePaddle End-to-End Development Toolkit&#xff08;『飞桨』…

在win10系统中安装anaconda

1、 Anaconda的下载 你可以根据你的操作系统是32位还是64位选择对应的版本到官网下载&#xff0c;但是官网下载龟速。 建议到清华大学镜像站下载 &#xff1a;Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source MirrorIndex of /anaconda/archiv…

1.微服务项目实战---SpringCloud Alibaba

1.1 系统架构演变 随着互联网的发展&#xff0c;网站应用的规模也在不断的扩大&#xff0c;进而导致系统架构也在不断的进行变化。 从互联网早起到现在&#xff0c;系统架构大体经历了下面几个过程 : 单体应用架构 ---> 垂直应用架构 ---> 分布 式架构--->SOA 架构…

【学习总结】openvins中的IMU数据仿真

本文介绍 openvins 中IMU仿真时基于控制轨迹和SPline插值&#xff0c;并计算IMU输出&#xff0c;的原理和代码。 参考 Open-vins中关于仿真的描述&#xff1a;https://docs.openvins.com/simulation.html Open-vins论文&#xff1a;https://pgeneva.com/downloads/papers/Gen…