调试环境搭建
-
迁出Vue3源码: git clone github.com/vuejs/vue-n…
-
安装依赖: yarn --ignore-scripts
-
⽣成sourcemap⽂件, package.json
"dev": "node scripts/dev.js --sourcemap" -
编译: yarn dev
打包执行流程
graph TD
package.json --> dev.js --> rollup.config.js
dev.js
const execa = require('execa')
const { fuzzyMatchTarget } = require('./utils')
const args = require('minimist')(process.argv.slice(2))
// 默认使用vue文件夹,目标文件夹
const target = args._.length ? fuzzyMatchTarget(args._)[0] : 'vue'
// 格式 设置支持的方式 commonJS umd
const formats = args.formats || args.f
// 源码映射
const sourceMap = args.sourcemap || args.s
const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7)
execa(
'rollup',
[
'-wc',
'--environment',
[
`COMMIT:${commit}`,
`TARGET:${target}`,
`FORMATS:${formats || 'global'}`,
sourceMap ? `SOURCE_MAP:true` : ``
]
.filter(Boolean)
.join(',')
],
{
stdio: 'inherit'
}
)
rollup.config.js
import path from 'path'
import ts from 'rollup-plugin-typescript2'
import replace from '@rollup/plugin-replace'
import json from '@rollup/plugin-json'
if (!process.env.TARGET) {
throw new Error('TARGET package must be specified via --environment flag.')
}
const masterVersion = require('./package.json').version
// 包目录
const packagesDir = path.resolve(__dirname, 'packages')
// 子模块目录,默认vue
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`))
const packageOptions = pkg.buildOptions || {}
// ensure TS checks only once for each build
let hasTSChecked = false
const outputConfigs = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
format: `es`
},
'esm-browser': {
file: resolve(`dist/${name}.esm-browser.js`),
format: `es`
},
cjs: { //node.js 方式
file: resolve(`dist/${name}.cjs.js`),
format: `cjs`
},
global: {//立即执行方式 默认使用的方式
file: resolve(`dist/${name}.global.js`),
format: `iife`
},
// runtime-only builds, for main "vue" package only
'esm-bundler-runtime': {
file: resolve(`dist/${name}.runtime.esm-bundler.js`),
format: `es`
},
'esm-browser-runtime': { //
file: resolve(`dist/${name}.runtime.esm-browser.js`),
format: 'es'
},
'global-runtime': {
file: resolve(`dist/${name}.runtime.global.js`),
format: 'iife'
}
}
const defaultFormats = ['esm-bundler', 'cjs']
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',')
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
const packageConfigs = process.env.PROD_ONLY
? []
: packageFormats.map(format => createConfig(format, outputConfigs[format]))
if (process.env.NODE_ENV === 'production') {
packageFormats.forEach(format => {
if (packageOptions.prod === false) {
return
}
if (format === 'cjs') {
packageConfigs.push(createProductionConfig(format))
}
if (/^(global|esm-browser)(-runtime)?/.test(format)) {
packageConfigs.push(createMinifiedConfig(format))
}
})
}
export default packageConfigs
function createConfig(format, output, plugins = []) {
if (!output) {
console.log(require('chalk').yellow(`invalid format: "${format}"`))
process.exit(1)
}
// 生成源码对应的映射配置
output.sourcemap = !!process.env.SOURCE_MAP
output.externalLiveBindings = false
const isProductionBuild =
process.env.__DEV__ === 'false' || /\.prod\.js$/.test(output.file)
const isBundlerESMBuild = /esm-bundler/.test(format)
const isBrowserESMBuild = /esm-browser/.test(format)
const isNodeBuild = format === 'cjs'
const isGlobalBuild = /global/.test(format)
if (isGlobalBuild) {
output.name = packageOptions.name
}
const shouldEmitDeclarations = process.env.TYPES != null && !hasTSChecked
// ts映射支持
const tsPlugin = ts({
check: process.env.NODE_ENV === 'production' && !hasTSChecked,
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
tsconfigOverride: {
compilerOptions: {
sourceMap: output.sourcemap,
declaration: shouldEmitDeclarations,
declarationMap: shouldEmitDeclarations
},
exclude: ['**/__tests__', 'test-dts']
}
})
// we only need to check TS and generate declarations once for each build.
// it also seems to run into weird issues when checking multiple times
// during a single build.
hasTSChecked = true
// 入口文件
const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`
const external =
isGlobalBuild || isBrowserESMBuild
? packageOptions.enableNonBrowserBranches
? // externalize postcss for @vue/compiler-sfc
// because @rollup/plugin-commonjs cannot bundle it properly
['postcss']
: // normal browser builds - non-browser only imports are tree-shaken,
// they are only listed here to suppress warnings.
['source-map', '@babel/parser', 'estree-walker']
: // Node / esm-bundler builds. Externalize everything.
[
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
...['path', 'url'] // for @vue/compiler-sfc
]
// the browser builds of @vue/compiler-sfc requires postcss to be available
// as a global (e.g. http://wzrd.in/standalone/postcss)
output.globals = {
postcss: 'postcss'
}
const nodePlugins =
packageOptions.enableNonBrowserBranches && format !== 'cjs'
? [
require('@rollup/plugin-node-resolve').nodeResolve({
preferBuiltins: true
}),
require('@rollup/plugin-commonjs')({
sourceMap: false
}),
require('rollup-plugin-node-builtins')(),
require('rollup-plugin-node-globals')()
]
: []
//这里是主要配置信息
return {
input: resolve(entryFile),
// Global and Browser ESM builds inlines everything so that they can be
// used alone.
external,
plugins: [
json({
namedExports: false
}),
tsPlugin,
createReplacePlugin(
isProductionBuild,
isBundlerESMBuild,
isBrowserESMBuild,
// isBrowserBuild?
(isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
!packageOptions.enableNonBrowserBranches,
isGlobalBuild,
isNodeBuild
),
...nodePlugins,
...plugins
],
output,
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
},
treeshake: {
moduleSideEffects: false
}
}
}
function createProductionConfig(format) {
return createConfig(format, {
file: resolve(`dist/${name}.${format}.prod.js`),
format: outputConfigs[format].format
})
}
function createMinifiedConfig(format) {
const { terser } = require('rollup-plugin-terser')
return createConfig(
format,
{
file: outputConfigs[format].file.replace(/\.js$/, '.prod.js'),
format: outputConfigs[format].format
},
[
terser({
module: /^esm/.test(format),
compress: {
ecma: 2015,
pure_getters: true
}
})
]
)
}
⽣成结果:
- packages\vue\dist\vue.global.js
- packages\vue\dist\vue.global.js.map 运行 yarn serve
源码分析
编译器和运⾏时环境。
编译器
- compiler-core 核⼼编译逻辑
- compiler-dom 针对浏览器平台编译逻辑
- compiler-sfc 针对单⽂件组件编译逻辑
- compiler-ssr 针对服务端渲染编译逻辑
运⾏时环境
- runtime-core 运⾏时核⼼
- runtime-dom 运⾏时针对浏览器的逻辑
- runtime-test 浏览器外完成测试环境仿真
- reactivity 响应式逻辑
- template-explorer 模板浏览器
- vue 代码⼊⼝,整合编译器和运⾏时
- server-renderer 服务器端渲染
- share 公⽤⽅法
测试例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1 @click="onclick">{{msg}}</h1>
<comp></comp>
</div>
<script src="../dist/vue.global.js"></script>
<script>
const {createApp} = Vue
// 实例创建
const app = createApp({
data() {
// 即使是根对象 data也必须是函数
return {
msg: 'hello, vue3'
}
},
methods: {
onclick() {
console.log('click me');
}
},
})
app.component('comp', {
template:'<div>comp</div>',
})
app.mount('#app')
</script>
</body>
</html>
Composition API
是组合API,它是为了实现基于函数的逻辑复⽤机制⽽产⽣的。
实例都是通过工厂函数返回实例化。
优势
- 可以借助tree-shking把无用的代码在打包的时候移除掉。
- 消除了this,只构建一个简单的上下文即可
- 同一个业务代码写在一块,避免了 复杂页面的代码频繁上下滚动问题
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>Composition API</h1>
<div>count: {{ state.count }}</div>
</div>
<script>
const {
createApp,
reactive,
onMounted
} = Vue;
// 声明组件
const App = {
// setup是⼀个新的组件选项,它是组件内使⽤Composition API的⼊⼝
// 调⽤时刻是初始化属性确定后, beforeCreate之前
setup() {
// 响应化:接收⼀个对象,返回⼀个响应式的代理对象
const state = reactive({ count: 0 })
onMounted(() => {
console.log('mounted');
})
//重复的逻辑可以多次注册,实现业务拆分调用
onMounted(() => {
console.log('mounted2');
})
// 返回对象将和渲染函数上下⽂合并
return { state }
}
}
createApp(App).mount('#app')
</script>
</body>
</html>
//输出结果
//mounted
//mounted2
常用api
//计算属性
<div>doubleCount: {{doubleCount}}</div>
const { computed } = Vue;
const App = {
setup() {
const state = reactive({
count: 0,
// computed()返回⼀个不可变的响应式引⽤对象
// 它封装了getter的返回值
doubleCount: computed(() => state.count * 2)
})
}
}
//事件处理
<div @click="add">count: {{ state.count }}</div>
const App = {
setup() {
// setup中声明⼀个add函数
function add() {
state.count++
}
// 传⼊渲染函数上下⽂
return { state, add }
}
}
//侦听器: watch()
const { watch } = Vue;
const App = {
setup() {
// state.count变化cb会执⾏
watch(() => state.count, (val, oldval) => {
console.log('count变了:' + val);
})
}
}
//引⽤对象:单值响应化,主要实现一个业务代码内聚,避免代码上下滚动
<div>counter: {{ counter }}</div>
const { ref } = Vue;
const App = {
setup() {
// 返回响应式的Ref对象
const counter = ref(1)
setTimeout(() => {
// 要修改对象的value
counter.value++
}, 1000);
// 添加counter
return { state, add, counter }
}
}
逻辑组合
//03-logic-composition.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>composition api</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div>
<h1>逻辑组合</h1>
<div id="app"></div>
</div>
<script>
const { createApp, reactive, onMounted, onUnmounted, toRefs } = Vue;
// ⿏标位置侦听
function useMouse() {
// 数据响应化
const state = reactive({ x: 0, y: 0 })
const update = e => {
state.x = e.pageX
state.y = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
// 转换所有key为响应式数据
return toRefs(state)
}
// 事件监测
function useTime() {
const state = reactive({ time: new Date() })
onMounted(() => {
setInterval(() => {
state.time = new Date()
}, 1000)
})
return toRefs(state)
}
// 逻辑组合
const MyComp = {
template: `
<div>x: {{ x }} y: {{ y }}</div>
<p>time: {{ time }}</p>
`,
setup() {
// 使⽤⿏标逻辑
const { x, y } = useMouse()
// 使⽤时间逻辑
const { time } = useTime()
// 返回使⽤
return { x, y, time }
}
}
createApp(MyComp).mount('#app')
</script>
</body>
</html>
对⽐mixins,
- x,y,time来源清晰
- 不会与data、 props等命名冲突
- 可维护性提⾼了:
Vue3响应式原理
Vue2 原理
// 1.对象响应化:遍历每个key,定义getter、 setter
// 2.数组响应化:覆盖数组原型⽅法,额外增加通知逻辑
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(
method => {
arrayProto[method] = function () {
originalProto[method].apply(this, arguments)
notifyUpdate()
}
}
)
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
// 增加数组类型判断,若是数组则覆盖其原型
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
} else {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(obj, key, val) {
observe(val) // 解决嵌套对象问题
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
val = newVal
notifyUpdate()
}
}
})
}
function notifyUpdate() {
console.log('⻚⾯更新!')
}
vue2响应式弊端:
- 响应化过程需要递归遍历,消耗较⼤
- 新加或删除属性⽆法监听
- 数组响应化需要额外实现
- Map、 Set、 Class等⽆法响应式
- 修改语法有限制
Vue3响应式原理剖析
vue3使⽤ES6的Proxy特性来解决这些问题。
优势
运行时才加入代理,不用提前遍历对象构建绑定相关的方法和维护关系
Proxy: 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
//reactivity.js
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
// Reflect⽤于执⾏对象默认操作,更规范、更友好
// Proxy和Object的⽅法Reflect都有对应
// http://es6.ruanyifeng.com/#docs/reflect
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}
//测试代码
const state = reactive({
foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok
嵌套对象响应式
//测试:嵌套对象不能响应
const state = reactive({
bar: { a: 1 }
})
// 设置嵌套对象属性
state.bar.a = 10 // no ok
添加对象类型递归
// 提取帮助⽅法
const isObject = val => val !== null && typeof val === 'object'
function reactive(obj) {
//判断是否对象
if (!isObject(obj)) {
return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
// ...
// 如果是对象需要递归
return isObject(res) ? reactive(res) : res
},
//...
}
}
//避免重复代理
//解决⽅式:将之前代理结果缓存, get时直接使⽤
// Map和WeakMap区别:
// 1.Map:实例的map对象存在,则的key会永远存在,即对象做的key会永远保留。即使设置值为空,除非调用api移除(Map可以拿任何值做key)
// 2.WeakMap:实例的weakmap对象存在,则的把对象作为key,在对应key对象释放时候,对应值会自动释放。(Map只能拿对象做key)
const toProxy = new WeakMap()
const toRaw = new WeakMap() //
function reactive(obj) {
//...
// 查找缓存,避免重复代理
if (toProxy.has(obj)) {// 已代理过的纯对象
return toProxy.get(obj)
}
if (toRaw.has(obj)) { // 就是一个proxy代理对象,无需要proxy
return obj
}
const observed = new Proxy(...)
// 缓存代理结果
toProxy.set(obj, observed)
toRaw.set(observed, obj)
return observed
}
// 测试效果
const state = reactive(data)
console.log(reactive(data) === state)
console.log(reactive(state) === state)
依赖收集
建⽴响应数据key和更新函数之间的对应关系。
//⽤法 设置响应函数
effect(() => console.log(state.foo))
// ⽤户修改关联数据会触发响应函数
state.foo = 'xxx'
实现三个函数:
- effect:将回调函数保存起来备⽤,⽴即执⾏⼀次回调函数触发它⾥⾯⼀些响应数据的getter
- track: getter中调⽤track,把前⾯存储的回调函数和当前target, key之间建⽴映射关系
- trigger: setter中调⽤trigger,把target, key对应的响应函数都执⾏⼀遍
target, key和响应函数映射关系
- ⼤概结构如下所示
- WeakMap Map Set
- {target: {key: [effect1,...]}}
实现
设置响应函数,创建effect函数,这里只有在effect函数里使用的变量才进行响应式处理。
完整代码
// 保存当前活动响应函数作为getter和effect之间桥梁
const effectStack = []
// effect任务:执⾏fn并将其⼊栈
function effect(fn) {
const rxEffect = function () {
// 1.捕获可能的异常
try {
// 2.⼊栈,⽤于后续依赖收集
effectStack.push(rxEffect)
// 3.运⾏fn,触发依赖收集
return fn()
} finally {
// 4.执⾏结束,出栈
effectStack.pop()
}
}
// 默认执⾏⼀次响应函数
rxEffect()
// 返回响应函数
return rxEffect
}
//依赖收集和触发
function reactive(obj) {
if (!isObject(obj)) {
return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
trigger(target, key)
return res
}
})
return observed
}
// 映射关系表,结构⼤致如下:
// {target: {key: [fn1,fn2]}}
let targetMap = new WeakMap()
function track(target, key) {
// 从栈中取出响应函数
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 获取target对应依赖表
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取key对应的响应函数集
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 将响应函数加⼊到对应集合
if (!deps.has(effect)) {
deps.add(effect)
}
}
}
// 触发target.key对应响应函数
function trigger(target, key) {
// 获取依赖表
const depsMap = targetMap.get(target)
if (depsMap) {
// 获取响应函数集合
const deps = depsMap.get(key)
if (deps) {
// 执⾏所有响应函数
deps.forEach(effect => {
effect()
})
}
}
}
//测试代码
const state = reactive({
foo: 'foo',
bar: { a: 1 }
})
effect(() => {//当前该方法可以理解为 vue执行dispatch 比较虚拟dom刷新页面
console.log('effect:', state.foo)
})
// state.foo
// state.foo = 'fooooooo'
// state.dong = 'dong'
// delete state.dong
// 运行时代理
// state.bar.a = 10
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactive.js"></script>
<script>
const data = reactive({
msg: 'xxx'
})
effect(() => {//当前该方法可以理解为 vue执行dispatch 比较虚拟dom刷新页面
app.innerText = data.msg
})
setTimeout(() => {
data.msg = 'yyyy'
}, 1000);
</script>
</body>
</html>