esbuild基本使用,及简单介绍vue3源码reactivity的打包

初始化环境

创建文件夹,开始试验

shell 复制代码
mkdir esbuild-demo
cd esbuild-demo
pnpm init
code .

生成

json 复制代码
{
  "name": "esbuild-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "zhm",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "0.19.3"
  }
}

配置最简单打包

1. package.json加type

package.json加type是module,可以解析import语法。

json 复制代码
  "main": "index.js",
  "type": "module",

2. 配置packages文件夹

新建packages文件夹,这个文件夹里可以新建n个包,这里新建add的包,然后建一个index.ts

ts 复制代码
// packages/add/index.ts
export function add(a: number, b: number) {
  return a + b
}

3. 配置scripts文件夹

新建scripts文件夹,这个文件夹里主要是,打包脚本,这里新建dev.js

先安装esbuild

shell 复制代码
pnpm install --save-exact --save-dev esbuild

dev.js内容如下:

js 复制代码
import esbuild from 'esbuild';

let ctx = await esbuild.context({
  // 入口
  entryPoints: ['./packages/add/index.ts'],
  // 输出文件
  outfile: './packages/add/dist/add.js',
})
// 每次entryPoints变动,就自动生成新的文件
await ctx.watch()

4. 配置命令

package.json里配置命令即可

json 复制代码
"scripts": {
    "dev": "node ./scripts/dev.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

然后运行

shell 复制代码
pnpm run dev

5.稍微重构下文件目录

正常一个包,通常的结构是index.js package.json src/index.ts src/...

生成package.json

js 复制代码
cd packages/add
pnpm init

其他文件调整:

额外注意dev.js里,需要配置bundle:true,这样依赖文件会打包到一个文件中

js 复制代码
let ctx = await esbuild.context({
  // ...
  // bundle是将所有的依赖打包到一个文件中
  bundle:true,
})

不然打包出来的结果就是export { add } from './add';

说说format

format有三种

  • iife ----- 自执行函数cjs是commonjs,esm是es6模块,xx.global.js
  • cjs ----- commonjs(require module.exports),xx.cjs.js
  • esm ----- es6模块(import export),xx.esm.js

打包vue的reactivity

安装插件@types/node和minimist

shell 复制代码
# 可以这么写,import { resolve } from 'node:path'
pnpm i -D @types/node
# 可以解析参数
pnpm i -D minimist

解析参数的多说说

如果运行下面的命令:

shell 复制代码
node ./scripts/dev.js a b c d -minify --format=iife`

process.argv获取,是这样的:

shell 复制代码
[
  '/usr/local/bin/node',
  '/Users/zhm/.../scripts/dev.js',
  'a',
  'b',
  'c',
  'd',
  '-minify',
  '--format=iife'
]

minimist(process.argv)获取,是这样的:

shell 复制代码
{
  _: [
    '/usr/local/bin/node',
    '/Users/zhm/.../scripts/dev.js',
    'a',
    'b',
    'c',
    'd'
  ],
  minify: true,
  format: 'iife'
}

minimist会将命令行里---单独用一个键值表示。对于node命令来说,前两个基本都是node xx.ts,所以一般取后面的minimist(process.argv.slice(2))

简单reactivity的打包

略微简化版的vue源码中的dev.js

js 复制代码
import esbuild from 'esbuild'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import minimist from 'minimist'

const args = minimist(process.argv.slice(2))
const target = args._[0] || 'reactivity'
const format = args.f || 'global'

const require = createRequire(import.meta.url) // import.meta.url当前文件路径
const pkg = require(`../packages/${target}/package.json`) // pkg就是当前文件夹下的package.json的对象


const outputFormat = format.startsWith('global')
  ? 'iife'
  : format === 'cjs'
  ? 'cjs'
  : 'esm'

const postfix = format.endsWith('-runtime')
  ? `runtime.${format.replace(/-runtime$/, '')}`
  : format

const __dirname = dirname(fileURLToPath(import.meta.url)) // __dirname当前文件的文件夹路径

const outfile = resolve(
  __dirname,
  `../packages/${target}/dist/${
    target === 'vue-compat' ? `vue` : target
  }.${postfix}.js`
)

esbuild
  .context({
    entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
    outfile,
    bundle: true,
    sourcemap: true,
    format: outputFormat,
    globalName: pkg.buildOptions?.name || target,
    platform: format === 'cjs' ? 'node' : 'browser',
    // define是将代码里这些key替换成value
    define: {
      __VERSION__: `"${pkg.version}"`,
      __DEV__: `true`,
    }
  })
  .then(ctx => ctx.watch())

reactivity的目录结构如下:

运行node ./scripts/dev.js之后,在dist文件,就已经生成了。

新建个index.html,输入以下内容,浏览器正常运行

html 复制代码
<script src="./reactivity.global.js"> </script>
  <script>
    const { reactive, effect } = VueReactivity
    const state = reactive({ count: 0 })
    effect(() => {
      console.log(state.count)
    })
    const timer = setInterval(() => {
      state.count++
      if(state.count>6){
        clearInterval(timer)
      }
    }, 1000)
  </script>

如果执行,node ./scripts/dev.js reactivity -f=esm,就会生成reactivity.esm.js,index.html内容也可以换成module写法:

html 复制代码
<script type="module">
  import { reactive, effect } from './reactivity.esm.js'
  const state = reactive({ count: 0 })
  // ...
  </script>

同样,如果执行node ./scripts/dev.js add也是没问题的!

reactivity其他文件的代码

package.json

json 复制代码
{
  "name": "@vue/reactivity",
  "version": "3.3.4",
  "description": "@vue/reactivity",
  "main": "index.js",
  "module": "dist/reactivity.esm-bundler.js",
  "types": "dist/reactivity.d.ts",
  "unpkg": "dist/reactivity.global.js",
  "jsdelivr": "dist/reactivity.global.js",
  "files": [
    "index.js",
    "dist"
  ],
  "sideEffects": false,
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vuejs/core.git",
    "directory": "packages/reactivity"
  },
  "buildOptions": {
    "name": "VueReactivity",
    "formats": [
      "esm-bundler",
      "esm-browser",
      "cjs",
      "global"
    ]
  },
  "keywords": [
    "vue"
  ],
  "author": "Evan You",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/vuejs/core/issues"
  },
  "homepage": "https://github.com/vuejs/core/tree/main/packages/reactivity#readme",
  "dependencies": {
    "@vue/shared": "3.3.4"
  }
}

src/index.ts

js 复制代码
export * from './reactive'
export * from './effect'

src/effect.ts

js 复制代码
// track的时候,需要拿到effect,所以用下全局变量存放effect
export let activeEffect: ReactiveEffect | null = null;
// 建立类,方便存放fn,和运行
/**
 * fn是函数,收集属性依赖,scheduler是函数,属性依赖变化的时候,执行
 * 属性deps是个二维数组,结构是 [[_effect1,_effect2],[_effect3,_effect2],]
 */
export class ReactiveEffect {
  // 是否主动执行
  private active = true
  // 新增deps
  deps = []
  parent
  constructor(private fn, public scheduler) {
  }

  run() {
    if (!this.active) {
      const res = this.fn()
      // 这里watch的时候,fn是函数返回字段,需要返回值
      return res;
    }

    this.parent = activeEffect
    activeEffect = this;
    // 运行之前,清除依赖
    clearupEffect(this);
    const res = this.fn();
    activeEffect = this.parent
    this.parent && (this.parent = null);
    return res
  }
  stop() {
    if (this.active) {
      // 清除依赖
      clearupEffect(this);
      // 标记不主动执行
      this.active = false;

    }
  }



}

// 清除依賴
function clearupEffect(_effect) {
  // deps结构是 [[_effect1,_effect2],[_effect3,_effect2],],假设去掉_effect2
  const deps = _effect.deps
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(_effect)
  }
  // 同时deps置空,保证每次effect运行都是新的属性映射
  _effect.deps.length = 0
}



// }
export function effect(fn, options) {
  const _effect = new ReactiveEffect(fn, options?.scheduler);
  _effect.run();
  // runner是个函数,等同于_effect.run,注意绑定this
  const runner = _effect.run.bind(_effect)
  // runner还有effect属性,直接赋值就好
  runner.effect = _effect
  return runner
}

// 本质是找到属性对应的effect,但属性存在于对象里,所以两层映射
// 响应性对象 和 effect的映射,对象属性和effect的映射
// targetMap = { obj:{name:[effect],age:[effect]} }
export const targetMap: WeakMap<object, Map<string, Set<ReactiveEffect>>> = new WeakMap();

// 让属性 订阅 和自己相关的effect,建立映射关系
export function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  trackEffects(dep)
}

/**
 * dep收集effect
 */
export function trackEffects(dep: Set<ReactiveEffect>) {
  if (activeEffect && !dep.has(activeEffect)) {
    // 收集effect
    dep.add(activeEffect)
    // effect同样收集下dep
    // @ts-ignore
    activeEffect?.deps?.push(dep)
  }
}
/**
 * dep执行触发effect
 */
export function triggerEffects(dep: Set<ReactiveEffect>) {
  [...dep].forEach((effect) => {
    const isRunning = activeEffect === effect
    if (!isRunning) {
      effect.scheduler ? effect.scheduler() : effect.run()
    }
  });
}

// 属性值变化的时候,让相应的effect执行
export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (!dep) {
    return;
  }
  // 触发执行
  triggerEffects(dep)
}

src/reactive.ts

js 复制代码
// import { isObject } from './shared'
import { track, trigger } from './effect';
export const isObject = (param) => {
  return typeof param === 'object' && param !== null
}
export const isFunction = (param) => {
  return typeof param === 'function';
}
const __v_isReactive = '__v_isReactive'
// 是不是响应式对象
export const isReactive = (param) => param[__v_isReactive];

// 代理对象的映射
export const reactiveMap = new WeakMap()

export function reactive(target) {
  // 如果不是对象,直接返回
  if (!isObject(target)) {
    return
  }

  // 如果已经代理过了,直接返回
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target)
  }

  // 如果已经代理过了,__v_isReactive肯定是true,那直接返回
  if (target[__v_isReactive]) {
    return target
  }
  // 如果是ref对象,直接返回value
  if (target.__v_isRef) {
    return target.value
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 这里埋点,加上__v_isReactive属性,标识已经代理过了
      if (key === __v_isReactive) {
        return true
      }
      // Reflect将target的get方法里的this指向proxy上,也就是receiver
      const res = Reflect.get(target, key, receiver);
      // 依赖收集
      track(target, key)
      // 如果是对象,递归代理
      if(isObject(res)) {
        return reactive(res)
      }
      return res;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const r = Reflect.set(target, key, value, receiver);
      // 响应式对象发生变化的时候,触发effect执行
      if(oldValue !== value) {
        trigger(target, key)
      }
      return r;
    },
  })
  // 如果没有代理过,缓存映射
  reactiveMap.set(target, proxy)
  return proxy
}

github示例

相关推荐
时光少年16 分钟前
Android 视频分屏性能优化——GLContext共享
前端
IT_陈寒34 分钟前
JavaScript开发者必知的5个性能杀手,你踩了几个坑?
前端·人工智能·后端
跟着珅聪学java37 分钟前
Electron 精美菜单设计
运维·前端·数据库
日光倾37 分钟前
【Vue.js 入门笔记】闭包和对象引用
前端·vue.js·笔记
EstherNi39 分钟前
左右两侧定位的效果,vue3
javascript·vue.js
一只程序熊44 分钟前
UniappX 未找到 “video“ 组件,已自动当做 “view“ 组件处理。请确保代码正确,或重新生成自定义基座后再试。
前端
林小帅1 小时前
【笔记】xxx 技术分享文档模板
前端
雾岛心情1 小时前
【HTML&CSS】HTML为文字添加格式和内容
前端·css·html
心.c1 小时前
如何在项目中减少 XSS 攻击
前端·xss
Rsun045511 小时前
Vue相关面试题
前端·javascript·vue.js