01- vdom 和模板编译源码

news2024/11/16 21:41:56

组件渲染的过程

template --> ast --> render --> vDom --> 真实的Dom --> 页面

Runtime-Compiler和Runtime-Only的区别 - 简书

编译步骤

模板编译是Vue中比较核心的一部分。关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:

第一步:将模板字符串转换成element ASTs( 解析器 parse

第二步:对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器 optimize

第三步:使用element ASTs生成render函数代码字符串(代码生成器 generate

 编译后的AST结构

template 模板:

<div class="box">
  <p>{{name}}</p>
</div>

AST 抽象语法树:

ast: {
  tag: "div" //  元素标签名
  type: 1,  // 元素节点类型 1标签 2包含字面量表达式的文本节点 3普通文本节点或注释节点
  staticRoot: false, // 是否静态根节点
  static: false,  // 是否静态节点
  plain: true, 
  parent: undefined,
  attrsList: [], // 标签节点的属性名和值的对象集合
  attrsMap: {}, // 和attrsList类似,不同在它是以键值对保存属性名和值
  children: [
    {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
        type: 2,
        text: "{{name}}",
        static: false,
        expression: "_s(name)"  // type为2时才有这个属性,表示表达式的内容
      }]
    }
  ]
}

generate,将AST转换成可以直接执行的JavaScript字符串

with(this) {
    return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}

注意一:平常开发中 我们使用的是不带编译版本的 Vue 版本(runtime-only)直接在 options 传入 template 选项 在开发环境报错

注意二:这里传入的 template 选项不要和.vue 文件里面的模板搞混淆了 vue 单文件组件的 template 是需要 vue-loader 进行处理的

我们传入的 el 或者 template 选项最后都会被解析成 render 函数 这样才能保持模板解析的一致性

以下代码实现是在在entry-runtime-with-compiler.js里面,和runtime-only版本需要区分开

1、模板编译入口

export function initMixin (Vue) {
	Vue.prototype._init = function (options) {
		const vm = this;
		vm.$options = options;
		initState(vm);
		// 如果有 el 属性,进行模板渲染
		if (vm.$options.el) {
			vm.$mount(vm.$options.el)
		}
	}

	Vue.prototype.$mount = function (el) {
		const vm = this;
		const options = vm.$options;
		el = document.querySelector(el);
		// 不存在 render 属性的几种情况
		if (!options.render) {
			// 不存在 render 但存在 template
			let template = options.template;
			// 不存在 render 和 template 但是存在 el 属性,直接将模板赋值到 el 所在的外层 html 结构 (就是 el 本身,并不是父元素)
			if (!template && el) {
				template = el.outerHTML;
			}
			// 最后把处理好的 template 模板转化成 render 函数
			if (template) {
				const render = compileToFunctions(template);
				options.render = render;
			}
		}
	}
}
  • 先初始化状态,initState(vm)
  • 再在 initMixin 函数中,判断是否有el,有则直接调用 vm.$mount(vm.$options.el) 进行模板渲染,没有则手动调用;
  • Vue.prototype.$mount 方法中,判断是否存在 render 属性,存在则给 template 赋值,如果不存在 render 和 template 但存在 el 属性,直接将模板赋值到 el 所在的外层 html 结构(就是el本身,并不是父元素);
  • 通过 compileToFunctions 将处理好的 template 模板转换成 render 函数

咱们主要关心$mount 方法 最终将处理好的 template 模板转成 render 函数

2、模板转化核心方法 compileToFunctions

export function compileToFunctions (template) {
	// html → ast → render函数
	// 第一步 将 html 字符串转换成 ast 语法树
	let ast = parse(template);
	// 第二步 优化静态节点
	if (mergeOptions.optimize !== false) {
		optimize(ast, options);
	}
	// 第三步 通过 ast 重新生成代码
	let code = generate(ast);
	// 使用 with 语法改变作用域为 this, 之后调用 render 函数可以使用 call 改变 this,方便 code 里面的变量取值。
	let renderFn = new Function(`with(this){return ${code}}`);
	return renderFn;
}

html → ast → render函数

这里需要把 html 字符串变成 render 函数,分三步:

(1)parse函数 把 HTML 代码转成 AST 语法树

        AST 是用来描述代码本身形成的树形结构,不仅可以描述 HTML,也可以描述 css 和 js 语法;很多库都运用到了 AST,比如 webpack,babel,eslint 等

(2) optimize函数 优化静态节点(主要用来做虚拟DOM的渲染优化)

        先遍历 AST,对 AST 进行静态节点标记,(即节点永远不会发生变化)并做出一些特殊的处理。例如:

  • 移除静态节点的 v-once 指令,因为它在这里没有任何意义。
  • 移除静态节点的 key 属性。因为静态节点永远不会改变,所以不需要 key 属性。
  • 将一些静态节点合并成一个节点,以减少渲染的节点数量。例如相邻的文本节点和元素节点可以被合并为一个元素节点。

(3)generate函数 通过 AST 重新生成代码

最后生成的代码需要和 render 函数一样

类似这样的结构:

_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  • _c 代表 createElement 创建节点
  • _v 代表 createTextVNode 创建文本节点
  • _s 代表 toString 把对象解析成字符串
     

模板引擎的实现原理  with + new Function,使用 with 语法改变作用域为 this, 之后调用 render 函数可以使用 call 改变 this,方便 code 里面的变量取值。

parse函数,解析 html 并生成 ast

// src/compiler/parse.js

// 以下为源码的正则  对正则表达式不清楚的同学可以参考小编之前写的文章(前端进阶高薪必看 - 正则篇);
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束  >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性  形如 id="app"

let root, currentParent; //代表根节点 和当前父节点
// 栈结构 来表示开始和结束标签
let stack = [];
// 标识元素和文本type
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
// 生成ast方法
function createASTElement(tagName, attrs) {
  return {
    tag: tagName,
    type: ELEMENT_TYPE,
    children: [],
    attrs,
    parent: null,
  };
}

// 对开始标签进行处理
function handleStartTag({ tagName, attrs }) {
  let element = createASTElement(tagName, attrs);
  if (!root) {
    root = element;
  }
  currentParent = element;
  stack.push(element);
}

// 对结束标签进行处理
function handleEndTag(tagName) {
  // 栈结构 []
  // 比如 <div><span></span></div> 当遇到第一个结束标签</span>时 会匹配到栈顶<span>元素对应的ast 并取出来
  let element = stack.pop();
  // 当前父元素就是栈顶的上一个元素 在这里就类似div
  currentParent = stack[stack.length - 1];
  // 建立parent和children关系
  if (currentParent) {
    element.parent = currentParent;
    currentParent.children.push(element);
  }
}

// 对文本进行处理
function handleChars(text) {
  // 去掉空格
  text = text.replace(/\s/g, "");
  if (text) {
    currentParent.children.push({
      type: TEXT_TYPE,
      text,
    });
  }
}

// 解析标签生成ast核心
export function parse(html) {
  while (html) {
    // 查找<
    let textEnd = html.indexOf("<");
    // 如果<在第一个 那么证明接下来就是一个标签 不管是开始还是结束标签
    if (textEnd === 0) {
      // 如果开始标签解析有结果
      const startTagMatch = parseStartTag();
      if (startTagMatch) {
        // 把解析好的标签名和属性解析生成ast
        handleStartTag(startTagMatch);
        continue;
      }

      // 匹配结束标签</
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
        advance(endTagMatch[0].length);
        handleEndTag(endTagMatch[1]);
        continue;
      }
    }

    let text;
    // 形如 hello<div></div>
    if (textEnd >= 0) {
      // 获取文本
      text = html.substring(0, textEnd);
    }
    if (text) {
      advance(text.length);
      handleChars(text);
    }
  }

  // 匹配开始标签
  function parseStartTag() {
    const start = html.match(startTagOpen);

    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
      };
      //匹配到了开始标签 就截取掉
      advance(start[0].length);

      // 开始匹配属性
      // end代表结束符号>  如果不是匹配到了结束标签
      // attr 表示匹配的属性
      let end, attr;
      while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(attribute))
      ) {
        advance(attr[0].length);
        attr = {
          name: attr[1],
          value: attr[3] || attr[4] || attr[5], //这里是因为正则捕获支持双引号 单引号 和无引号的属性值
        };
        match.attrs.push(attr);
      }
      if (end) {
        //   代表一个标签匹配到结束的>了 代表开始标签解析完毕
        advance(1);
        return match;
      }
    }
  }
  //截取html字符串 每次匹配到了就往前继续匹配
  function advance(n) {
    html = html.substring(n);
  }
  //   返回生成的ast
  return root;
}

generate函数,把 ast 转化成 render 函数结构

// src/compiler/codegen.js

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{  }} 捕获花括号里面的内容

function gen(node) {
  // 判断节点类型
  // 主要包含处理文本核心
  // 源码这块包含了复杂的处理  比如 v-once v-for v-if 自定义指令 slot等等  咱们这里只考虑普通文本和变量表达式{{}}的处理

  // 如果是元素类型
  if (node.type == 1) {
    //   递归创建
    return generate(node);
  } else {
    //   如果是文本节点
    let text = node.text;
    // 不存在花括号变量表达式
    if (!defaultTagRE.test(text)) {
      return `_v(${JSON.stringify(text)})`;
    }
    // 正则是全局模式 每次需要重置正则的lastIndex属性  不然会引发匹配bug
    let lastIndex = (defaultTagRE.lastIndex = 0);
    let tokens = [];
    let match, index;

    while ((match = defaultTagRE.exec(text))) {
      // index代表匹配到的位置
      index = match.index;
      if (index > lastIndex) {
        //   匹配到的{{位置  在tokens里面放入普通文本
        tokens.push(JSON.stringify(text.slice(lastIndex, index)));
      }
      //   放入捕获到的变量内容
      tokens.push(`_s(${match[1].trim()})`);
      //   匹配指针后移
      lastIndex = index + match[0].length;
    }
    // 如果匹配完了花括号  text里面还有剩余的普通文本 那么继续push
    if (lastIndex < text.length) {
      tokens.push(JSON.stringify(text.slice(lastIndex)));
    }
    // _v表示创建文本
    return `_v(${tokens.join("+")})`;
  }
}

// 处理attrs属性
function genProps(attrs) {
  let str = "";
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    // 对attrs属性里面的style做特殊处理
    if (attr.name === "style") {
      let obj = {};
      attr.value.split(";").forEach((item) => {
        let [key, value] = item.split(":");
        obj[key] = value;
      });
      attr.value = obj;
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`;
  }
  return `{${str.slice(0, -1)}}`;
}

// 生成子节点 调用gen函数进行递归创建
function getChildren(el) {
  const children = el.children;
  if (children) {
    return `${children.map((c) => gen(c)).join(",")}`;
  }
}
// 递归创建生成code
export function generate(el) {
  let children = getChildren(el);
  let code = `_c('${el.tag}',${
    el.attrs.length ? `${genProps(el.attrs)}` : "undefined"
  }${children ? `,${children}` : ""})`;
  return code;
}

code 字符串生成 render 函数

export function compileToFunctions (template) {

	let ast = parseHTML(template);

	let code = genCode(ast);
	// 将模板变成 render 函数,通过 with + new Function 的方式让字符串变成 JS 语法来执行
	const render = new Function(`with(this){return ${code}}`);

	return render;
}

 https://juejin.cn/spost/7267858684667035704

Vue 模板 AST 详解 - 文章教程 - 文江博客

滑动验证页面

手写Vue2.0源码(二)-模板编译原理|技术点评_慕课手记

前端鲨鱼哥 的个人主页 - 文章 - 掘金

前端进阶高薪必看-正则篇 - 掘金

从vue模板解析学习正则表达式

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

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

相关文章

sklearn机器学习库(二)sklearn中的随机森林

sklearn机器学习库(二)sklearn中的随机森林 集成算法会考虑多个评估器的建模结果&#xff0c;汇总之后得到一个综合的结果&#xff0c;以此来获取比单个模型更好的回归或分类表现。 多个模型集成成为的模型叫做集成评估器&#xff08;ensemble estimator&#xff09;&#xf…

RabbitMq-1基础概念

RabbitMq-----分布式中的一种通信手段 1. MQ的基本概念&#xff08;message queue,消息队列&#xff09; mq:消息队列&#xff0c;存储消息的中间件 分布式系统通信的两种方式&#xff1a;直接远程调用&#xff0c;借助第三方完成间接通信 消息的发送方是生产者&#xff0c…

爬虫逆向实战(九)--猿人学第十三题

一、数据接口分析 主页地址&#xff1a;猿人学第十三题 1、抓包 通过抓包可以发现数据接口是api/match/13 2、判断是否有加密参数 请求参数是否加密&#xff1f; 无请求头是否加密&#xff1f; 无响应是否加密&#xff1f; 无cookie是否加密&#xff1f; 在“cookie”模块…

python数据分析需要学哪些,python数据分析要学多久

大家好&#xff0c;小编为大家解答python数据分析应该学什么软件的问题。很多人还不知道python数据分析需要什么基础&#xff0c;现在让我们一起来看看吧&#xff01; 根据调查结果&#xff0c;十大最常用的数据工具中有八个来自或利用Python。Python广泛应用于所有数据科学领域…

STM32 FLASH 读写数据

1. 《STM32 中文参考手册》&#xff0c;需要查看芯片数据手册&#xff0c;代码起始地址一般都是0x8000 0000&#xff0c;这是存放整个项目代码的起始地址 2. 编译信息查看代码大小&#xff0c;修改代码后第一次编译后会有这个提示信息 2.1 修改代码后编译&#xff0c;会有提示…

物联网在制造业中的应用

制造业目前正在经历第四次工业革命&#xff0c;物联网、人工智能和机器人等技术进步正在推动行业的发展。研究表明&#xff0c;到2024年&#xff0c;全球制造商将在物联网解决方案上投资700亿美元&#xff0c;许多制造商正在实施物联网设备&#xff0c;以利用预测性维护和复杂的…

优化GitHub网站访问慢的问题

方法一、修改host文件解决 大型网站服务器都不会是只有一台服务器,而是多台服务器组成的集群一起对外提供服务。 使用站长工具测速&#xff0c;找一个速度比较快的服务器。 图中可以看到140.82.121.4这个ip比较快&#xff0c; 下面修改hosts: Mac 在 /etc/hosts 中&#x…

.net连接mysql,提示找不到请求的 .Net Framework Data Provider。可能没有安装

开发完成的.net程序需要连接mysql数据库&#xff0c;在个人电脑上运行没问题&#xff0c;别人运行时提示“提示找不到请求的 .Net Framework Data Provider。可能没有安装”。经过查询&#xff0c;安装Connector/NET 8.1.0&#xff0c;下载地址如下所示&#xff1a; https://d…

Mac OS minicom 无法设置921600问题

MacOS minicom 无法设置921600问题 介绍过程解决方案参考资料 介绍 minicom是Mac上一款非常好用的串口工具。本文假设你已经安装minicom&#xff0c;并且知道minicom的一般配置和使用方法。这是“MacOS minicom 无法设置921600”的解决问题记录。它在以下环境中设置成功&#…

【制作npm包1】申请npm账号、认识个人包和组织包

概述 在开发当中经常有一种现象&#xff0c;重复代码写了N多遍&#xff0c;再次写同样的逻辑就再次翻查以前的代码逻辑。效率低下且容易出错&#xff0c;封装一个npm包的价值也不仅仅是给别人用&#xff0c;封装一套属于自己或者本部门的npm包也是相当有必要。 也许经常看到一…

fastadmin 自定义按钮弹窗不是异步xhr提交

遇到一个奇怪的问题 按官方文档要求&#xff0c;js中也重新绑定事件了 但弹窗出来的表单还不是xhr提交&#xff0c;这为什么&#xff1f; 经过我不断测试发现&#xff0c;如上述的方法名不能带有下划线&#xff0c;蛇形大小写&#xff0c;否则一律不生效。 浪费了我一天半时…

小程序CSS button按钮自定义高度之后不居中

问题&#xff1a; 按钮设置高度后不居中 <view><button class"btn1" size"">Save</button> </view> page {font-size: 30rpx; }.btn1 {margin-top: 100rpx;height: 190rpx;background: linear-gradient(90deg, #FF8A06, #FF571…

webSocket 聊天室 node.js 版

全局安装vue脚手架 npm install vue/cli -g 创建 vue3 ts 脚手架 vue create vue3-chatroom 后端代码 src 同级目录下建 server: const express require(express); const app express(); const http require(http); const server http.createServer(app);const io req…

微信小程序:模板使用

目录 模板的优点&#xff1a; 一、静态模板创建 二、静态模板使用 1.*.wxml引入模板 2.模板使用 3.*.wxss引入模板的样式 三、动态模板创建 四、动态模板使用 1.*.wxml引入模板 2.模板使用 3.*.js定义动态数据 五、结果展示 总结 模板的优点&#xff1a; 有利于保持网…

第二章 搜索 No.1BFS之Flood Fill与最短路模型

文章目录 Flood Fill算法1097. 池塘计数1098. 城堡问题1106. 山峰和山谷 最短路模型1076. 迷宫问题188. 武士风度的牛1100. 抓住那头牛 Flood Fill算法 可以在线性时间复杂度内&#xff0c;找到某个点所在的连通块 想象一个矩阵&#xff0c;有洼地和高地&#xff0c;选择一个洼…

经验分享:企业数据仓库建设方案总结!

导读 在企业的数字化转型浪潮中&#xff0c;数据被誉为“新时代的石油”&#xff0c;而数据仓库作为数据管理与分析的核心基础设施&#xff0c;在企业的信息化建设中扮演着重要的角色。本文将深入探讨企业数据仓库建设过程中所遇到的问题以及解决经验&#xff0c;为正在筹备或…

微信程序 自定义遮罩层遮不住底部tabbar解决

一、先上效果 二 方法 1、自定义底部tabbar 实现&#xff1a; https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html 官网去抄 简单写下&#xff1a;在代码根目录下添加入口文件 除了js 文件的list 需要调整 其他原封不动 代码&#xf…

电脑键盘打不了字按哪个键恢复?最新分享!

“有没有朋友知道电脑键盘为什么会莫名其妙就打不了字&#xff1f;明明用得好好的&#xff0c;突然就打不了字了&#xff0c;真的让人很迷惑&#xff01;有什么方法可以解决吗&#xff1f;” 电脑键盘为我们的办公提供了很大的方便&#xff0c;我们可以利用键盘输入我们需要的文…

如何用树莓派Pico针对IoT编程?

目录 一、Raspberry Pi Pico 系列和功能 二、Raspberry Pi Pico 的替代方案 三、对 Raspberry Pi Pico 进行编程 硬件 软件 第 1 步&#xff1a;连接计算机 第 2 步&#xff1a;在 Pico 上安装 MicroPython 第 3 步&#xff1a;为 Thonny 设置解释器 第 4 步&#xff…

深度学习实战48-【未来的专家团队】基于AutoCompany模型的自动化企业概念设计与设想

大家好,我是微学AI,今天给大家介绍一下深度学习实战48-【未来的专家团队】基于AutoCompany模型的自动化企业概念设计与设想,文本将介绍AutoCompany模型的概念设计,涵盖了AI智能公司的各个角色,并结合了GPT-4接口来实现各个角色的功能,设置中央控制器,公司运作过程会生成…