如何从 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文件解析器内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

从零实现一个vue文件解析器的更多相关文章

  1. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  2. vue自定义加载指令v-loading占位图指令v-showimg

    这篇文章主要为大家介绍了vue自定义加载指令和v-loading占位图指令v-showimg的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  3. vue使用动画实现滚动表格效果

    这篇文章主要为大家详细介绍了vue使用动画实现滚动表格效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  4. 关于Vue 监控数组的问题

    这篇文章主要介绍了Vue 监控数组的示例,主要包括Vue 是如何追踪数据发生变化,Vue 如何更新数组以及为什么有些数组的数据变更不能被 Vue 监测到,对vue监控数组知识是面试比较常见的问题,感兴趣的朋友一起看看吧

  5. Vue子组件props从父组件接收数据并存入data

    这篇文章主要介绍了Vue子组件props从父组件接收数据并存入data的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  6. PHP实现文件安全下载

    例如你希望客户要填完一份表格,才可以下载某一文件,你第一个想法一定是用"Redirect"的方法,先检查表格是否已经填写完毕和完整,然后就将网址指到该文件,这样客户才能下载,但如果你想做一个关于"网上购物"的电子商务网站,考虑安全问题,你不想用户直接复制网址下载该文件,笔者建议你使用PHP直接读取该实际文件然后下载的方法去做。feof){echofread;}fclose;}这样就可以用PHP直接输出文件了。

  7. Vue h函数的使用详解

    本文主要介绍了Vue h函数的使用详解,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  8. VUE响应式原理的实现详解

    这篇文章主要为大家详细介绍了VUE响应式原理的实现,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助

  9. vue+Element ui实现照片墙效果

    这篇文章主要为大家详细介绍了vue+Element ui实现照片墙效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. vue+elemet实现表格手动合并行列

    这篇文章主要为大家详细介绍了vue+elemet实现表格手动合并行列,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部