Vue系列
Vue3
源码结构
compiler-sfc将SFC转为JS代码,依赖了compiler-core和compiler-dom
compiler-core将Template转为Render函数
reactivity核心流程
所谓响应式,就是当我们修改数据后,可以自动做某些事情;对应到组件的渲染,就是修改数据后,能自动触发组件的重新渲染。
Vue 3 实现响应式,本质上是通过 Proxy API 劫持了数据对象的读写,当我们访问数据时,会触发 getter 执行依赖收集;修改数据时,会触发 setter 派发通知。
packages\reactivity\src\reactive.ts文件
先通过createReactiveObject()创建Proxy对象
Proxy主要传入两个参数:
- target:需要代理的目标
- baseHandlers:响应式对象的处理器
这里使用了weekMap做缓存优化,在createReactiveObject()中,先判断缓存中是否已经有了该代理对象,有的话直接返回回去,没有就new Proxy(target,baseHandlers),再将new的Proxy存入weekMap缓存中。
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
// 如果命中的话就直接返回就好了
// 使用缓存做的优化点
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
baseHandlers():
packages\reactivity\src\baseHandlers.ts
主要用于代理响应式数据对象,实现数据在不同场景下的自动更新。具体来说,baseHandlers通过重写数据对象的get、set、delete等操作,实现了数据的响应式更新和数据依赖的收集和计算。
处理器Handlers中的get,set主要通过createGetter(),createSetter()函数创建,
里面使用了Reflect.xxx:在外界更新、读取值等操作时,触发set、get...等,将新值传入如,再返回新值,同时触发trigger()或track();
track()和trigger():
packages\reactivity\src\effect.ts
- track()函数主要用于收集依赖,记录数据属性被哪些计算属性或者watcher所依赖。每当一个计算属性或者watcher访问一个数据属性时,track()就会被调用,用来收集这个依赖关系。
-
- 例如,在模板中使用了一个计算属性,这个计算属性依赖于data中的一个属性,那么track()函数就会收集这个依赖关系。
- trigger()函数主要用于触发依赖,当一个数据属性发生变化时,trigger()会被调用,用来通知计算属性或者watcher去重新计算和渲染。
-
- 例如,当上面的依赖关系中,data中的属性发生变化时,trigger()函数就会被调用,用来触发更新计算属性或者watcher的操作。
先基于 target 找到对应的 dep, 如果是第一次的话,那么就需要初始化,再将dep放入全局缓存targetMap中
triggger()先收集所有的 dep 放到 deps 里面
runtime-core模板编译核心流程
1.创建App根组件
1.1CreateApp(App):
初始化:main.ts调用mount()
2.1 基于root Component生成vnode
2.2 进行render——>调用patch():基于vnode的类型对不同类型组件进行处理
function patch(n1, n2, container = null, anchor = null, parentComponent = null) {
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
case Fragment:
processFragment(n1, n2, container);
break;
default:
if (shapeFlag & 1) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
}
else if (shapeFlag & 4) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
n1为之前的值,n2为当前的值,用于组件更新的diff算法。
如果n1为null,则表示这是在 组件初始化,则执行mountComponent() ,否则就为 组件更新,执行updateComponent()
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
//初始化组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
//更新组件
updateComponent(n1, n2, optimized)
}
}
初始化组件mountComponent()
- 创建Component组件实例对象
- setup component
-
- 初始化props :
initProps(instance, props):就是将给实例的props属性赋值instance.props = Props; - 初始化slots:
initSlots(instance, children) setupStatefulComponent
- 初始化props :
-
-
- 创建一个proxy代理对象,并绑定到
instance.proxy上 - 设置组件实例对象:
setCurrentInstance(instance)。这样使用者就可以通过getCurrentInstance()API获取当前组件实例。 - 设置上下文对象:
createSetupContext(instance): context会传入setup函数 - 调用setup()函数,并拿到返回值result:
setup(shallowReadonly(instance.props), setupContext)
- 创建一个proxy代理对象,并绑定到
-
-
-
-
- 如果返回值是function(setup里面return一个函数):会将这个函数当初
render()函数处理。也可以在setup外另外定义一个render()函数。 - 如果返回值是object : 将用户写的template编译成render函数,再将其挂载到
instance.render上 - 注意:以上步骤并未调用render函数,只是设置了render函数
- 如果返回值是function(setup里面return一个函数):会将这个函数当初
-
-
- setupRenderEffect
-
- renderComponentRoot()调用用户定义的render()函数,获取vnode
- 调用patch(),初始化子组件。(这是一个递归), 这时,子组件的
shapeFlag变成了Element类型.
-
-
processElement(n1, n2, container, anchor, parentComponent)
-
-
-
-
- 判断n1是否null,
mountElement()和patchElement() - 初始化element :
- 判断n1是否null,
-
-
-
-
-
-
hostCreateElement()创建真实元素。源码中这些函数是在packages\runtime-dom\src\nodeOps.ts中实现的。这样就vue可以自定义渲染器,比如改写成渲染到canvas上。- 处理children节点:
mountChildren():遍历所有子节点,递归调用patch()。 - 设置元素props :
hostPatchProp(),也是runtime-dom的api,底层是setAttribute - 将当前节点插入到父节点中:
hostInsert(el, container, anchor)
-
-
-
-
-
-
- 更新element
-
-
- 所有根元素创建完成后,
hostInsert("#root")将其插入到#root元素上
Update:
当响应式数值发生变化时触发update流程,例如count++,触发当前组件的effect()
- 在
mountComponent()时的setupRenderEffect()阶段,执行effect(), - 执行
componentUpdateFn()的else分支,主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
-
- 先更新组件的数据:
updateComponentPreRender() - 调用
render()函数,拿到最新的组件树 - 替换子树, 将之前的节点数,用prevTree变量存储,将实例上的
subTree换为最新的。 - 调用
patch(prevTree, nextTree, prevTree.el, null, instance);
- 先更新组件的数据:
-
-
processElement()——>updateElements(n1,n2,container):
-
-
-
-
- 获取新旧Props
- 将旧vnode的
el赋值给新el,以此保持新节点的完整性 - 对比Props:
patchProps()
-
-
-
-
-
-
- 遍历新Props,如果与旧Props不一样则 执行
hostPatchProp()来removeAttribute
- 遍历新Props,如果与旧Props不一样则 执行
-
-
-
-
-
-
- 对比Children:
patchChildren(n1, n2, el, anchor, parentComponent);
- 对比Children:
-
-
-
-
-
-
- Text类型,且不一样,直接使用
hostSetElementText()innerText - 其他类型,且不一样,patchKeyedChildren() ,diff算法核心:
- Text类型,且不一样,直接使用
-
-
-
// 在 vue3.2 版本里面是使用的 new ReactiveEffect
// 至于为什么不直接用 effect ,是因为需要一个 scope 参数来收集所有的 effect
// 而 effect 这个函数是对外的 api ,是不可以轻易改变参数的,所以会使用 new ReactiveEffect
// 因为 ReactiveEffect 是内部对象,加一个参数是无所谓的
// 后面如果要实现 scope 的逻辑的时候 需要改过来
// 现在就先算了
//简易版
instance.update = effect(componentUpdateFn, {
scheduler: () => {
// 把 effect 推到微任务的时候在执行
// queueJob(effect);
queueJob(instance.update);
},
});
//源码版
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
effect数据结构
源码实战
环境搭建
Babel,Jest,TS
响应式
测试用例:
import { effect, stop } from "../effect";
import { reactive } from "../reactive";
//测试effect
describe("effect", () => {
it.skip("happy path", () => {
const user = reactive({
age: 10,
});
let nextAge;
effect(() => {
nextAge = user.age + 1;
});
expect(nextAge).toBe(11);
//数值更新,希望触发effect()
user.age++;
expect(nextAge).toBe(12);
});
//测试获取effect的返回值
it("should return runner", () => {
let foo = 10;
const runner = effect(() => {
foo++;
return "foo";
});
expect(foo).toBe(11);
const r = runner();
expect(foo).toBe(12);
expect(r).toBe("foo");
});
it("scheduler", () => {
//1.通过 effect 的第二个参数给定的一个 scheduler 的fn
//2.effect第一次执行时,还会执行fn
//3.当响应式对象set,update时,不会执行fn,而是执行scheduler
//4.如果说 当执行runner时,会再次执行fn
let dummy;
let run: any;
const scheduler = jest.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
const runner = effect(
() => {
dummy = obj.foo;
},
{ scheduler }
);
//断言一开始不会被调用
expect(scheduler).not.toHaveBeenCalled();
expect(dummy).toBe(1);
//断言当响应式值发生改变时,不会调用effect的第一个参数,
// 而是会调用scheduler
// should be called on first trigger
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
// should not run yet
expect(dummy).toBe(1);
// manually run
run();
// should have run
expect(dummy).toBe(2);
});
//取消响应式功能
it("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
obj.prop = 2;
expect(dummy).toBe(2);
stop(runner);
obj.prop = 3;
expect(dummy).toBe(2);
// stopped effect should still be manually callable
runner();
expect(dummy).toBe(3);
});
//调用stop后的回调函数
it("events: onStop", () => {
const obj = reactive({ prop: 1 });
const onStop = jest.fn();
let dummy;
const runner = effect(
() => {
dummy = obj.foo;
},
{
onStop,
}
);
stop(runner);
expect(onStop).toHaveBeenCalled();
});
});
import { reactive } from "../reactive";
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 };
const observed = reactive(original);
//来验证源对象与响应式后的对象不相等
expect(observed).not.toBe(original);
//用来验证响应式对象与和源对象的一致性
expect(observed.foo).toBe(1);
});
});
Reactive
- 创建,并返回Proxy
- get 触发收集依赖
track() - set 触发依赖
trigger()
import { readonly } from "../reactive";
//readonly不可以被改写(set)
describe("readonly", () => {
it("happy path", () => {
const original = { foo: 1, bar: { baz: 2 } };
const observed = readonly(original);
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1);
});
});
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
import { track, trigger } from "./effect";
export function reactive(raw) {
return new Proxy(
raw,
{
get(target, key) {
//target:代理的对象
//key:要get的代理对象的属性
const res = Reflect.get(target, key);
//TODO 依赖收集
track(target, key);
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
//TODO 触发依赖
trigger(target, key, value);
return res;
},
}
);
}
//readonly不能set,没有track()
export function readonly(raw) {
return new Proxy(
raw,
readonlyHandlers
{
get(target, key) {
const res = Reflect.get(target, key);
return res;
},
set(target, key, value) {
throw new Error("readonly不可被更改");
}
}
);
}
我们可以发现,get和set函数的逻辑重复了,我们可以将其抽取成 createGetter()和createSetter()
import { track, trigger } from "./effect";
//重构get,set更具通用性.
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
//非只读,则可以触发track()
if (!isReadonly) {
track(target, key);
}
return res;
};
}
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key, value);
return res;
};
}
export function reactive(raw) {
return new Proxy(
raw,
{
get: createGetter(),
set: createSetter()
}
);
}
//readonly不能set,没有track()
export function readonly(raw) {
return new Proxy(
raw,
{
get: createGetter(true),
set: createSetter(),
}
);
}
我们又发现,整个Proxy的第二个参数都可以抽离到另一个文件baseHandlers.ts,用于存放所有Handler,甚至创建整个new Proxy都可以抽离成一个函数createActiveObject()用于创建代理。
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
import { track, trigger } from "./effect";
export function reactive(raw) {
return createActiveObject(raw, mutableHandlers);
}
//readonly不能set,没有track()
export function readonly(raw) {
return createActiveObject(raw, readonlyHandlers);
}
function createActiveObject(raw: any, baseHandlers) {
return new Proxy(raw, baseHandlers);
}
import { track, trigger } from "./effect";
//重构get,set更具通用性.
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
//非只读,则可以触发track()
if (!isReadonly) {
track(target, key);
}
return res;
};
}
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key, value);
return res;
};
}
//reactive的handler
export const mutableHandlers = {
get: createGetter(),
set: createSetter(),
};
//readonly的handler
export const readonlyHandlers = {
get: createGetter(true),
set: createSetter(),
};
每次都要重新创建getter,setter。我们可以对getter,setter做缓存,一开始就创建好这两个。
import { track, trigger } from "./effect";
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
//重构get,set更具通用性.
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
//非只读,则可以触发track()
if (!isReadonly) {
track(target, key);
}
return res;
};
}
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key, value);
return res;
};
}
export const mutableHandlers = {
get,
set,
};
export const readonlyHandlers = {
readonlyGet,
set(target, key, value) {
console.warn(`${key} set失败 ,因为target为readonly的`);
return true;
},
};
重构reactive.ts
effect
_effect是每个ReactiveEffect的实例,他代表每个响应式dep。
对reactive重构
思维导图来源:催学社催学社 (cuixueshe.com)
原文是在语雀编写的,格式有点不对请见谅