原文链接(格式更好):《3-5 Vue3-核心源码讲解》
结构更新
Vue3 的源码采用 TS + monorepo
Vue3 的重大更新(breaking changes)
[科普文] Vue3 到底更新了什么?-腾讯云开发者社区-腾讯云
⭐️组合式 API
将 Vue2 的选项式更新为组合式
选项式:
<script>
export default {
data() {
return {
count: 1
}
},
mounted() {
this.count = 0
},
methods: {
addCount() {
this.count++
}
}
}
</script>
缺点:
- 遵循语法写在特定区域:data、methods、computed、watch 等都是有固定语法的
- 当项目的负责度增加后,这些逻辑就会散落在代码的各处,不利于后期维护
组合式:
<script setup>
import { ref, onMounted } from "vue"
const count = ref(0)
const addCount = () => {
count.value++
}
onMounted(() => {
count.value = 0
})
</script>
优点:
- 不需要遵循在特点区域写,可以按照逻辑一行行书写,就跟传统的 JS 代码写法一致,可以将相同的逻辑放在一起
⭐️响应式原理
Vue2:全部基于Object.defineProperty()的get set实现。通过对data里面的数据递归处理,才能为每个属性增加getter setter,这样会有更高的性能开销,并且对于运行时动态新增/删除的属性无法自动处理为响应式
Vue3:基础类型基于对象的get|set实现,复杂类型则基于Proxy 实现。
Proxy是 ES6 新增的 API,可以直接拦截对象上的所有操作,所以解决了vue2 中的运行时动态新增/删除的属性无法自动处理为响应式问题,并且减少了不必要的性能开销
其他
Fragment允许组件返回多个根元素,减少层级slot插槽的增强与语法简化Suspense 组件异步内容加载组件,可以展示备用 UITeleport 组件允许将元素渲染到 DOM 的任意位置- 编译优化:优化了 VDOM 的对比算法
- TS 的支持
- tree-shaking 的支持
- 生命周期优化
- 等等...
初始化
Vue.createApp({
template: `
<div>
<h1>你好呀</h1>
<p>{{ msg }}</p>
<p v-if="array.length">{{ array.length }}</p>
</div>
`,
setup() {
const msg = Vue.ref('hello, my children')
const array = Vue.reactive([1, 2])
setTimeout(() => {
// debugger
msg.value = 'hello, my children~~~~~~'
}, 2000)
return { msg, array }
}
}).mount('#app')
初始化入口为:createApp 函数
初始化流程图:
响应式原理
Ref 原理
完整源码:vue3-ref.ts
核心源码解析:
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
// ref方法:里面调用 createRef 方法
export function ref(value?: unknown) {
return createRef(value, false)
}
// createRef方法:
function createRef(rawValue: unknown, shallow: boolean) {
// 当为 Ref 类型时表明是响应式了,所以直接返回
if (isRef(rawValue)) {
return rawValue
}
// 否则,调用 new RefImpl,并返回已处理为响应式的实例
return new RefImpl(rawValue, shallow)
}
// RefImpl:最核心的代码
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
// ref 调用时,__v_isShallow 为 false,所以直接返回 value(ref 的传参)
// this._rawValue = value
this._rawValue = __v_isShallow ? value : toRaw(value)
// ref 调用时,__v_isShallow 为 false,所以直接返回 value(ref 的传参)
// this._value = value
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 在读取 实例.value 属性时触发:
// 1. 收集依赖
trackRefValue(this)
// 2. 返回值
return this._value
}
set value(newVal) {
// 在设置 实例.value 属性时触发:
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
// 1. 设置 _rawValue 的值
this._rawValue = newVal
// 2. 设置 _value 的值
this._value = useDirectValue ? newVal : toReactive(newVal)
// 3. 收集依赖
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
总结:通过核心代码的解析,可以发现调用ref(0)后,最终返回的是个对象,传入的值是放在.value上的,并且通过get|set 函数实现响应式
所以这也是为什么const count = ref(0)后,使用/设置时要用count.value = 1
但在<template>里面可以省略.value,因为Vue框架在<template>解析编译时,自动加上了value
Reactive 原理
完整源码:vue3-reactive.ts
核心源码解析:
const user = reactive({ name: 'xx' })
// reactive方法:调用 createReactiveObject 方法
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
// createReactiveObject:最核心的代码
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// 边缘检测 --- start
// 传参不为对象时,警告,原值返回
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 existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target) // COMMON:Object|Array;COLLECTION:Map|WeekMap|Set|WeekSet
if (targetType === TargetType.INVALID) {
return target
}
// 边缘检测 --- end
// 调用 new Proxy,进行数据代理
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, // handler 函数
)
proxyMap.set(target, proxy)
return proxy
}
总结:通过核心代码的解析,可以发现核心在于new Proxy,针对不同的复杂类型,使用不同的handler函数,针对性的处理get|set方法
依赖收集、触发流程与原理
当明白了数据的能被改为响应式后,则需要研究下数据变化后为什么,对应的页面/函数会执行呢?
这就涉及到依赖的收集与触发
关键词:
- 副作用函数
-
- 会产生副作用的函数:函数内使用/更改的函数外的变量的函数
const userInfo = { name: 'lisi' }
function getUserInfo() { // 副作用函数
return userInfo.name // 引用了外部变量
}
- 响应式数据
-
- 数据发生变化时,能触发其他使用该数据的,这种数据就被称为响应式数据
conts obj = { text: 'hello!' }
function effect() {
document.body.innerHTML = obj.text
}
obj.text = '你好'
// 当重新赋值'你好'后,如果该 effect 能自动重新执行,则 obj 就是响应式数据
实现思路(简易代码):
conts obj = { text: 'hello!' }
function effect() {
document.body.innerHTML = obj.text
}
obj.text = '你好'
通过上述例子代码观察可知:
- 当副作用函数执行时,可以发现会触发
obj.text的读取操作 - 当修改
obj.text时,会触发obj.text的设置操作
那我们是不是可以在读取与设置时进行拦截呢?ES6 的 Proxy可以做代理
当读取时,把对应的副作用函数收集存起来
当设置时,把收集的副作用函数拿出来执行
const bucket = new Set(); // 存储副作用函数的“桶”
// 原始数据
const obj = { text: "hello!" };
// 对原始数据的代理
const data = new Proxy(obj, {
// 拦截读取操作
get(target, key) {
// 将副作用函数放入“桶”
bucket.add(effect);
console.log("[ bucket ] >", bucket);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newValue) {
// 设置属性值
target[key] = newValue;
// 将副作用函数拿出“桶”,并执行
bucket.forEach((fn) => fn());
},
});
function effect() {
document.body.innerHTML = data.text;
}
effect(); // 执行副作用函数,触发读取操作
setTimeout(() => {
data.text = "你好"; // 触发设置操作
}, 3000);
上面的代码就是一个简易的可运行的响应式原理(还存在很多设计问题)
完善的响应式
问题1:
副作用函数的命名被我们固定为effect了,真实情况可能是其他名字或匿名
解决:设计一个专门注册副作用函数的函数
let activeEffect = undefined // 全局变量:存储被注册的副作用函数
// 注册副作用函数的函数
function effect(fn) {
// 存储传入的副作用函数
activeEffect = fn
// 执行该副作用函数
activeEffect()
}
// 使用 effect
effect(
// 一个匿名的副作用函数
() => {
document.body.innerHTML = data.text;
}
)
问题2:
当给响应式数据设置一个新值时,也会触发副作用函数的执行
解决:将副作用的存储与响应式数据的属性关联起来,存储就不能再使用Set了
// 副作用函数1
effect(
// 一个匿名的副作用函数
() => {
document.body.innerHTML = data.text;
}
)
观察上述副作用函数,可以得到一个关系:
data(target)
-- text(key)
-- effect
// 副作用函数2
effect(
// 一个匿名的副作用函数
() => {
// 使用了 text 与 name 的副作用函数
document.body.innerHTML = data.text + data.name
}
)
若再新增一个副作用函数,可以得到一个关系:
data(target)
-- text(key)
-- effect1
-- effect2
-- name(key)
-- effect2
整理一下可得这样一个数据结构:
{
[target]: {
[key]: [effect1, effect2, ...]
// ...
}
}
解决问题1、问题2后的完善代码如下:
let activeEffect = undefined; // 全局变量:存储被注册的副作用函数
// 注册副作用函数的函数
function effect(fn) {
// 存储传入的副作用函数
activeEffect = fn;
// 执行该副作用函数
activeEffect();
}
const bucket = new WeakMap(); // 存储副作用函数的
// bucket 的数据结构为:{
// [target]: {
// [key]: [effect1, effect2, ...]
// // ...
// }
// }
// 对 Proxy 的封装
const reactive = (_obj) => {
// 对原始数据的代理
return new Proxy(_obj, {
// 拦截读取操作
get(target, key) {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) deps = new Set();
deps.add(activeEffect);
depsMap.set(key, deps);
}
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newValue) {
// 设置属性值
target[key] = newValue;
let depsMap = bucket.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
if (!deps) return;
deps.forEach((fn) => fn());
},
});
};
const data = reactive({ text: "hello!", name: "张三" });
function myEffect1() {
console.log("[ myEffect1() ] >");
document.body.innerHTML = data.text;
}
function myEffect2() {
console.log("[ myEffect2() ] >");
document.body.innerHTML = data.text + data.name;
}
// 使用 effect
effect(myEffect1);
effect(myEffect2);
setTimeout(() => {
console.log("[ setTimeout 3000 ] >");
data.pp = "你好!"; // 触发设置操作
}, 3000);
setTimeout(() => {
console.log("[ setTimeout 5000 ] >");
data.text = "你好!"; // 触发设置操作
}, 5000);
其中的bucket的数据结构如下:
在将上述完善后的代码的reactive函数再次完善下,可以得到越来越接近于 Vue3 源码的代码:
// ...
const bucket = new Map(); // 存储副作用函数的“桶”
// 依赖收集(追踪)
const track = (target, key) => {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) deps = new Set();
deps.add(activeEffect);
depsMap.set(key, deps);
}
};
// 依赖触发
const trigger = (target, key) => {
let depsMap = bucket.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
if (!deps) return;
deps.forEach((fn) => fn());
};
const reactive = (_obj) => {
// 对原始数据的代理
return new Proxy(_obj, {
// 拦截读取操作
get(target, key) {
// 依赖收集(追踪)
track(target, key);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newValue) {
// 设置属性值
target[key] = newValue;
// 依赖触发
trigger(target, key);
},
});
};
// ...
问题3:
当使用过的属性不在使用时,已绑定的依赖项还会触发
effect(() => {
document.body.innerHTML = obj.success ? obj.msg : 'error'
})
// 当 success 为 true 时,则使用 msg,那对应的依赖项为:
obj(target)
-- success(key)
-- effect
-- msg(key)
-- effect
// 但当 success 为 false 时,则固定显示文本 'error',但 msg 已绑的依赖项未解除
// 则如果执行 obj.msg = 'xx',则还是会触发 effect 的执行,这完全是多余的
解决:给副作用函数增加一个属性,用于存储相关联的依赖项,在读取副作用时先断开联系,等真正执行副作用时会重新建立联系
let activeEffect = undefined
// 新增的:clearup 函数,用于断开联系
function clearup(effectFn) {
effectFn.deps.forEach((deps) => {
deps.delete(effectFn);
});
effectFn.deps = [];
}
// 注册副作用函数的函数
function effect(fn) {
const effectFn = () => {
clearup(effectFn); // 新增的:clearup 函数,用于断开联系
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn;
// 执行该副作用函数
fn();
};
effectFn.deps = []; // 新增的:deps 数组,用于存储相关联的依赖项
effectFn();
}
// 改造 track,用于收集副作用函数的关联依赖项
// 依赖收集(追踪)
const track = (target, key) => {
if (activeEffect) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) deps = new Set();
deps.add(activeEffect);
depsMap.set(key, deps);
activeEffect.deps.push(deps) // 新增的:用于收集该副作用函数的关联依赖项
}
};
// 依赖触发
const trigger = (target, key) => {
let depsMap = bucket.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
if (!deps) return;
const newDeps = new Set(deps); // 新增的:用于避免出现死循环
newDeps.forEach((fn) => fn());
};
Vue3 源码内的依赖收集与触发
以reactive为例,讲述下依赖收集、触发的完整流程
reactive 的核心代码:
function reactive() {
// 调用 new Proxy,进行数据代理
const proxy = new Proxy(
target,
mutableHandlers, // handler 函数
)
}
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
deleteProperty,
has,
ownKeys
}
const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// ...
const res = Reflect.get(target, key, receiver)
// ...
// ⭐️ 依赖收集
track(target, TrackOpTypes.GET, key)
// ...
return res
}
}
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// ...
const result = Reflect.set(target, key, value, receiver)
// ...
// ⭐️ 依赖触发
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
}
依赖收集
通过getter实现依赖的收集
// ⭐️ 依赖收集
track(target, TrackOpTypes.GET, key)
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
// targetMap = {
// [target]: {
// [key]: []
// }
// }
let activeEffect = null
function track(target, type, key) {
let depsMap = targetMap.get(target)
if(!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if(!dep) {
dep = new Set()
depsMap.set(key, dep)
}
trackEffects(dep)
}
function trackEffects(dep) {
dep.add(activeEffect)
activeEffect!.deps.push(dep)
}
依赖触发
通过setter实现依赖的收集
// ⭐️ 依赖触发
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
function trigger(target, type, key, value, oldValue) {
let depsMap = targetMap.get(target)
if(!depsMap) return
let deps = depsMap.get(key)
triggerEffects(deps)
}
function triggerEffects(deps) {
for (const dep of deps) {
dep()
}
}
渲染流程
流程
大致跟 Vue2 一样的:编译 -> 运行时
- 编译
-
<template>转为模板 AST 树(用来描述模板的)- 将
模板 AST 树转换为JS AST 树(用来描述渲染函数的)
-
-
- 期间会打上
patchFlag(值为 number),用于精确化标记每个节点,只要打上了patchFlag则一定是动态的节点;没有打上的就是静态节点 - 并且还会额外使用
dynamicChildren数组来储存打标的节点,直接用该数据进行 diff
- 期间会打上
-
-
- 基于
JS AST 树生成render字符串 - 最后基于
render字符串生成render函数
- 基于
- 运行时
-
- 运行实例的
render函数,生成vnode - 基于
vnode进行挂载或 diff 后更新页面
- 运行实例的
源码
编译的主入口:Compile.ts,触发条件:.mount('#app')函数的调用,并完成首次页面的渲染
// ...
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// ...
// ⭐️ <template> 转为模板 AST 树(用来描述模板的)
const ast = isString(template) ? baseParse(template, options) : template
// ...
// ⭐️ 将模板 AST 树转换为 JS AST 树(用来描述渲染函数的)
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// ⭐️ 基于 JS AST 树生成 render 字符串
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
// ...
// ⭐️ 基于 render 字符串生成 render 函数
const render = (
__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction
运行时的主入口:无固定,触发条件:某个响应的数据的改变
// 若为 ref() 的值的改变,触发页面的重新渲染
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// ⭐️ 依赖变更通知函数
triggerRefValue(this, newVal)
}
}
}
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
// ...
effect.run()
// ...
patch(...)
补充知识
如何获取复杂数据的具体类型?
比如:
{ a: 1 },期望返回类型为object[{ a: 1 }],期望返回类型为arrayconst a = function () {},期望返回类型为function
const objectType = (obj: object): string => {
const fullTypeString = Object.prootype.toString.call(obj) // '[object Array]'
const typeString = fullTypeString.slice(8, -1) // Array
return typeString.toLocaleLowerCase() // array
}
Map、WeakMap、Set、WeakSet
Map:类似于object的,采用键值对存储数据,键可以是任意类型的(基础/复杂类型都可以)
WeakMap:虚弱版的Map,键必须为复杂类型,弱引用自动垃圾回收,不支持遍历
Set:类似于array的,里面的值不允许重复,值是任意类型的(基础/复杂类型都可以),无法通过索引取值,只能循环取值
WeakSet:虚弱版的Set,值必须为复杂类型,弱引用自动垃圾回收,不支持遍历
Proxy
new Proxy(target, handle);
// target: 目标对象
// property: 属性名
// value: 新值
// receiver: 最初接收赋值的对象,通常是 proxy 本身
const handle = {
get: function (target, property, receiver) {},
set(target, property, value, receiver) {}
}
面试题
手写一份 Vue3 的响应式
let activeEffect = undefined
const effect = fn => {
const effectFn = () => {
activeEffect = effectFn
fn()
}
effectFn()
}
const targetMap = new WeakMap()
// 依赖收集
const track = (target, key, receiver) => {
let depsMap = targetMap.get(target)
if(!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let depMap = depsMap.get(key)
if(!deps) deps = new Set()
deps.add(activeEffect)
depsMap.set(key, deps)
}
// 依赖触发
const trigger = (target, key, receiver) => {
let depsMap = targetMap.get(target)
if(!depsMap) return
let depMap = depsMap.get(key)
if(!depMap) return
depMap.forEach(fn => fn())
}
const reactive = _obj => {
return new Proxy(_obj, {
get(target, key, receiver) {
track(target, key, receiver)
return Reflect.get(target, key, receiver)
},
set(target, key, newValue, receiver) {
Reflect.set(target, key, newValue,receiver)
trigger(target, key, receiver)
}
})
}