从零实现一个vue文件解析器

如何从 0 处理一个 vue 文件并实现简单的响应式?

在现在的前端工程化中,打包工具是不可或缺的,其中webpack无疑是占据了主导地位,当然也有尤大搞的vite,但是论生态和使用人数,至少在目前webpack还是更胜一筹。

打包工具能帮助我们打包前端文件,在webpack中,不同后缀的文件通过不同loader来处理。

本文就讨论下怎么实现一个处理.vue文件的loader,以及用loader处理完.vue文件怎么把内容渲染在浏览器上,并实现简单的响应式。

源码地址 gezhicui/vue-webpack

webpack 部分

首先进行 webpack 打包,把.vue 文件通过 vue-loader 处理。

实现一个简易的vue-loader,通过一系列正则,最终一个.vue 文件的内容会被包装到一个对象中

比方说我现在的.vue 文件写了下面这些内容:

<template>
  <div>
    <h2>{{ count + 1 }}</h2>
    <button @click="plus(1)">+</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      count: 0
    }
  },
  methods: {
    plus (num) {
      this.count += num;
    }
  }
}
</script>

那么经过 vue-loader 处理,就会变成一个对象:

{
  template:
   `<div>
     <h2>{{ count + 1 }}</h2>
      <button @click="plus(1)">+</button>
  </div>`,
  name: 'App',
  data() {
    return { count: 0 }
  },
  methods: {
    plus(num) { this.count += num; },
  }
}

那么,在浏览器执行这个文件的时候,我们就能通过createApp方法,把这个对象使用 createApp 进行处理,挂载到页面上

createApp 实现部分

在 vue 的main.js文件中,我们通常会把根组件传递给createApp作为入参,如:

import App from './App';
import { createApp } from '../modules/vue';
createApp(App).mount('#app');

那我们实现的重点就在于createAppvue 组件的处理,以及在createApp的返回内容(就是 vm)中添加mount方法,实现处理完的节点的挂载

接下来就一步步实现createApp,首先,我们先来定义一个 vm,一会儿所有的属性都可以放在 vm 上,同时把vue-loader解析过的文件对象中的内容给解构出来

function createApp(component) {
  const vm = {};
  const { template, methods, data } = component;
}

template 解析

在上面经过vur-loader处理后,template以字符串形式被放到对象中,所以我们可以拿到 dom 元素字符串,把他转成 dom 元素

/*
  template:
   `<div>
     <h2>{{ count + 1 }}</h2>
      <button @click="plus(1)">+</button>
  </div>`,
*/
vm.$node = createNode(template);

function createNode(template) {
  const _tempNode = document.createElement('div');
  _tempNode.innerHTML = template;
  return getFirstChildNode(_tempNode);
}

这样,我们就拿到了 html 接下来就是对 js 的操作

data 响应式处理

vue 的核心就在于响应式,vue2 通过Object.defineProperty实现响应式,我们来实现个简单的响应式处理

首先拿到data,为了创建多个组件时data不被互相影响,所以data是一个函数

vm.$data = data();

for (let key in vm.$data) {
  Object.defineProperty(vm, key, {
    get() {
      return vm.$data[key];
    },
    set(newValue) {
      vm.$data[key] = newValue;
      // update触发节点更新,至于实现我放到后面再说
      update(vm, key);
    },
  });
}

这样,我们就监听了data中每个属性的getset,实现了数据的响应式处理

初始化数据池

在上面的 template 解析中,我们已经拿到了template转换过后的节点,但是有个问题,节点的内容没有经过任何处理,如{{count + 1}}会原封不动的展示在浏览器中,我们希望的是最终展示的是 count 这个变量+1 的结果,所以我们需要对双括号语法进行解析

我们先定义一个正则表达式,匹配{{}}中的内容,以及定义一个节点数据池

// 节点数据池
const exprPool = new Map();
// 正则获取双括号中内容
const regExpr = /\{\{(.+?)\}\}/;

然后,从我们刚刚定义的vm.$node中拿到所有节点,并查看该节点是否有双括号语法,如果有的话存入节点数据池中

const allNodes = $node.querySelectorAll('*');
allNodes.forEach((node) => {
  // 这里获取到的textContent是原原始的没经过任何处理的节点内容,如{{count + 1}}
  const vExpression = node.textContent;
  /* exprMatched:{
      0: "{{ count + 1 }}"
      1: " count + 1 "
      groups: undefined
      index: 0
      input: "{{ count + 1 }}"
    }
    */
  const exprMatched = vExpression.match(regExpr);
  // 如果有双括号语法
  if (exprMatched) {
    const poolInfo = checkExpressionHasData($data, exprMatched[1].trim());
    // 把节点存入节点数据池
    poolInfo && exprPool.set(node, poolInfo);
  }
});

function checkExpressionHasData(data, expression) {
  for (let key in data) {
    if (expression.includes(key) && expression !== key) {
      // count + 1,返回{key:count,expression:count+1}
      return {
        key,
        expression,
      };
    } else if (expression === key) {
      // count,返回{key:count,expression:count}
      return {
        key,
        expression: key,
      };
    } else {
      return null;
    }
  }
}

初始化事件池

处理完双括号语法,我们还需要处理@click这样的事件语法,首先,我们创建一个事件池,再定义两个正则分别匹配函数

const eventPool = new Map();
// 匹配函数名
const regStringFn = /(.+?)\((.+?)\)/;
// 匹配函数参数
const regString = /\'(.+?)\'/;

同样的,我们也需要遍历所有节点

const allNodes = $node.querySelectorAll('*');
allNodes.forEach((node) => {
  const vClickVal = node.getAttribute(`@click`);
  if (vClickVal) {
    /*
      比如@click='plus(1)',解析完成的fnInfo就是
      fnInfo:{
        args: [1]
        methodName: "plus"
      }
      */
    const fnInfo = checkFunctionHasArgs(vClickVal);
    const handler = fnInfo
      ? //有参函数传入args
        methods[fnInfo.methodName].bind(vm, ...fnInfo.args)
      : //无参函数直接绑定
        methods[vClickVal].bind(vm);

    //存入事件池,节点为key,事件为value
    eventPool.set(node, {
      type: vClick,
      handler,
    });
    //删除dom上的attr,不然浏览器查看源代码就会显示自定义事件  这样不好
    node.removeAttribute(`@${vClick}`);
  }
});

function checkFunctionHasArgs(str) {
  const matched = str.match(regStringFn);

  if (matched) {
    const argArr = matched[2].split(',');
    const args = checkIsString(matched[2])
      ? argArr // ['1']
      : argArr.map((item) => Number(item));

    return {
      methodName: matched[1],
      args,
    };
  }
}
function checkIsString(str) {
  return str.match(regString);
}

这样,我们有拥有了节点数据池和事件池,接下来我们就要拿节点数据池和事件池做操作了

绑定事件处理

有了事件池,我们就要把事件池中的事件绑定到 dom 元素上去,让事件能够触发。这步其实是很容易的,因为我们把 vue 事件加入事件池中时,key 是 dom 元素value 是事件处理函数,只要把他们两个互相绑定就行

function (vm) {
  //node:key  info:value
  for (let [node, info] of eventPool) {
        // type:事件类型  handler:事件处理函数
    let { type, handler } = info;
    //在vue中,是用this.function 去访问方法,所以方法要被绑定到vm上
    vm[handler.name] = handler;
    //给节点绑定事件处理函数
    node.addEventListener(type, vm[handler.name], false);
  }
}

render 页面

执行完上面的内容,我们就到了最后一步 render 页面了,我们只要把节点数据池中的节点内容渲染到浏览器

function render(vm) {
  exprPool.forEach((info, node) => {
    _render(vm, node, info);
  });
}
function _render(vm, node, info) {
  //info:{key: 'count',expression 'count + 1'}
  const { expression } = info;
  //expression是一个字符串,为了执行字符串,所以我们需要new Function
  const r = new Function(
    'vm',
    'node',
    `
    with (vm) {
      node.textContent = ${expression};
    }
  `
  );

  r(vm, node);
}

在这里,我们先解决两个问题

  • with 是干啥用的?
  • 为什么_render 要抽离出来?

首先先来介绍下 with

with 的作用是用来改变标识符的查找优先级,优先从 with 指定对象的属性中查找。e.g:

var a = 1;
var obj = {
  a: 2,
};
with (obj) {
  console.log(a); //2
}

那为什么_render 要单独抽成一个函数? 因为在前面的 data 响应式处理 中,set被触发时,我们需要拿到新的数据值去update页面元素,这时候就也会用到render函数,那就简单实现下上面提到的updata

export function update(vm, key) {
  //在节点数据池中查找哪个节点的key==当前改变的key,找到则重新render
  exprPool.forEach((info, node) => {
    if (info.key === key) {
      _render(vm, node, info);
    }
  });
}

到此为止,就能实现一个完整的不通过任何第三方插件解析 vue 文件,并实现简单的响应式处理了!!

到此这篇关于实现一个vue文件解析器的文章就介绍到这了,更多相关vue文件解析器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue3 AST解析器-源码解析

    目录 1.生成 AST 抽象语法树 2.创建 AST 的根节点 3.解析子节点 4.解析模板元素 Element 5.示例:模板元素解析 上一篇文章Vue3 编译流程-源码解析中,我们从 packges/vue/src/index.ts 的入口开始,了解了一个 Vue 对象的编译流程,在文中我们提到 baseCompile 函数在执行过程中会生成 AST 抽象语法树,毫无疑问这是很关键的一步,因为只有拿到生成的 AST 我们才能遍历 AST 的节点进行 transform 转换操作,比如解析 v

  • vue parseHTML 函数源码解析AST基本形成

    目录 AST(抽象语法树)? 子节点 Vue中是如何把html(template)字符串编译解析成AST 解析html 代码重新改造 接着解析 html (template)字符串 解析div AST(抽象语法树)? 在上篇文章中我们已经把整个词法分析的解析过程分析完毕了. 例如有html(template)字符串: <div id="app"> <p>{{ message }}</p> </div> 产出如下: { attrs: [&q

  • vue parseHTML函数解析器遇到结束标签

    目录 引言 match函数匹配正则endTag 关键 parseEndTag 函数代码 总结parseEndTag 函数作用 handleStartTag函数后续 最后更新 stack 栈以及 lastTag 引言 承接上篇 parseHTML 函数源码解析拿到返回值后的处理 接下来我们将会讲解当 textEnd === 0 解析器遇到结束标签,parse 结束标签的代码如下: // End tag: var endTagMatch = html.match(endTag); if (endTa

  • vue parseHTML源码解析hars end comment钩子函数

    目录 引言 chars源码: parseText end 源码: 引言 接上文  parseHTML 函数源码解析(六) start钩子函数 接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理. parseHTML(template, { chars: function(){ //... }, //... }) chars源码: chars: function chars(text) { if (!currentParent) { {

  • 从零实现一个vue文件解析器

    如何从 0 处理一个 vue 文件并实现简单的响应式? 在现在的前端工程化中,打包工具是不可或缺的,其中webpack无疑是占据了主导地位,当然也有尤大搞的vite,但是论生态和使用人数,至少在目前webpack还是更胜一筹. 打包工具能帮助我们打包前端文件,在webpack中,不同后缀的文件通过不同loader来处理. 本文就讨论下怎么实现一个处理.vue文件的loader,以及用loader处理完.vue文件怎么把内容渲染在浏览器上,并实现简单的响应式. 源码地址 gezhicui/vue-

  • 带你一步步从零搭建一个Vue项目

    目录 一.项目创建 1.打开命令行窗口Cd /d进入想要创建项目的位置,输入vue create 项目名 2.选择Vue2 3.运行该项目 4.创建成功 二.路由的配置 1.安装路由(vue2 只能安装3版本的vue-router) 2.配置路由 三.API接口配置 1.安装axios 2.axios的二次封装 3.跨域问题 四.Vuex 总结 一.项目创建 1.打开命令行窗口Cd /d进入想要创建项目的位置,输入vue create 项目名 2.选择Vue2 3.运行该项目 4.创建成功 在浏

  • 浅谈vue中.vue文件解析流程

    我们平时写的 .vue 文件称为 SFC(Single File Components),本文介绍将 SFC 解析为 descriptor 这一过程在 vue 中是如何执行的. vue 提供了一个 compiler.parseComponent(file, [options]) 方法,来将 .vue 文件解析成一个 descriptor: // an object format describing a single-file component. declare type SFCDescrip

  • Android :okhttp+Springmvc文件解析器实现android向服务器上传照片

    A.前言:为了解决安卓端向服务器上传照片的问题 1.获得相册权限,选取照片,取到照片的url 2.使用okhttp访问服务器并向服务器传照片 3.配置springmvc文件解析器 4.搭建服务器,获取数据保存照片 B.Android添加一个按钮和一个ImageView,设置它的点击事件,打开相册选择照片,解析得到照片的本机url,并把照片显示到ImageView里 添加权限: <uses-permission android:name="android.permission.INTERNE

  • 使用70行Python代码实现一个递归下降解析器的教程

     第一步:标记化 处理表达式的第一步就是将其转化为包含一个个独立符号的列表.这一步很简单,且不是本文的重点,因此在此处我省略了很多. 首先,我定义了一些标记(数字不在此中,它们是默认的标记)和一个标记类型: token_map = {'+':'ADD', '-':'ADD', '*':'MUL', '/':'MUL', '(':'LPAR', ')':'RPAR'} Token = namedtuple('Token', ['name', 'value']) 下面就是我用来标记 `expr` 表

  • 基于PyQt5制作一个PDF文件合并器

    操作说明:选择多个PDF文件,执行完合并后会生成一个新的PDF文件,这个新的PDF文件包含所有源PDF文件的页面. 将相关的三方模块导入到代码块中... from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * import sys import os import PyPDF2 # PDF操作库 QThread是PyQt5的子线程应用,之前已经使用过比较多的次数了.一般使用时通过创建一个

  • 封装一个Vue文件上传组件案例详情

    目录 前言 1. 子组件 2 父组件使用 3.效果 4.总结 前言 在面向特定用户的项目中,引 其他ui组件库导致打包体积过大,首屏加载缓慢,还需要根据UI设计师设计的样式,重写大量的样式覆盖引入的组件库的样式.因此尝试自己封装一个自己的组件,代码参考了好多前辈的文章 1. 子组件 <template> <div class="digital_upload"> <input style="display: none" @change=&

  • 详解.vue文件解析的实现

    vue单文件 vue是现今非常流行的框架之一,整体给人的感觉就是优雅,小巧,最近开始学习着使用该框架做一些项目,学习,当然是从实践开始,在浏览了一遍官方文档之后,便开始用vue-cli脚手架来快速搭建一个vue项目,从实践中快速学习.在看了一遍项目文件结构后,对于.vue结尾的单文件却是有很多不解的地方,具体碰到的问题如下: 什么是<template/>标签 template是html5的一个新元素,主要用于保存客户端中的内容,表现为浏览器解析该内容但不渲染出来,可以将一个模板视为正在被存储以

  • 如何实现一个webpack模块解析器

    最近在学习 webpack源码,由于源码比较复杂,就先梳理了一下整体流程,就参考官网的例子,手写一个最基本的 webpack 模块解析器. 代码很少,github地址:手写webpack模块解析器 整体流程分析 1.读取入口文件. 2.将内容转换成 ast 语法树. 3.深度遍历语法树,找到所有的依赖,并加入到一个数组中. 4.将 ast 代码转换回可执行的 js 代码. 5.编写 require 函数,根据入口文件,自动执行完所有的依赖. 6.输出运行结果. createAsset // 读取

  • Java Class 解析器实现方法示例

    最近在写一个私人项目,名字叫做ClassAnalyzer,ClassAnalyzer的目的是能让我们对Java Class文件的设计与结构能够有一个深入的理解.主体框架与基本功能已经完成,还有一些细节功能日后再增加.实际上JDK已经提供了命令行工具javap来反编译Class文件,但本篇文章将阐明我实现解析器的思路. Class文件 作为类或者接口信息的载体,每个Class文件都完整的定义了一个类.为了使Java程序可以"编写一次,处处运行",Java虚拟机规范对Class文件进行了严

随机推荐