区别
- 源码采用
monorepo
方式进行管理,将模块拆分到packages
目录中 Vue3
采用ts
开发,增强类型检查。Vue2
采用flow
Vue3
的性能优化,支持tree-shaking
, 不使用就不会被打包Vue2
后期引入RFC
, 使每个版本改动可控rfcs
Vue3
劫持数据采用proxy
,Vue2
劫持数据采用defineProperty
,defineProperty
有性能问题和缺陷Vue3
中对模块编译进行了优化,编译时生成了Block Tree
, 可以对子节点进行收集,可以减少比较,并且采用了patchFlag
标记动态节点Vue3
采用Composition API
进行组织功能,解决反复横条,优化重复逻辑(Mixin 带来的数据来源不清晰、命名冲突等问题),相比options API
类型推断更加方便- 增加了
Fragment
、Teleport
、Suspense
组件
目录结构
- reactivity 响应式系统
- compiler-core 与平台无关的编译器核心
- compiler-dom 针对浏览器的编译模块
- compiler-sfc 针对 vue 文件的编译模块
- compiler-ssr 针对服务端的编译模块
- runtime-core 与平台无关的运行时核心(可以创建针对特定平台的运行时 - 自定义渲染器)
- runtime-dom 针对浏览器的运行时。包括 DOM API, 属性, 事件处理等
- runtime-test 用于测试
- server-renderer 用于服务端渲染
- shared 多个包之间共享的内容
- size-check 用来测试代码体积
- template-explorer 用于调试编译器输出的开发工具
- vue 完整版本,包括运行时和编译时
安装依赖
yarn add typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa
注意:安装依赖的时候会提示忽略工作空间:--ignore-workspace-root-check
依赖 | 解释 |
---|---|
typescript | 支持 ts 语法 |
rollup | 打包工具 |
rollup-plugin-typescript2 | 连接 rollup 和 ts |
@rollup/plugin-node-resolve | 解析 node 中的第三方模块 |
@rollup/plugin-json | 支持引入 json |
execa | 开启子进程执行命令 |
Reactivity 响应式模块
reactivity 模块是 Vue3 的响应式模块。包含以下文件:
baseHandlers.ts
collectionHandlers.ts
computed.ts
effect.ts
index.ts
operations.ts
reactive.ts
ref.ts
接下来,我们一一的分析以上文件中的核心逻辑~
- reactivity.ts 文件
我们都知道 Vue2.x 中,响应式系统有以下缺点:
- 无法监听到数组长度的改变和 length 的改变
- 对象新增和删除属性无法监听
- 只能劫持对象的属性
该文件中最核心的方法就是:reactive 方法,该方法的主要作用是将目标对象转换为 Proxy 实例
export function reactive(target: object) {
// 我们需要将目标变成响应式对象,Proxy实例
return createReactiveObject(target, mutableHandlers) // 核心的操作就是读取文件时做依赖收集,当数据变化时做视图更新
}
createReactiveObject() 方法根据不同的参数返回不同的 proxy 实例, 有以下几种实例:
- 完全响应式的 proxy 实例,如果有嵌套对象,会 递归 调用 reactive
- 只读的 proxy 实例
- 浅层响应式的 proxy 实例(只有第一层的属性是响应式的)
- 只读的浅层响应式的 proxy 实例
扩展:浅层的响应式 proxy 实例是什么?
由于 proxy 只代理对象的第一层属性,更深层次的属性不会代理。如果需要完全响应式对象,就需要 递归 调用 reactive
function createReactiveObject(
target: Target, // 用户传的对象
isReadonly: boolean, // 是否只读
baseHandlers: ProxyHandler<any>, // (普通对象或数组)的处理器函数
collectionHandlers: ProxyHandler<any> // (Set、WeakSet、Map、WeakMap)的处理器函数
) {
if (!isObject(target)) {
// 如果用户传的不是对象就之间返回
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers // 普通的对象 Object 或 Array,处理器对象就使用 baseHandlers;如果是 Set, Map, WeakMap, WeakSet 中的一个,就使用 collectionHandlers
)
proxyMap.set(target, proxy)
return proxy
}
该核心函数主要做了以下事情:
- 若 target 不是对象,则直接返回 target
- 若 target 已经是 proxy 实例,则直接返回 target
- 若 target 不是一个可观察的对象,则直接返回 target
- 创建 proxy 实例
- baseHandlers.ts 文件
该文件主要针对 4 种 proxy 实例定义不同的处理器
mutableHandlers: 完全响应式处理器
readonlyHandlers: 只读处理器
shallowReactiveHandlers: 浅层的响应式处理器
shallowReadonlyHandlers: 浅层的只读处理器
- 完全响应式处理器
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
}
mutableHandlers 处理器对五种操作做了拦截,分别是:
- get 获取属性
- set 设置属性
- deleteProperty 删除属性
- has 是否拥有某个属性
- ownKeys
重点:get、has、ownKeys 操作会做 track
依赖收集; set、deleteProperty 操作会 trigger
触发更新
get 属性的处理器对应的创建函数是 createGetter
const get = /*#__PURE__*/ createGetter() // /*#__PURE__*/表示纯函数
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) { // target 是否是响应式对象
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) { // target 是否是只读的
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
) { // 如果访问的 key 是 __v_raw,并且 receiver == target.__v_readonly || receiver == target.__v_reactive,则直接返回 target
return target
}
const targetIsArray = isArray(target)
// 如果 target 不是只读的是数组并且 key 属于三个方法之一 ['includes', 'indexOf', 'lastIndexOf'],即触发了这三个操作之一
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) { // 如果 key 是 Symbol 并且属于 symbol 的内置方法之一,或者访问的是原型对象,直接返回结果
return res
}
if (!isReadonly) { // 只读对象不收集依赖
track(target, TrackOpTypes.GET, key)
}
if (shallow) { // 浅层响应立即返回,不递归调用 reactive()
return res
}
// 如果是 ref 对象,则返回真正的值,即 ref.value,数组除外。
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
// 由于 proxy 只能代理一层,所以 target[key] 的值如果是对象,就继续对其进行代理
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
需要注意以下几点:
- Reflect.get() 方法等价于 target[key], 有返回值
- 数组的处理
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const arrayInstrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
const arr = toRaw(this)
// 对数组的每个值进行 track 操作,收集依赖
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = method.apply(arr, args)
// 参数有可能是响应式的,函数执行后返回值为 -1 或 false,那就用参数的原始值再试一遍
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = method.apply(this, args)
resetTracking()
return res
}
})
- builtInSymbols.has(key) 为 true 或原型对象不收集依赖
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(isSymbol)
)
set 属性的处理器对应的创建函数是 createSetter
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value // 如果原来的值是 ref,但新的值不是,将新的值赋给 ref.value 即可
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) { // 如果 target 没有 key,就代表是新增操作,需要触发依赖
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
deleteProperty、has、ownKeys
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) { // 如果删除结果为 true 并且 target 拥有这个 key 就触发依赖
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
- effect.ts 文件
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) { // 如果已经是 effect 函数,取得原来的 fn
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
// 如果 lazy 为 false,马上执行一次
// 计算属性的 lazy 为 true
if (!options.lazy) {
effect()
}
return effect
}
核心函数:createReactiveEffect
let uid = 0
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
// 为了避免递归循环,所以要检测一下
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true // 用于判断当前 effect 是否激活,有一个 stop() 来将它设为 false
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
// activeEffect 为空,代表没有依赖,直接返回
return
}
// targetMap 依赖管理中心,用于收集依赖和触发依赖
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// targetMap 为每个 target 建立一个 map
// 每个 target 的 key 对应着一个 dep
// 然后用 dep 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数
// 类似于这样
// targetMap(weakmap) = {
// target1(map): {
// key1(dep): (fn1,fn2,fn3...)
// key2(dep): (fn1,fn2,fn3...)
// },
// target2(map): {
// key1(dep): (fn1,fn2,fn3...)
// key2(dep): (fn1,fn2,fn3...)
// },
// }
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key,
})
}
}
}
注意: WeakMap
键是弱引用的 key 必须是一个对象,value 可以为任意值
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}
- operations.ts 文件
export const enum TrackOpTypes { // track 的类型
GET = 'get', // get 操作
HAS = 'has', // has 操作
ITERATE = 'iterate' // ownKeys 操作
}
export const enum TriggerOpTypes { // trigger 的类型
SET = 'set', // 设置操作,将旧值设置为新值
ADD = 'add', // 新增操作,添加一个新的值 例如给对象新增一个值 数组的 push 操作
DELETE = 'delete', // 删除操作 例如对象的 delete 操作,数组的 pop 操作
CLEAR = 'clear' // 用于 Map 和 Set 的 clear 操作。
}
type 主要用于标识 track() 和 trigger() 的类型
Compiler 模板编译模块
Vue3.2 setup 语法
1. 变量、方法不需要 return 出来
<template>
<div class="home">
显示的值 {{flag }}
<button @click="changeHandler">改变值</button>
</div>
</template>
<!-- 只需要在script上添加setup -->
<script lang="ts" setup>
import { ref } from 'vue'
// flag变量不需要在 return出去了
let flag = ref('开端-第一次循环')
// 函数也可以直接引用,不用在return中返回
let changeHandler = () => {
flag.value = '开端-第二次循环'
}
</script>
2. 组件不需要在注册
<!-- 这个是组件 -->
<template>
<div>
<h2>你好-我是肖鹤云</h2>
</div>
</template>
使用的页面
<template>
<div class="home">
<test-com></test-com>
</div>
</template>
<script lang="ts" setup>
// 组件命名采用的是大驼峰,引入后不需要在注册
// 在使用的使用直接是小写和横杠的方式连接 test-com
import TestCom from '../components/TestCom.vue'
</script>
defineProps
3. 新增 <!-- 父组件 -->
<template>
<div class="home">
<test-com :info="msg" time="42分钟"></test-com>
</div>
</template>
<script lang="ts" setup>
// 组件命名采用的是大驼峰,引入后不需要在注册
import TestCom from '../components/TestCom.vue'
let msg = '公交车-第一次循环'
</script>
<!-- 子组件 -->
<template>
<div>
<h2>你好-我是汤小梦</h2>
<p>信息:{{ info}}</p>
<p>{{ time }}</p>
</div>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
defineProps({
info: {
type: String,
default: '----',
},
time: {
type: String,
default: '0分钟',
},
})
</script>
4. 新增 defineEmits
<!-- 子组件 -->
<template>
<div>
<h2>你好-我是汤小梦</h2>
<button @click="handler1Click">新增</button>
<button @click="handler2Click">删除</button>
</div>
</template>
<script lang="ts" setup>
import { defineEmits } from 'vue'
// 使用defineEmits创建名称,接受一个数组
let $myEmit = defineEmits(['myAdd', 'myDel'])
let handler1Click = () => {
$myEmit('myAdd', '新增的数据')
}
let handler2Click = () => {
$myEmit('myDel', '删除的数据')
}
</script>
<!-- 父组件 -->
<template>
<div class="home">
<test-com @myAdd="myAddHandler" @myDel="myDelHandler"></test-com>
</div>
</template>
<script lang="ts" setup>
// 组件命名采用的是大驼峰,引入后不需要在注册,是不是爽歪歪呀!
// 在使用的使用直接是小写和横杠的方式连接 test-com
import TestCom from '../components/TestCom.vue'
let myAddHandler = (mess): void => {
console.log('新增==>', mess)
}
let myDelHandler = (mess): void => {
console.log('删除==>', mess)
}
</script>
5. 获取子组件中的属性值, defineExpose
<!-- 子组件 -->
<template>
<div>
<h2>你好-我是汤小梦</h2>
<p>性别:{{ sex}}</p>
<p>其他信息:{{ info}}</p>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, defineExpose } from 'vue'
let sex = ref('男')
let info = reactive({
like: '喜欢小明',
age: 28,
})
// 将组件中的属性暴露出去,这样父组件可以获取
defineExpose({
sex,
info,
})
</script>
<!-- 父组件 -->
<template>
<div class="home">
<button @click="getSonHandler">获取子组件中的数据</button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const testRef = ref()
const getSonHandler = () => {
console.log('获取子组件中的性别', testRef.value.sex)
console.log('获取子组件中的其他信息', testRef.value.info)
}
</script>
6. 新增指令 v-memo
该指令接收一个固定长度的数组作为依赖值进行[记忆比对]。如果数组中的每个值都和上次渲染的时候相同,则整个子树的更新会被跳过。即使是虚拟 DOM 的 VNode 创建也将被跳过,因为子树的记忆副本可以被重用。因此渲染的速度会非常的快。
7. style 新增 v-bind
<template>
<span> 哈哈哈哈哈 </span>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
color: 'red',
})
</script>
<style scoped>
span {
/* 使用v-bind绑定state中的变量 */
color: v-bind('state.color');
}
</style>