【系列 2】手写vue模板编译

2023-11-06

模板编译流程是什么?

1. 获取 outerHTML

<div id="app">{{name}}</div>

2. 正则查找转义成 ast 语法树

ast = {
    "tag": "div",
    "attrs": [
        {
            "name": "id",
            "value": "app"
        }
    ],
    "children": [
        {
            "type": 3,
            "text": "{{name}}"
        }
    ],
    "parent": null,
    "type": 1
}

3. ast 语法树转换成 render 字符串函数

 const code = '_c("div", {id:"app"},_v(_s(name)))'

4. 执行 redner 函数 生成 vnode

const render = new Function(`with(this){return ${code}}`)
const vnode = render.call(vm) // 目的就是让 render 函数 this 指向 vue 实例, 因为 vue 实例有 _c, _v, _s 这些方法

// vnode 大体结构如下
vnode = {
    children: [{children: null, data: null, key: null, tag: null, text: "小米", vm: Vue}]
    data: {id: 'app'}
    key: undefined
    tag: "div"
    text: null
    vm: Vue {$options: {}, _data: {}, $el: undefined,}
}

其实 vue 模板编译就上面这几个步骤,拆分一下是不是很清晰,接下来我们一步一步看看是怎么实现的!

1. 获取 outerHTML

很简单 el 就是用户 new Vue({ el: '#app' }) 里面的 #app, outerHTML 就是获取节点的 html 代码

 const el = document.querySelector(el);
 const template = el.outerHTML // <div id="app">{{name}}</div>

2. 正则查找转义成 ast 语法树

复制一下代码放入浏览器执行你会发现 ast 就被解析出来了

const template = '<div id="app">{{name}}</div>'
const ast = parserHTML(template)

解析过程:

  1. 找到标签头 <div 准备对象 {tagName: ‘div’,attrs: []}, 删除html上匹配到的内容
  2. 找到标签属性 id="app" 添加属性,------------------- 删除html上匹配到的内容
  3. 找到标签头结尾 > 返回 ast 入栈 [ ast ],-------------- 删除html上匹配到的内容
  4. 找到标签内容 {{name}} ast 添加 children,----------- 删除html上匹配到的内容
  5. 找到标签尾 </div> 出栈 [ ],-------------------------- 删除html上匹配到的内容

在这里插入图片描述

看懂了吗?就是将匹配到的一点一点删掉,直到 html 没了

下面的正则理解不了, 可以复制到 正则解析网站 里看看

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 用来描述标签的
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的  捕获的是结束标签的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的  分组1 拿到的是属性名  , 分组3 ,4, 5 拿到的是key对应的值

const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 /> or >

function parserHTML(html) {
  function advance(n) {
    html = html.substring(n) // 每次根据传入的长度截取html
    console.log("html 剩下", html)
  }

  // 树的操作 ,需要根据开始标签和结束标签产生一个树
  let root
  // 如何创建树的父子关系
  let stack = []

  function createASTElement(tagName, attrs) {
    return {
      tag: tagName,
      attrs,
      children: [],
      parent: null,
      type: 1
    }
  }

  // 开始标签进栈 (先进后出原理)
  function start(tagName, attrs) {
    let element = createASTElement(tagName, attrs)
    if (root == null) {
      root = element
    }
    let parent = stack[stack.length - 1] // 取到栈中的最后一个
    if (parent) {
      element.parent = parent       // 让这个元素记住自己的父亲是谁
      parent.children.push(element) // 让父亲记住儿子是谁
    }
    stack.push(element) //入栈
  }

  // 结束标签出栈
  function end(tagName) {
    stack.pop() //出栈
  }

  // 处理标签内容
  function chars(text) {
    text = text.replace(/\s/g, '')
    if (text) {
      let parent = stack[stack.length - 1]
      parent.children.push({ // 增加一个子元素
        type: 3, // 类型 3 表示文本
        text
      })
    }
  }

  //  ast 描述的是语法本身 ,语法中没有的,不会被描述出来  虚拟dom 是描述真实dom的可以自己增添属性
  while (html) {
    // 1. 处理开始标签 (就是处理 <div id="app">{{name}}</div>  的 <div id="app"> 部分)
    let textEnd = html.indexOf('<')
    if (textEnd === 0) {
      const startTagMatch = parseStartTag(); // 解析开始标签  {tagName:'div',attrs:[{name:"id",value:"app"}]}
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs)
        continue
      }

      // 3. 处理结束标签 (就是处理 <div id="app">{{name}}</div>  的 </div> 部分)
      let matches
      if (matches = html.match(endTag)) {
        end(matches[1])
        advance(matches[0].length)
        continue
      }
    }

    // 2. 处理标签内容 (就是处理 <div id="app">{{name}}</div> 的 {{name}} 部分)
    let text
    if (textEnd >= 0) {
      text = html.substring(0, textEnd)
    }
    if (text) {
      advance(text.length) // html 删去 text, 处理一点删一点
      chars(text)
    }
  }

  function parseStartTag() {
    const matches = html.match(startTagOpen) // 获取标签头 <div id="app">{{name}}</div> 的 <div 部分
    if (matches) {
      const match = {
        tagName: matches[1],
        attrs: []
      }
      advance(matches[0].length) // 删除html前面匹配到的标签名字符串
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        // while循环取属性 直到取完
        match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] || true })
        advance(attr[0].length) // 取到一个属性删除一个
      }
      if (end) {
        advance(end[0].length)
        return match
      }
    }
  }

  return root
}

3. ast 语法树转换成 render 字符串函数

const ast = {"tag": "div","attrs": [{"name": "id","value": "app"}],"children": [{"type": 3,"text": "{{name}}"}],"parent": null,"type": 1}  // 别看了就是上面那个 ast 对象

let code = genCode(ast) // '_c("div", {id:"app"},_v(_s(name)))'

逻辑如下:(看不下去就别看了,放浏览器执行一下看结果吧)

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配双花括号中间单的内容

function genCode(ast) {
  let code
  code = `_c("${ast.tag}", ${
    ast.attrs.length ? genProps(ast.attrs) : 'undefined'
  }${
    ast.children ? (',' + genChildren(ast)) : ''
  })` // 后面的参数都是 children

  return code
}

// 参数拼接成对象
function genProps(attrs) {
  let str = ''
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i]
    if (attr.name === 'style') {
      let obj = {}
      attr.value.split(';').reduce((memo, current) => {
        let [key, value] = current.split(':')
        memo[key] = value
        return memo
      }, obj)
      attr.value = obj // 这里是样式对象 例:{color:red,background:blue}
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`
  }
  return `{${str.slice(0,-1)}}` // 删除最后的 ,
}

function genChildren(ast) {
  const children = ast.children
  return children.map(child => gen(child)).join(',') // 孩子 , 拼接
}


function gen(node) {
  if (node.type === 1) {  // 是节点
    return genCode(node)
  } else {
    let text = node.text
    if (!defaultTagRE.test(text)) {
      return `_v(${JSON.stringify(text)})` // 不带表达式的
    } else {
      let tokens = []
      let match
      // exec 遇到全局匹配会有 lastIndex 问题 每次匹配前需要将lastIndex 置为 0
      let startIndex = defaultTagRE.lastIndex = 0
      while (match = defaultTagRE.exec(text)) {
        let endIndex = match.index // 匹配到索引
        if (endIndex > startIndex) {
          tokens.push(JSON.stringify(text.slice(startIndex, endIndex)))
        }
        tokens.push(`_s(${match[1].trim()})`)
        startIndex = endIndex + match[0].length
      }
      if (startIndex < text.length) { // 最后的尾巴放进去
        tokens.push(JSON.stringify(text.slice(startIndex)))
      }
      return `_v(${tokens.join('+')})` // 最后将动态数据 和非动态的拼接在一起
    }
  }
}

4. 执行 redner 函数 生成 vnode

new Function('字符串') 可以让字符串变成函数
with(this){内部的代码} 可以让内部的代码变量从 this 里获取

function Vue() {}
const code = '_c("div", {id:"app"},_v(_s(name)))'
lifeCycleMixin(Vue)  // 给 Vue 实例添加 _c, _v, _s 等方法

const vm = new Vue()
vm.$options = {
   render: new Function(`with(this){return ${code}}`)
}
mountComponent(vm)  // 生成 vnode 并且交给 _update 方法将 vnode 渲染成真实 dom

今天就介绍到生成 vnode 了,后续的 _update 方法里面包含了 diff 算法,这不是本系列的重点,放后面讲吧!

function lifeCycleMixin(Vue) {
  Vue.prototype._c = function() { // 生成 vnode
    return createElement(this, ...arguments)
  }
  Vue.prototype._v = function() {
    return createTextNode(this, ...arguments)
  }
  Vue.prototype._s = function(value) { // 将数据转化成字符串 因为使用的变量对应的结果可能是一个对象
    if(typeof value === 'object' && value !== null){
      return JSON.stringify(value)
    }
    return value
  }
  Vue.prototype._render = function() {
    const vm = this;
    const render = vm.$options.render;
    let vnode = render.call(vm); // _c( _s _v)  with(this)
    console.log("vnode =", vnode)
    return vnode;
  }
  Vue.prototype._update = function(vnode) { // 将虚拟节点变成真实节点
    // 将 vnode 渲染el元素中
    // const vm = this;
    // vm.$el = patch(vm.$el,vnode); // 可以初始化渲染, 后续更新也走这个patch方法
  }
}

function createElement(vm, tag, data = {}, ...children) {
    return vnode(vm,tag,data,children,data.key,null)
}

function createTextNode(vm,text) {
    return vnode(vm,null,null,null,null,text)
}

function vnode(vm,tag,data,children,key,text){
    return {
        vm,
        tag,
        data,
        children,
        key,
        text
        // ...
    }
}


function mountComponent(vm) {
  // 实现页面的挂载流程
  const updateComponent = () => {
    // 需要调用生成的render函数 获取到虚拟节点  -> 生成真实的dom
    vm._update(vm._render());
  }
  updateComponent(); // 如果稍后数据变化 也调用这个函数重新执行 
  // 后续:观察者模式 + 依赖收集 + diff算法
}

如果你想运行以上代码,ctrl + shift + n 进入无痕模式运行

在这里插入图片描述

看到这为什么 vue 不直接把 template 一步到位转成 vnode,而是 template 转成 render 再转成 vnode?
知道就写评论里,不知道就算了我不会告诉你的?

手写 vue 代码仓库 链接
GitHub https://github.com/shunyue1320/vue-resolve/tree/vue-02
Gitee https://gitee.com/shunyue/vue-resolve/tree/vue-02/
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【系列 2】手写vue模板编译 的相关文章

  • Bootstrap 页脚不在底部

    我试图强制我的页脚位于网站底部 我不希望它在滚动时粘住 只是在向下滚动网页时出现在底部 目前 网页显示时页脚位于内容下方 我添加了这样的代码bottom 0 并发现它粘住了并且不适合我的网站 我还添加了这样的代码html body heig
  • 使用 mongoDB 插入子文档

    我收集了以下文件 id 2 workspace name 1 widgets name 2 widgets name 3 widgets name 4 widgets 我怎样才能插入 id 1 blabla blabla 在 小部件 中的
  • 强制浏览器打开文件而不是提示下载

    在 Firefox 和 Chrome 中单击 PDF 链接时 有时会打开该文件以在浏览器中查看 有时会提示 另存为 对话框 如果我想强制链接始终提示下载 我可以使用downloadHTML5 属性 然而 我想做相反的事情 IE 强制链接始终
  • 防止 Node.js 中的 SQL 注入

    是否有可能以与 PHP 具有防范 SQL 注入的预准备语句相同的方式防止 Node js 中的 SQL 注入 最好使用模块 如果是这样 怎么办 如果不 有哪些例子这可能会绕过我提供的代码 见下文 一些背景 我正在制作一个 Web 应用程序
  • 禁用拉斐尔论文上的浏览器上下文菜单

    我有一篇拉斐尔论文定义为R1 Raphael 0 0 800 600 我想在本文中禁用上下文菜单 以便我可以捕获鼠标右键单击事件 我没有这样的Raphael someDiv 800 600 禁用 div 的 oncontextmenu 属性
  • 有效 JSON 上的 Firefox JSON“格式不正确”错误

    我在 Firefox 中收到以下错误消息 Error not well formed Source File http school courses booking add php 1287657494723 Line 1 Column 1
  • 通过标记或JS强制下载

    假设我在 CDN 来自 Rackspace 的云文件 上有一个文件 以及一个包含该文件链接的静态 html 页面 有什么方法可以强制下载此文件 以防止它在浏览器中打开 例如 mp3 我们可以让我们的服务器读取该文件并将相应的标头设置为 he
  • 如何替换 CSS 表达式

    我有一个用于时间表条目的旧 ASP Web 应用程序 其中充满了 CSS 表达式 它们出现在 CSS 文件中 ApptPage position relative height expression Math max document bo
  • jQuery onclick 隐藏其父元素[关闭]

    很难说出这里问的是什么 这个问题是含糊的 模糊的 不完整的 过于宽泛的或修辞性的 无法以目前的形式得到合理的回答 如需帮助澄清此问题以便重新打开 访问帮助中心 help reopen questions 我想隐藏 li tag on a 使
  • 在 Bootstrap 选择器上使用 jQuery 取消选择选项

    我对一些 UI 元素使用 Bootstrap SelectPicker 它允许用户选择多个选项并将其呈现在段落标签中的屏幕上 他们还应该能够删除选定的选项 这是我的代码 用于将选定的选项渲染到屏幕上 以便每个选项旁边都会显示一个 X 单击它
  • LightningChart JS - LineSeries / Progressive X 的损坏

    我在使用 LightningChart 时遇到了一个有趣的问题 它似乎会破坏或以其他方式减少我的数据 具体取决于它与图表的 DateOrigin 的距离 我的数据是每秒 1000 个样本 我试图一次显示 1 2 周的数据 我正在使用 Cha
  • 如何转义 onClick 处理程序内 JavaScript 代码中的字符串?

    也许我只是想得太难了 但我在弄清楚链接的 onClick 处理程序内的某些 JavaScript 代码中的字符串上使用什么转义时遇到了问题 例子 a href Select a The and 是发生模板替换的地方 我的问题是项目名称可以包
  • 过滤数据表中的行

    我目前的 JQuery 插件 DataTables 工作正常 并且我在页面顶部插入了一个按钮来进一步过滤列表 我还使用了 DataTables 内置的搜索栏 我希望按钮向下过滤表格 并只显示包含特定值的行 下面是我一直在做的事情 但似乎没有
  • Bootstrap 面板主体,内有表格

    我有一个引导面板 单击图标即可折叠并自动关闭 该面板内部包含一个全宽的表格 但只有在没有任何内容时才看起来像这样panel body 例如 这张桌子横跨面板的整个宽度和高度 看起来不错 但如果我可以有一个围绕桌子的类 我会更喜欢 但是 如果
  • Google Maps API - 调整大小会生成空白区域

    我正在研究一个看起来非常简单的功能 但却引起了一些头痛 我使用 GIS 并将其与 Google 地图集成 其中一位客户要求能够打开一个仅显示地图的单独弹出窗口 打开窗口不是问题 但当我尝试扩展包含地图的 IFrame 的宽度时 为了适应窗口
  • 如何从 Instagram 的 media_preview 原始数据重新创建预览?

    如果您从 Instagram 的 API 获取 JSON 数据 您会发现media previewkey 其值是一些 Base64 编码的数据 它看起来确实像一些非常小的预览二进制数据 也许是压缩的 Take 这个帖子 https www
  • React cloneElement 未设置键

    我正在构建一个动态生成键的表控件 我理解这可能不是一个好主意 我想键应该与其代表的数据唯一关联 否则 React 只能为我们生成唯一的 id 但无论哪种方式似乎没有设置按键 我不知道为什么 表中的行是用可以找到的函数生成的here http
  • 多个引导模式的滚动问题

    我有一个带有大量信息的模态页面 因此您需要滚动 该模式包含指向第二个模式的链接 When I 打开模态 1 单击链接打开模式 2 模式 1 保持在后台 然后关闭模式 2 以便我回到模式 1 modal 1 失去滚动 仍然有一个滚动条 但它不
  • Promise链基本问题

    我正在尝试理解 Promise 我创建了一些有效的承诺链 而另一些则无效 我已经取得了进步 但显然缺乏基本概念 例如 以下承诺链不起作用 这是一个愚蠢的例子 但说明了问题 我正在尝试在链中使用 Node 的函数 randomBytes 两次
  • 如何将国家/地区代码与电话号码分开?

    我的数据库中有很多电话号码 例如 1 123 456 7890 我要做的是将国家 地区拨号代码 在本例中为美国 加拿大的 1 与电话号码分开 我尝试创建所有国家 地区的 JSON 列表 并在加载页面时将电话号码和国家 地区代码分开 它工作正

随机推荐

  • 基于Matlab的激光雷达与单目摄像头联合外参标定

    1 背景介绍 目前团队正在与某主机厂合作开发L4级自动驾驶出租车项目 所用的传感器包括80线激光雷达和单目摄像头 为了充分利用Lidar和Cam各自的优势 设计了一种融合算法 需要获得Lidar2Camera的联合外参 前期使用Autowa
  • Java数组&二维数组

    Java数组 二维数组 1 一维数组 1 1 数组介绍 数组就是存储数据长度固定的容器 存储多个数据的数据类型要一致 1 2 数组的定义格式 1 2 1 第一种格式 数据类型 数组名 示例 int arr double arr char a
  • python调用多级目录中的文件_python复制多层目录下的文件至其他盘符对应的目录中...

    tmp c cmd js d TZT2 0 js d TZT js d TZT 346 226 207 346 241 243 350 257 264 346 230 216 json d c modules config js d css
  • Go_包、工程管理

    包 包其实就是文件夹 把文件分类放到不同的包利于管理 作用 如果把所有的代码都放在一个文件中 后续的可维护性 阅读性都比较差 所以可以使用包的来区分不同的模块 功能分别放在不同包中 然后其它的文件使用到功能就调用就可以了 在同一个包下是函数
  • android studio在XML预览中出现Rendering Problems问题

    http blog csdn net u014365838 article details 52078501 如图所示 出现问题的原因是style不和规范 虽然没有大问题 编译也可以通过 不过总有些碍眼 解决方法 1 在styles xml
  • apache-ant build.xml 实例

    文章目录 version apache ant 1 9 15 build xml
  • php发送邮件样式_php简单实现发送带附件的邮件

    这篇文章主要介绍了php简单实现发送带附件的邮件 涉及附件上传及邮件发送的相关技巧 需要的朋友可以参考下 本文实例讲述了php简单实现发送带附件的邮件 分享给大家供大家参考 具体如下 下面是静态html代码 带附件的邮件发送 发送人 收件人
  • 多gpu训练梯度如何计算,求和是否要求平均

    作者 智星云服务 链接 https www zhihu com question 271226455 answer 1521784627 来源 知乎 著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处 作者 itsAndy
  • libc.so.6

    libc so 6是一个类似于WINDOWS下的一个快捷指向型的文件 用命令LN产生 ln s root arm unknown linux gnu arm unknown linux gnu lib libc so root worksp
  • uni-app生产环境去掉console

    uni app生产环境去掉console 运行期判断 运行期判断是指代码已经打入包中 仍然需要在运行期判断平台 此时可使用 uni getSystemInfoSync platform 判断客户端环境是 Android iOS 还是小程序开
  • kubernetes安全检测工具-kube-bench

    一 kube bench基础介绍 kube bench是基于go语言开发 一款针对kubernetes进行安全检测的工具 主要是检测kubernetes集群的各个组件的配置 确认配置文件是否符合安全基线标准 输出检测报告 并给出修复建议 从
  • 蓝桥杯 算法训练 拿金币 Java

    import java util Scanner public class Main public static void main String args Scanner scanner new Scanner System in int
  • 第十二届蓝桥杯javaB组刷题day2

    1 直线 我们思路 暴力解法 将所有可能的情况列举出来 故需要两个坐标的横纵坐标 便需要四个for循环来进行遍历 出现问题 需要排除斜率为0的情况 后面再单独进行相加 因为需要set来加入斜率 而一条斜线y kx b 包括k和b set只能
  • Linux ceontOS7.X 安装RabbitMQ

    安装erlang环境 https blog csdn net qq 37279783 article details 90515409 下载 解压MQ 下载 https www rabbitmq com releases rabbitmq
  • 测试用例设计方法---等价类划分法

    1 等价类划分法 1 1 定义 是把所有可能输入的数据 即程序的输入域划分策划国内若干部分 子集 然后从每一个子集中选取少数具有代表性的数据作为测试用例 方法是一种重要的 常用的黑盒测试用例设计方法 1 1划分等价类 1 有效等价类 指对于
  • 大师兄!SLAM 为什么需要李群与李代数?

    from https mp weixin qq com s sVjy9kr 8qc9W9VN78JoDQ 作者 electech6 来源 计算机视觉life 编辑 Tony 很多刚刚接触SLAM的小伙伴在看到李群和李代数这部分的时候 都有点
  • 编译原理第二版4.4答案

    4 4 节的练习 4 4 1 为下面的每个文法设计一个预测分析器 并给出预测分析表 你可能先要对文法进行提取左公因子或者消除左递归的操作 练习 4 2 2 中 1 7 中的文法 解答 S gt 0 S 1 0 1 step1 提取左公因子
  • [1182]clickhouse的日期函数

    文章目录 时间获取 时间转换 时间计算 时间格式化 时间获取 获取当前时间戳 select toUnixTimestamp now 获取指定时间的时间戳 select toUnixTimestamp 2021 05 11 14 37 37
  • Java线程:并发协作-死锁

    本文转载至 http lavasoft blog 51cto com 62575 222074 Java线程 并发协作 死锁 线程发生死锁可能性很小 即使看似可能发生死锁的代码 在运行时发生死锁的可能性也是小之又小 发生死锁的原因一般是两个
  • 【系列 2】手写vue模板编译

    模板编译流程是什么 1 获取 outerHTML div name div 2 正则查找转义成 ast 语法树 ast tag div attrs name id value app children type 3 text name pa