一、前言
前提:熟悉ES6的Proxy,Reflect,WeakMap,Map,Set。不熟悉的可参考:读懂Vue3响应式原理的前提(Proxy,Reflect,Set,Map,WeakMap)
版本:V3.0.0。
整个流程如图所示(图片比较大,可使用png格式保存到本地再查看):
先说结论,Vue的响应式原理:数据拦截+发布订阅者模式。Vue3使用了Proxy代替了Vue2的Object.defineProperty进行数据拦截,并使用了effect代替了watcher
为了方便理解,文本对源码做了一些删减,去除了部分其他功能
二、响应式入口
1. createApp
packages\runtime-dom\src\index.ts
createApp主要作用:1. 调用渲染器;2. 重写mount函数
js
export const createApp = ((...args) => {
//1. 调用渲染器
const app = ensureRenderer().createApp(...args)
//2. 重写mount函数,将传入的参数使用document.querySelector进行查找
// ...省略
return app
})
2. ensureRenderer
packages\runtime-dom\src\index.ts
ensureRenderer:判断是否有渲染器,有则返回,没有则调用createRenderer进行创建
js
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
3. createRenderer
packages\runtime-core\src\renderer.ts
createRenderer:调用baseCreateRenderer
js
export function createRenderer(options) {
return baseCreateRenderer(options)
}
4. baseCreateRenderer
packages\runtime-core\src\renderer.ts
1. 返回值
先看baseCreateRenderer函数的返回值
js
function baseCreateRenderer(
options,
createHydrationFns
){
//...先省略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
可以看到createApp是createAppAPI(render, hydrate)的返回值,createAppAPI方法中包含了平时用来注册插件的use以及其他功能函数。
createAppAPI不是本文的重点,接着看内部函数render
2. render
render:判断是否有虚拟节点,有则调用baseCreateRenderer内部函数patch
js
const render = (vnode, container) => {
if (vnode !== null) patch(container._vnode || null, vnode, container)
container._vnode = vnode
}
3. patch
patch:通过switch语句,处理了不同类型节点。先看与响应式相关的processComponent
js
const patch= (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
const { type, ref, shapeFlag } = n2
switch (type) {
//... 省略
default:
if (shapeFlag & ShapeFlags.COMPONENT) {
//处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
}
4. processComponent
processComponent:如果初次渲染则调用mountComponent,否则调用updateComponent
js
const processComponent = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
if (n1 == null) {
//初次渲染
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
//更新
updateComponent(n1, n2, optimized)
}
}
5. mountComponent
mountComponent:是响应式原理的目标函数,负责初始化组件状态,包含设置响应式数据等功能
mountComponent 主要作用分为三部分:1. 创建组件实例;2. 初始化组件;3. 创建渲染effect,并执行
js
const mountComponent= (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
//1. 创建组件实例
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
//2. 初始化组件
setupComponent(instance)
//3. 创建渲染effect,并执行
setupRenderEffect(
instance, // 组件实例
initialVNode, //vnode
container, // 容器元素
anchor,
parentSuspense,
isSVG,
optimized
)
}
三、响应式原理
1. setupComponent
packages\runtime-core\src\component.ts
setupComponent 主要作用包含两部分:1. 执行setup,初始化响应式API reactive等;2. 执行compile编译模版内容,得到实例的render渲染函数
2. reactive
packages\reactivity\src\reactive.ts
在setup中,我们常用reactive定义响应式数据。reactive函数的作用就是通过createReactiveObject函数创建一个proxy,而且针对不同的数据类型给定了不同的处理方法
js
export function reactive(target: object) {
return createReactiveObject(
target, //目标对象
false, //是否只读
mutableHandlers, //处理原始数据类型和引用数据类型
mutableCollectionHandlers //处理Set, Map, WeakMap, WeakSet类型
)
}
3. createReactiveObject
packages\reactivity\src\reactive.ts
createReactiveObject:使用ES6的Proxy创建代理对象
js
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
//创建响应式对象
const proxy = new Proxy(target, baseHandlers)
//用来存储原对象和代理后的对象之间的映射关系
reactiveMap.set(target, proxy)
return proxy
}
baseHandlers 就是在reactive函数中传入的用于处理原始数据类型和引用数据类型的拦截器mutableHandlers
4. mutableHandlers
packages\reactivity\src\baseHandlers.ts
mutableHandlers中包含了通常用于JavaScript对象的代理以控制对对象属性的访问和修改
js
export const mutableHandlers = {
get, //拦截属性读取
set, //拦截属性赋值
deleteProperty,//拦截属性删除
has, //拦截判断属性是否存在
ownKeys //拦截对目标对象自身属性的枚举操作
}
get是用createGetter函数创建,set是用createSetter函数创建
5. createGetter
packages\reactivity\src\baseHandlers.ts
createGetter主要负责三件事:1. 使用Reflect.get访问属性值;2. 使用track进行数据收集;3. 如果取出来的数据依旧为对象,再使用reactive进行代理(原因:Proxy只代理一层,这也提高了vue3的初始化性能,只有访问到的数据属性才进行响应式处理。Vue2是在初始化的时候就得递归遍历所有属性,使用Object.defineProperty进行响应式处理)
js
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
//如果已经被代理过,直接返回target
return target
}
//1.取值
const res = Reflect.get(target, key, receiver)
//2.数据收集
track(target, TrackOpTypes.GET, key)
if (isObject(res)) {
//3.如果取出来的数据依旧为对象,再使用reactive进行代理
return reactive(res)
}
return res
}
}
6. track
packages\reactivity\src\effect.ts
使用track函数进行数据收集,需要先了解三个属性:1. targetMap;2. depsMap;3. dep
targetMap:一个WeakMap数据结构,用来存放目前对象和depsMap
depsMap:一个Map数据结构,用来存放属性和其对应的dep依赖项数组
dep:一个Set数据结构,存放effect,有负责渲染的effect,也有使用watchEffect自定义的effect
结构如图所示:
track函数的作用:根据target从targetMap中寻找depsMap,再从depsMap中根据对象的key,存储该key相关的effect
js
export function track(target: object, type: TrackOpTypes, key: unknown) {
//activeEffect:当前激活的副作用函数effect,用于渲染
if (!shouldTrack || activeEffect === undefined) {
return
}
//targetMap:一个WeakMap数据结构,用来存放目前对象和depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
//depsMap:一个Map数据结构,用来存放属性和其对应的dep依赖项数组
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
//收集依赖
dep.add(activeEffect)
//effect 也记录一下 dep
activeEffect.deps.push(dep)
}
}
activeEffect.deps.push(dep):将当前dep数组添加到activeEffect.deps上。作用后面会详细讲述,先继续往下看。
例如使用reactive定义一个对象:
js
const state = reactive({
count:0,
})
那么targetMap的数据结构将变为:
js
targetMap:{
{ count:0 } : {
_v_isRef : [activeEffect],
count :[activeEffect]
}
}
_v_isRef是当前整个对象的key,简化下来的数据结构为:
js
targetMap:{
{ count:0 } : {
count :[activeEffect]
}
}
7. createSetter
packages\reactivity\src\baseHandlers.ts
createGetter主要负责两件事:1. 先使用Reflect.set赋值;2. 然后调用trigger触发更新
js
function createSetter(shallow = false) {
return function set(
target,
key,
value,
receiver
): boolean {
const oldValue = (target as any)[key]
//判断target中是否有对应的key
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
//赋值
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
if (!hadKey) {
//当前key不存在,说明是赋值新属性
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
//值有改变
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
8. trigger
packages\reactivity\src\effect.ts
trigger函数主要作用:1. 依据key从depsMap中找到对应的dep数组;2. 使用run方法调用effects数组中每个effect,从而进行更新
js
export function trigger(
target,
type,
key,
newValue,
oldValue,
oldTarget
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
/* effect钩子队列 */
const effects = new Set<ReactiveEffect>()
/* 定义add函数,将effect添加到effects中 */
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.options.allowRecurse) {
effects.add(effect) /* 储存effect */
}
})
}
}
//取出属性对应的dep栈作为参数传入add
add(depsMap.get(key))
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
/* 进行调度更新*/
effect.options.scheduler(effect)
} else {
effect()
}
}
//依次执行effect回调
effects.forEach(run)
}
get与set的流程如下所示:
四、编译收集
get是要访问属性时才会被触发,那么什么时候触发get,进行依赖收集的呢?接着看mountComponent函数的第三部分setupRenderEffect。
1. setupRenderEffect
packages\runtime-core\src\renderer.ts
setupRenderEffect:负责创建一个渲染effect,并把它赋值给组件实例的update方法,作为渲染更新视图用。setupRenderEffect内部有两个函数,分别为effect和componentEffect。
effect:是一个高阶函数,负责给componentEffect配置初始化参数,以及给activeEffect赋值,执行cleanup清除所有依赖该effect的dep数组。
componentEffect主要有两个部分:1. 使用renderComponentRoot将实例转为树形结构(此阶段触发get);2. 对整个树进行patch
js
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
//创建一个渲染effect,并把它赋值给组件实例的update方法,作为渲染更新视图用
instance.update = effect(function componentEffect() {
//实例还未挂载
if (!instance.isMounted) {
//1. renderComponentRoot:将实例转为树形结构
const subTree = (instance.subTree = renderComponentRoot(instance))
//2. 对整个树进行patch
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
initialVNode.el = subTree.el
//3. 实例已挂载
instance.isMounted = true
} else {
//自发性更新部分代码 ...
}
}, prodEffectOptions)
}
2. renderComponentRoot
packages\runtime-core\src\componentRenderUtils.ts
在renderComponentRoot中,调用了之前在setupComponent中生成的render函数。此时会读取真实属性,触发get,进行依赖收集
js
export function renderComponentRoot(
instance
): VNode {
const {
//...
render,
} = instance
let result
//...
result = normalizeVNode(
//调用实例的render函数
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
return result
}
3. effect
effect:1. 调用createReactiveEffect给componentEffect配置初始化参数等操作;2. 如果不是懒加载,立即执行
js
export function effect<T = any>(
fn,
options
) {
const effect = createReactiveEffect(fn, options)
//如果不是懒加载 立即执行由createReactiveEffect创建出来的ReactiveEffect函数
if (!options.lazy) {
effect()
}
return effect
}
4. createReactiveEffect
createReactiveEffect的作用主要是配置了一些初始化的参数,然后包装了之前传进来的fn
js
function createReactiveEffect<T = any>(
fn,
options
) {
const effect = function reactiveEffect(): unknown {
if (!effectStack.includes(effect)) {
//清空effect的deps依赖栈,同时也删除了对应属性的dep栈中的此effect(双向删除)
cleanup(effect)
try {
enableTracking() //允许收集
effectStack.push(effect) //往effect数组中里放入当前 effect
activeEffect = effect // effect 赋值给当前的 activeEffect
return fn() // fn 为effect传进来 componentEffect(renderer.ts)
} finally {
effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // 替换activeEffect为新的栈顶
}
}
}
//配置初始化参数
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
在了解cleanup函数的作用前,先看之前提到的track函数这两行代码:
js
//收集依赖
dep.add(activeEffect)
//effect 也记录一下 dep
activeEffect.deps.push(dep)
第一行:收集依赖,将effect添加到属性的dep栈中
第二行:将dep栈添加到effect.deps数组中,让effect知道它被哪些属性依赖了
他们是一个双向依赖的关系,dep中有effect,effect.deps中有dep,如图所示:
再看cleanup函数:
js
function cleanup(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
入参是一个effect,删除所有属性dep下的effect,并使effect.deps数组置空。
例如使用reactive定义一个对象:
js
const state = reactive({
count:0,
name:'zhangsan'
})
经过数据收集后,targetMap的数据结构将变为:
js
targetMap:{
{count: 0, name: 'zhangsan'}:{
_v_isRef:[activeEffect],
count:[activeEffect],
name:[activeEffect]
}
}
如果修改数据,将count变为1,触发set方法,执行effect。effect中又会先执行cleanup方法,删除 dep 里面对应的 effect。此时targetMap的数据结构将变为:
js
targetMap:{
{count: 1, name: 'zhangsan'}:{
_v_isRef:[],
count:[],
name:[]
}
}
思考:为什么要去删除各个属性下的dep中effect?
答案:这是为了在更新后, 清扫用不到的属性dep。假设有这样的模板结构
js
<div>
{{state.count}}
<div v-if="state.count < 1"> {{state.name}} </div>
<button @click="add">按钮</button>
</div>
当调用add函数使count变为1,state.name将不会再被渲染。那么数据更新 -> 模板重新读取数据 -> 触发get重新收集依赖,targetMap数据结构将变为:
js
targetMap:{
{count: 1, name: 'zhangsan'}:{
_v_isRef:[activeEffect],
count:[activeEffect],
name:[]
}
}
name属性的dep依赖将置空。这里再放一个简易版供大家理解:
js
<!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>
<script>
const state = {
count: 0,
name:'张三'
};
function cleanup(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
deps.length = 0;
}
}
const targetMap = new WeakMap();
/* effect函数 */
const effect = function reactiveEffect() {
cleanup(effect);
};
effect.deps = [];
let proxy = new Proxy(state, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
console.log("数据收集");
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()));
}
if (!dep.has(effect)) {
dep.add(effect);
effect.deps.push(dep);
}
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
console.log("数据更新");
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
dep.forEach((effect) => effect());
return res;
},
});
proxy.count; //触发get
proxy.name; //触发get
proxy.count = 3; //触发set
//更新完之后,再次读取数据触发get,进行依赖收集
if(proxy.count < 2){
proxy.name //不会再被访问到
}
console.log('targetMap',targetMap)
</script>
</body>
</html>
targetMap的打印结果为:
js
targetMap:{
{count: 3, name: '张三'}:{
count:[reactiveEffect],
name:[]
}
}
五、小结
在vue3中:
-
使用Proxy代替Object.defineProperty实现数据劫持
-
在编译阶段,执行render,触发get
-
在get中收集依赖,使用targetMap,depsMap和dep来管理使用到的属性的依赖项
-
当数据变动时触发set,使用run方法依次执行该dep下所有effect,进行更新
六、断点调试
授人以鱼不如授人以渔,原理的理解肯定离不开源码的阅读。而使用断点调试可以帮助我们阅读源码。
-
克隆vue3源码或者下载对应的版本包:github.com/vuejs/core....
-
编译,运行
yarn
-
建立git仓库并提交一个commit
-
修改package.json,添加 -s 或者 -sourcemap
"dev": "node scripts/dev.js -sourcemap",
-
启动,运行
yarn dev
-
VS Code 使用Live Server插件 打开packages\vue\examples\composition\grid.html
例如打开的的地址为:http://127.0.0.1:5500/
- 在vscode debug中新增Chrome 启动配置
js
{
"name": "Launch Chrome",
"request": "launch",
"type": "chrome",
"url": "http://127.0.0.1:5500/",
"webRoot": "${workspaceFolder}"
},
- 打断点,然后运行debug模式
七、结尾
对debug不熟的,可以参考:还在console.log?试试VSCode Debug!
对vue3项目搭建感兴趣的,可参考Vite4.3+Typescript+Vue3+Pinia 最新搭建企业级前端项目
参考资源:
文章有不对的,或者需要补充的,欢迎在评论区下留言,文章将持续更新。如果本篇文章点赞量和收藏量都不错的话,将继续更新Vue或React的相关源码分析