从零手写Vue3响应式模块

今天课程是正式课第一天,珠峰架构课正式课上课时间是 周三、周五晚上8-10 周日全天,想报名参加的同学可以联系右下角客服老师微信

课程计划安排

上课时进度可能略有调整

  • 07-18 Vue3开发环境搭建,Vue3 compositionAPI的使用,手写Vue3响应式模块原理 (monorepo环境搭建,rolllup环境打包项目)
  • 07-21 Vue3响应式源码分析及代码调试
  • 07-23 手写Vue3初始化流程实现、虚拟DOM实现
  • 07-25 Vue3中组件实现原理,手写Vue3diff算法实现watchApi异步渲染 、生命周期实现
  • 07-28 vue3中模板编译原理,template编译render函数原理
  • 07-30 手写模板编译 transform 核心逻辑,及codegen原理
  • 08-01 Vue3生态源码剖析Vuex实现原理
  • 08-04 Vue3路由实现原理
  • 08-06 Vue3路由实现原理
  • 08-08 ts课程:类、接口、泛型、兼容性、条件、类型保护、声明文件.....等
  • 08-11 vite开发环境搭建及原理剖析
  • 08-13 vite开发环境搭建及原理剖析
  • 08-15 Vue3 +TS编写组件库,编写常用组件
  • 08-18 组件库的讲解 ,08-20 组件库的讲解

区别介绍

  • 源码采用 monorepo 方式进行管理,将模块拆分到package目录中
  • Vue3 采用ts开发,增强类型检测。 Vue2 则采用flow
  • Vue3的性能优化,支持tree-shaking, 不使用就不会被打包
  • Vue2 后期引入RFC , 使每个版本改动可控 rfcs

文档地址

内部代码优化

  • Vue3 劫持数据采用proxy Vue2 劫持数据采用definePropertydefineProperty有性能问题和缺陷
  • Vue3中对模板编译进行了优化,编译时 生成了Block tree,可以对子节点的动态节点进行收集,可以减少比较,并且采用了 patchFlag 标记动态节点 jsx
  • Vue3 采用compositionApi 进行组织功能,解决反复横跳,优化复用逻辑 (mixin带来的数据来源不清晰、命名冲突等), 相比optionsApi 类型推断更加方便
  • 增加了 Fragment,TeleportSuspense组件

一.Vue3架构分析

1.Monorepo介绍

Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)

  • 一个仓库可维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用,调用都非常方便

缺点:仓库体积会变大。

2.Vue3项目结构

  • reactivity:响应式系统
  • runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
  • runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
  • runtime-test:用于测试
  • server-renderer:用于服务器端渲染
  • compiler-core:与平台无关的编译器核心
  • compiler-dom: 针对浏览器的编译模块
  • compiler-ssr: 针对服务端渲染的编译模块
  • compiler-sfc: 针对单文件解析
  • size-check:用来测试代码体积
  • template-explorer:用于调试编译器输出的开发工具
  • shared:多个包之间共享的内容
  • vue:完整版本,包括运行时和编译器
                            +---------------------+
                            |                     |
                            |  @vue/compiler-sfc  |
                            |                     |
                            +-----+--------+------+
                                  |        |
                                  v        v
               +---------------------+    +----------------------+
               |                     |    |                      |
     +-------->|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
     |         |                     |    |                      |
+----+----+    +---------------------+    +----------------------+
|         |
|   vue   |
|         |
+----+----+   +---------------------+    +----------------------+    +-------------------+
    |         |                     |    |                      |    |                   |
    +-------->|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
              |                     |    |                      |    |                   |
              +---------------------+    +----------------------+    +-------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

3.安装依赖

依赖
typescript 支持typescript
rollup 打包工具
rollup-plugin-typescript2 rollup 和 ts的 桥梁
@rollup/plugin-node-resolve 解析node第三方模块
@rollup/plugin-json 支持引入json
execa 开启子进程方便执行命令
npm install typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa -D
1

4.workspace配置

npm init -y && npx tsc --init
1
{
  "private":true,
  "workspaces":[
    "packages/*"
  ]
}
1
2
3
4
5
6

目录结构配置

C:.
│  package.json        # 配置运行命令 
│  rollup.config.js    # rollup配置文件
│  tsconfig.json       # ts配置文件 更改为esnext
│  yarn.lock
│  
├─packages             # N个repo
│  └─reactivity
│      │  package.json
│      └─src
│          index.ts
│              
└─scripts              # 打包命令
        build.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14

配置模块名称及打包选项

{
  "name": "@vue/reactivity",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "module": "dist/reactivity.esm-bundler.js",
  "author": "",
  "license": "ISC",
  "buildOptions":{
    "name":"VueReactivity",
    "formats":[
      "esm-bundler",
      "cjs",
      "global"
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

创建软链yarn install

二.构建环境搭建

1.对packages下模块进行打包

scripts/build.js

const fs = require('fs');
const execa = require('execa')
// 过滤packages目录下所有模块
const targets = fs.readdirSync('packages').filter(f => {
    if (!fs.statSync(`packages/${f}`).isDirectory()) {
        return false;
    }
    return true;
})
// 开始并行打包
runParallel(targets, build)
async function runParallel(source, iteratorFn) {
    const ret = [];
    for (const item of source) {
        const p = iteratorFn(item)
        ret.push(p);
    }
    return Promise.all(ret);
}
async function build(target) {
    await execa(
        'rollup',
        [
            '-c',
            '--environment',
            `TARGET:${target}`
        ], 
        { stdio: 'inherit' }
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

2.rollup配置

rollup.config.js

import path from 'path';
import ts from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
import resolvePlugin from '@rollup/plugin-node-resolve'
const packagesDir = path.resolve(__dirname, 'packages'); // 获取packages目录


const packageDir = path.resolve(packagesDir, process.env.TARGET); // 获取要打包的目标目录
const name = path.basename(packageDir); // 获取打包的名字

const resolve = p => path.resolve(packageDir, p);
const pkg = require(resolve(`package.json`)) // 获取目标对应的package.json

const packageOptions = pkg.buildOptions; // 打包的选项
const outputConfigs = {
    'esm-bundler': {
        file: resolve(`dist/${name}.esm-bundler.js`), // webpack打包用的
        format: `es`
    },
    'cjs': {
        file: resolve(`dist/${name}.cjs.js`), // node使用的
        format: 'cjs'
    },
    'global': {
        file: resolve(`dist/${name}.global.js`), // 全局的
        format: 'iife'
    }
}

function createConfig(format, output) {
    output.name = packageOptions.name;
    output.sourcemap = true;
    return {
        input: resolve(`src/index.ts`), // 入口
        output,
        plugins:[
            json(),
            ts({
                tsconfig:path.resolve(__dirname,'tsconfig.json')
            }),
            resolvePlugin(),
        ]
    }
}
// 根据模块配置信息选择性打包
export default packageOptions.formats.map(format => createConfig(format, outputConfigs[format]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

3.开发环境打包

const execa = require('execa')
const target = 'reactivity'
execa('rollup', [
        '-wc',
        '--environment',
        `TARGET:${target}`
    ], {
        stdio: 'inherit'
    }
)
1
2
3
4
5
6
7
8
9
10