如何自己实现一个mini-vue功能

293 阅读2分钟

如何实现一个简易版本的vue

前言

这是在下第一次在掘金上写技术博客,还是有点紧张,如有不足之处还请各位大拿指正,我会及时改正,也感谢各位小伙伴能够阅读鄙人的陋文

功能简述

支持.vue语法糖,支持script与template标签,以及双大括号语法,v-if/v-show指令和@事件处理。未来我或许会新增style支持,生命周期回调以及其他相关语法糖

我的思路

1.使用自定义vue-loader讲vue文件解析为js文件 2.引入js文件作为对象传递给手写的vue进行解析 3.vue拿到对象后进行初始化,将数据放入Vue实例 4.将data通过proxy进行数据双向绑定 5.将temlpate作为dom树的方式进行预处理 6.将dom树与methods和data进行依赖绑定 7.根据依赖处理vue语法的指令 7.渲染成真实的浏览器dom树 8.挂载到真实dom中 9.在更新数据时触发update方法,将具有变更的数据重新处理

正文

源码参考

项目搭建

  • 初始化项目 打开vscode在终端中使用npm init -y初始化package.json文件
npm init -y
  • 安装项目依赖 安装项目依赖时候需可能会出现webpack相关版本兼容问题,大家可以按照我pakage.json配置的版本进行安装
// 我的package.json文件配置
{
  "name": "mini-vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2",
    "html-webpack-plugin": "^5.5.0",
    "webpack-dev-server": "^4.7.4"
  },
  "dependencies": {

  }
}
npm install -D webpack webpack-cli html-webpack-plugin webpack-dev-server
  • 配置webpack
const { resolve } = require('path')
const WebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        filename: 'bound.js',
        path: resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /.vue$/,
            // 这里因为我们是手写loader,需要配置loader位置
                use: resolve(__dirname, 'vue-loader') 
            }

        ]
    },
    devtool: 'source-map',
    plugins: [
        new WebpackPlugin()
    ]
}
  • 建立文件目录
- src
    - index.js // 项目入口
    - test.vue // 用于测试的vue文件
- vue // 用于存放自己写的vue
    - index.js
- vue-loader // 存放自己写的webpack
    - index.js

vue-loader编写

  • 补全scr目录 在编写之前,我们需要把src内的index.js文件进行补全便于测试
// src/index.js
import test from './test.vue';
console.log(test)
  • 编写loader入口文件 因为loader是运行在nodejs环境下,所以我们需要使用commonjs规范进行编写loader
// vue-loader/index.js
function vueLoader(source) {
    console.log(source)
}
module.exports = vueLoader;

这时候我们补全package.json的脚本 在scripts下加入dev脚本

// package.json
  "scripts": {
    "dev": "webpack-dev-server"
  },

补全之后,我们可以调试一下,使用npm run dev,如果在脚本下打印了.vue脚本,说明我们的loader成功啦

  • 关于loeader的补充 loader原理,loader其实是webpack给我们提供的一个可以处理js之外其他不支持js的脚本,它应该是一个函数,其传参就是我们需要处理的脚本文件,所以这时我们能通过source打印出相应的文件.像我们最终的目的是需要将.vue文件最终解析为大致如下的结构:
// 解析之后的.vue文件大致如下
{
    template: ...,
    data: ...,
    methods: ...
}
  • 解析source 那么我们在解析的时候需要提取tempalte和script的内容,然后将template的内部注入script作为一个新的对象导出 。
  • 首先我们使用正则表达式提取template和script内容,新建一个vue-loader/pare.js文件
// 解析原文件
function parse(source) {
    const template = getTemplate(source)
    const script = getScript(source)
    return { template, script }
}
// 获取template的内容
function getTemplate(source) {
    const reg = /(?<=<template>)([\s\S]+?)(?=<\/template>)/g;
    return source.match(reg)?.[0]
}
// 获取script的内容
function getScript(source) {
    const reg = /(?<=<script>)([\s\S]+?)(?=<\/script>)/g;
    return source.match(reg)?.[0]
}
// 导出parse
module.exports = {
    parse
}

这时我们在vue-loader/index.js文件中导入该文本,并测试

const { parse, mergeTemplateToScript } = require('./parse')

function vueLoader(source) {
    const { template, script } = parse(source);
    console.log(template, script)
}

module.exports = vueLoader;

这时,可以通过npm run dev重新跑一下项目,如果看到tempalte和script的内容被打印了,恭喜你,第一步成功啦!!!

  • 将template内容注入script 这时我们只需要完成最后一步把template的文件注入到script并导出该内容.我们在vue-loader/parse.js文件中新写一个注入的方法
// 将templatre 合并到script
// 这里的正则大致意思是匹配export default 后面的第一个花括号 {
function mergeTemplateToScript(temp, script) {
    return script.replace(/(?<=export\s+default\s+)(\{)/, () => {
        return `
            {
                template: \`${temp}\`,
        `
    })
}

最终vue-loader目录下的文件如下

// vue-loader/index.js
const { parse, mergeTemplateToScript } = require('./parse')

function vueLoader(source) {
    const { template, script } = parse(source);
    return mergeTemplateToScript(template, script)
}

module.exports = vueLoader;


// vue-loader/parse.js
function parse(source) {
    const template = getTemplate(source)
    const script = getScript(source)
    return { template, script }
}

function getTemplate(source) {
    const reg = /(?<=<template>)([\s\S]+?)(?=<\/template>)/g;
    return source.match(reg)?.[0]
}

function getScript(source) {
    const reg = /(?<=<script>)([\s\S]+?)(?=<\/script>)/g;
    return source.match(reg)?.[0]
}

// 将templatre 合并到script
function mergeTemplateToScript(temp, script) {
    return script.replace(/(?<=export\s+default\s+)(\{)/, () => {
    // 这里我们是
        return `
            {
                template: \`${temp}\`,
        `
    })
}

module.exports = {
    parse,
    mergeTemplateToScript
}

这时,npm run dev,打开浏览器http://localhost:8080/ 如果能看到对象被的打印,那么恭喜你,你成功地完成了vue-loader的编写。

编写vue

接下来,我们开始写vue的内容

初始化vue

在vue/index.js中,我们开始编写Vue构造函数

// vue/index.js
// 导入init函数
import init from './init'

function Vue(component) {
    this.init(component)
}
// 将初始化的init方法挂载到Vue原型上
Vue.prototype.init = init

// 因为我们可能也是通过createApp进行创建,所以同时也可以把creatApp方法写上
export function creatApp(component) {
    return new Vue(component)
}
// 导出Vue
export default Vue
  • 编写vue/init.js 在初始化时,我们分步骤进行初始化
  • 将传入的component挂载到势例
  • 处理响应式的data
  • 注册dom
  • 将props与dom的依赖绑定
  • 渲染dom
import { render } from "./initRender";

function init(component) {
    // this 容易出现混淆 所以用vm便于认清实列
    const vm = this
    // 将数据放入this实例
    const { data, methods, template } = component;
    vm.$methods = methods;
    vm.$template = template;
    // 处理响应式数据
    vm.initData(vm, data);
    // 注册dom
    vm.initDom()
    // 注册vue数据与真实dom依赖
    vm.initProps()
    // 渲染
    render(vm)
}

export default init
  • 编写响应式数据