[Vue] 解析 Vue 框架底层源码(上)
温馨提示:在写这篇文章的时候其实是图文并茂的,但由于图片都保存在第三方平台的图床中(notion, juejin),搬运到博客也较为麻烦一些,所以博文中就没有图片,如果对图片感兴趣的小伙伴,可以看我的掘金文章,那里有图文并茂的源码解释
目录
1. 组件挂载原理
2. 组件更新原理
3. Reactive
4. Computed
5. watch
6. ref
7. emit
8. slot
组件挂载原理
代码示例
<body>
<script src="../dist/runtime-dom.global.js"></script>
<div id="app"></div>
<script>
const { createApp, h } = VueRuntimeDOM;
const App = {
render() {
return h("div", "hello world");
},
};
let app = createApp(App, {});
app.mount("#app");
</script>
</body>
组件挂载
第一:执行第一句代码 createApp 函数,传入 App 组件。实际上调用的是 createAppAPI 函数返回的 createApp,在 createApp 函数中中返回 app 对象,app对象上有 mount, use, component, directive, mixin…… 等等常用的方法。
```
export function createAppAPI
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
version,
use(plugin: Plugin, ...options: any[]) { // use方法
},
mixin(mixin: ComponentOptions) { // mixin方法
},
directive(name: string, directive?: Directive) { // 对应的指令
},
mount(rootContainer: HostElement, isHydrate?: boolean): any { // 挂载方法
},
unmount() {
},
provide(key, value) {
}
})
return app
}
第二:执行 app.mount("#app"), 通过 createVNode 递归创建虚拟 Dom, 我们的 Hello World 场景中是如下情况:
return vnode.component!.proxy;
}
}; ```
第三:执行 render 函数将 vnode 渲染到容器当中。在render 函数调用 patch 方法,初次挂载 patch 的第一参数为 null。进入 patch 函数。
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPreFlushCbs()
flushPostFlushCbs()
container._vnode = vnode
}
第四:在 patch 函数中根据就不同的标签,进行不同的 process。这里是 App 组件标签,会调用 processComponent 函数。每个 process 处理函数都会对应两个逻辑,第一个是挂载,第二个是更新。我们这里调用的是挂载函数 mountComponent。
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 调用 setupRenderEffect 函数进行组件初始化。
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
第六:调用 setupRenderEffect 函数。
创建 componentUpdateFn 函数,该函数 是 ReactiveEffct 对象的 fn 属性。不懂这句话的意思,可以看之前的文章。Vue3 Reactive原理
- new ReactiveEffect 函数,创建 ReactiveEffect 对象,并且传入第二个参数 scheduler,进行组件更新的批处理。
- 绑定 update 方法给 组件 instance。
- 调用 update 方法,调用 effect.fn 属性,也就是调用 componentUpdateFn 函数。
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
update()
}
第七: componentUpdateFn 函数中,在 componentUpdateFn 中也有两个函数,一个是挂载,一个是更新,这里我们走到挂载的逻辑。
- 调用 renderComponentRoot 函数,调用组件的 render 方法,得到子节点,放到 组件 instance.subTree 属性上。
- 继续拿着子节点去 Patch。
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
initialVNode.el = subTree.el
instance.isMounted = true
}
}
第八:patch 子节点,也就是 h1 元素,在 patch 函数中根据就不同的标签,进行不同的 process。这里是普通节点标签,会调用 processElement。调用 mountElement 函数。
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
第九:在 mountElement 函数中:
- hostCreateElement 函数,创建 H1 节点赋值给 vnode.el 属性。
- 深度优先原则去 patch 子节点,这里没有我们默认跳过。
- hostPatchProp 函数,挂载 props。
- Object.defineProperty 方法将 vnode 定义为 el.__vnode 属性。
- hostInsert 函数,传入容器和锚点 anchor 通过 parent.insertBeofore 函数插入节点。自此 h1 被插入到了 div#app 中。
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
}
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, dirs } = vnode
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
// props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
Object.defineProperty(el, '__vnode', {
value: vnode,
enumerable: false
})
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
})
}
hostInsert(el, container, anchor)
}
自此组件挂载完毕。
组件更新
代码示例
<body>
<script src="../dist/runtime-dom.global.js"></script>
<div id="app"></div>
<script>
const { createApp, h, reactive } = VueRuntimeDOM;
const Son = {
props: {
name: String,
},
render() {
return h("p", this.name);
},
};
const App = {
setup() {
const state = reactive({ name: "zf" });
setTimeout(() => {
state.name = "jw";
}, 3000);
return () => {
return h(Son, { name: state.name });
};
},
};
createApp(App).mount("#app");
</script>
</body>
挂载阶段
第一步,执行第一句代码,createApp(App).mount("#app")
; 进行组件挂载。可以查看之前的文章,来看详细流程。
我们这里只聊聊组件挂载的核心,详细挂载请看: Vue3 组件挂载原理,执行 setup 函数。
- 对于 App 组件来讲,挂载时的核心就是会创建组件的 ReactiveEffect 对象。
- 每次更新和挂载都会调度
instance.update()
也就是 componentUpdateFn 函数。 - 并且将将返回的函数作为 render 函数,进行调用得到组件的虚拟 Dom 节点,来作为 instance.subtree 属性。
- Son 组件同理。
第二步:render 函数中 state.name 触发,触发 state 响应式对象的的 getter 操作,收集 组件的 ReaciveEffect 对象作为响应式对象的依赖。如下结构:
{
{ name: "zf" }: {
name : [组件的 ReaciveEffect, 其他的 ReaciveEffect(比如 watch)]
}
}
挂载核心操作完毕。
更新阶段
执行第一句代码:state.name = "jw"
第一:state.name = "jw"
, 原理可以看这篇文章:Vue 3.2 - Reactive 原理,这里几句带过,触发响应式对象 state 的 setter 操作,通过 Reflect.set 方法更新属性,然后触发 trigger 操作,执行 name 属性的 RectiveEffects 对象的 fn 属性,也就是 componentUpdateFn 函数。
第二:在 componentUpdateFn 函数中,调用 renderComponentRoot 函数,该函数中,再次调用 instance.render.call(), 获得新的虚拟 Dom, 再去将旧的和新的虚拟 Dom 进行 patch。
const nextTree = renderComponentRoot(instance)
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
第三:由于本次是更新逻辑,所以来到了 updateComponent 方法,在 updateComponent 调用 Son 组件 instace.update(),也就是 componentUpdateFn 函数。 在该函数中调用了 Son 组件 instance.render 方法,拿到最新的虚拟 Dom 进行渲染。
if (shouldUpdateComponent(n1, n2, optimized)) {
instance.next = n2
invalidateJob(instance.update)
instance.update()
}
this 访问的时候,实际上是对 instance 上属性的代理。这里是可以代理的属性,请大家自行参考。
Reactive
代码示例
<script src="./dist/reactivity.global.js"></script>
<body>
<div id="app"></div>
<script>
const { effect, reactive, ref, watch } = VueReactivity
const state = reactive({ name: 'cyan', age: 18 })
effect(() => {
app.innerHTML = state.name + state.age
})
setTimeout(() => {
state.name = 'tom'
}, 1000)
</script>
</body>
挂载阶段
执行第一句代码:const state = reactive({ name: 'cyan', age: 18 })
第一: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 函数中 通过 proxy 和 baseHandlers 创建响应式对象。并将响应式对象返回给 state。
const proxy = new Proxy(
target,
targetType = baseHandlers
)
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 如果已经是一个响应式对象了,直接返回该响应式对象
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如果多次通过一个相同的对象,创建响应式对象,直接返回该响应式对象
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 不支持简单值
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 创建响应式对象
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
执行第二句代码 effect(() => {app.innerHTML = state.name + state.age})
第三:调用 effect 函数
- 通过 new ReactiveEffect 来创建 ReactiveEffect 对象。
- 执行 ReactiveEffect 对象上的 Run 方法,之后返回 run 方法,暴露给用户进行调度(可以用于批量更新)。
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
const _effect = new ReactiveEffect(fn)
_effect.run()
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
第四:ReactiveEffect 类的 constructor 被调用, 将 effect 的参数作为 ReactiveEffect 对象的 fn 属性。
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
第五:调用 run 方法,当前 activeEffect 指向 effect 函数创建的 ReactiveEffect 对象,ReactiveEffect 对象的 fn 属性被调用, 调用过程中去访问了响应式对象 state(state.name + state.age
),从而触发响应式对象/代理Proxy 对象的 getter 方法。
ReactiveEffect.fn = () => {
app.innerHTML = state.name + state.age;
})
第六:在 state 的 getter 方法中
- 先通过 Reflect.get 取值。
- 调用 track 函数跟踪当前的 activeEffefcts 依赖对象。
- 如果是一个引用类型值,继续去通过 reactive 递归创建响应式对象。
- 返回 Reflect.get 的取值结果。
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)
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.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
第七:通过 track函数, trackEffects 函数建立双 Map 结构跟踪属性依赖。结果是这样的:我们访问了 state.name, state.age
, 所以 name,age 对应的 映射列表里有 当前 activeEffects 也就是 ReactiveEffect 对象。
const targetMap = {
{name: "cyan", age: 18}: {
name: [ReactiveEffect],
age: [ReactiveEffect]
}
}
图中还有一个细节没有表示,每一个 ReactiveEffefct 对象的 deps 属性,都指向该属性的依赖对象列表。用于每次 run 之后清理依赖对象。
第七:到这里为止,初始化完毕。app.innerHTML 中初始值渲染完成。
更新阶段:
第一:当执行 state.name = "tom" 时,会触发响应式对象的 settter 操作,在 settter 操作中
- 通过 Reflect.set 改变响应式对象的值。
- 然后通过 trigger 函数,触发 ReactiveEffects 依赖对象。
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 如果新旧值相同,不更新
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
const result = Reflect.set(target, key, value, receiver)
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result
}
第二:通过 trigger, triggerEffects, triggerEffect 三个函数,来触发更新,通过 cleanEffects 清理上次的依赖对象, 执行每个 ReactEffect 依赖对象的 run 方法,重新调用 ReactEffcts 对象的 fn属性。也就是 effect 函数的第一个参数。 也就是 javascriptReactiveEffect.fn = () => {app.innerHTML = state.name + state.age; })
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
triggerEffects(createDep(effects), eventInfo)
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
effect.run()
}
第三:state.name 又去调用了响应式对象的 getter 方法,由于通过 Reflect.set 修改了值,所以再次访问就是最新值,同时 再次收集依赖,返回新值,等待下一次更新。
第四:至此更新完毕。
Computed
代码示例
<script src="./dist/reactivity.global.js"></script>
<body>
<div id="app"></div>
<script>
let { effect, reactive, ref, shallowRef, toRef, computed } = VueReactivity
let state = reactive({ firstName: 'Tom', lastName: 'Benjamin' }) // 将普通的类型 转化成一个对象,这个对象中有value属性 指向原来的值
const fullName = computed({
// getter
get() {
return state.firstName + ' ' + state.lastName
},
// setter
set(newValue) {
;[state.firstName, state.lastName] = newValue.split(' ')
}
})
effect(() => {
app.innerHTML = fullName.value
})
setTimeout(() => {
fullName.value = 'Cyan Benjamin'
}, 2000)
挂载阶段
第一:第一篇文章详细介绍了 reactive 创建响应式流程,我们这里默认已经创建好了响应式对象 obj。 [[Vue 源码] Vue 3.2 - Reactive 原理](https://juejin.cn/post/7208708762265550885)
执行 第二句代码:
const fullName = computed({
// getter
get() {
return state.firstName + ' ' + state.lastName
},
// setter
set(newValue) {
;[state.firstName, state.lastName] = newValue.split(' ')
}
})
第二:调用 computed 函数, 在 computed 函数中初始化 getter 和 setter函数,然后通过 new ComputedRefImpl 创建 computedRef 对象。
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
getter = getterOrOptions.get
setter = getterOrOptions.set
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
return cRef as any
}
第三: 执行 ComputedRefImpl 的 constructor 方法
- 创建 ReactiveEffect 对象 赋值给 computed 对象的 effect 属性。创建过程 [[Vue 源码] Vue 3.2 - Reactive 原理](https://juejin.cn/post/7208708762265550885) 有详细说明。
- 默认创建好了 ReactiveEffect 对象 后 将 computedRef 的 getter 作为 ReactiveEffect 对象对象的 fn 属性。
- 创建ReactiveEffect 对象时 传入第二个参数 -> scheduler 函数。
- 初始化 dirry 属性 false 来表示是否为脏数据。
- 返回 computed 对象 > dirty 表示是否为脏数据来缓存计算属性,第一次或者说默认取值是脏数据执行 effect.run 方法取值,二次/多次取值 dirty 不是脏数据, 取缓存数据,等待依赖变了 scheduler 将 dirty 置为 true, 下次取值会重新执行 effect.run 方法来计算缓存值。
public _dirty = true
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
第三句代码调用:effect(() => {app.innerHTML = fullName.value})
第四:
- effect 函数调用,创建 ReactiveEffect 对象,执行 ReactiveEffect 对象上的 Run 方法,将 activeEfffect 对象只为当前创建的ReactiveEffect 对象,清除上一次的依赖列表, 执行
() => {app.innerHTML = fullName.value}
- 执行 fullname.value 触发 fullname.value 的 getter 函数。
javascript get value() { trackRefValue(self) if (self._dirty || !self._cacheable) { self._dirty = false self._value = self.effect.run()! } return self._value }
- getter 函数中 调用 trackRefValue(self)。
- trackRefValue 中调用 trackEffects 函数,使用 computedRef 对象的 dep 属性来收集依赖对象。也就是将 activeEffect 对象也就是 effect 产生的 ReacttiveEffects 传入到 computedRef 对象的依赖对象列表当中去。
- 执行
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
getter 函数中 调用 self.effect.run()。也就是调用 computed 的 getter 或者说 ReactiveEffects 对象的 fn属性,也就是
get() { return state.firstName + ' ' + state.lastName }
执行 obj.firstName 和 obj.lastName 触发 obj 的 getter ,将当前 computed 的 ReactEffect 对象收集到 firstName, lastName 属性的 effects Set 集合当中。通过 Reflect.get 方法得到值。
const targetMap = {
{ firstName: 'Tom', lastName: 'Benjamin' }: {
firstName: [ReactEffect],
lastName: [ReactEffect]
}
}
- 返回计算值赋值给 computed 对象的 _value 属性。
至此初次渲染完毕。
更新阶段
第一:fullname.value = "Cyan Benjamin", 触发 computedRef 对象 value 属性的 setter 方法,调用 this.settter 函数,传入新值。
set value(newValue: T) {
this._setter(newValue)
}
第二:
- setter 函数中通过 Reflect.set 给响应式对象赋值
- 触发响应式对象 的 setter 操作, 通过 Reflect.set() 修改 lastname 和 firstname。
- 继而触发 trigger 操作,不过这一次并没有执行该 ReactiveEffect 对象的run 方法,而是优先执行了 scheduler 方法。
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
第三:scheduler 方法中执行 triggerRefValue ,
var scheduler = () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
}
- 在 triggerRefValue 中 调用了 triggerEffects 函数,trigerEffects 执行了 triggerEffect 函数。
- triggerEffect 函数中遍历 computedRef 对象的依赖列表。
- 依次然后调用依赖对象的 run 方法
- run 方法中 先通过 cleanEffect 函数清空依赖。 然后让 activeEffect 指向当前的 ReactiveEffect 对象(computed 创建的 ReactiveEffect 对象), 执行依赖对象的 fn 函数,也就是 getter 方法, 也就是
get() { return state.firstName + ' ' + state.lastName }
- getter 方法中拿到最新值,同时 stata.firstName , state.lastName 又一次触发了 响应式对象 触发 getter 操作, 再一次跟踪activeEffect 依赖对象(computed 创建的 ReactiveEffect 对象)。
- 返回并渲染最新值,等待下一次更新。
自此更新完毕
watch
代码示例
<script src="./dist/vue.global.js"></script>
<body>
<div id="app"></div>
<script>
let { reactive, watch } = Vue
let state = reactive({ age: 0 })
watch(
() => state.age,
(newValue, oldValue) => {
app.innerHTML = `count is ${newValue} `
}, {flush: "sync"}
)
setInterval(() => {
state.age++
}, 1000)
</script>
挂载阶段
第一:第一篇文章详细介绍了 reactive 创建响应式流程,我们这里默认已经创建好了响应式对象 obj。 [[Vue 源码] Vue 3.2 - Reactive 原理](https://juejin.cn/post/7208708762265550885 "https://juejin.cn/post/7208708762265550885")
执行 第二句代码:调用 watch 函数。
watch(
() => state.age,
(newValue, oldValue) => {
app.innerHTML = `count is ${newValue} `
}, {flush: "sync"}
)
第一:watch 函数调用了 doWatch 函数,
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
return doWatch(source as any, cb, options)
}
第二:调用 doWatch 函数, 下面是 doWatch 函数流程
- 在 doWatch 函数中 初始化 getter
// 初始化 getter if (isRef(source)) { getter = () => source.value forceTrigger = isShallow(source) } else if (isReactive(source)) { getter = () => source deep = true }else if (isFunction(source)) { if (cb) { // getter with cb getter = () => source() } }
- 如果是 reactive 响应式对象,则需要深度监听,deep = true
- 初始化清理函数 clearUp 可以执行上一个 watch 的 cb 函数
// 清理函数
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
- 初始化 job 函数
const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, onCleanup ]) oldValue = newValue } } }
- job 函数 作为 ReactEffective 对象的 scheduler 函数。
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
}
const effect = new ReactiveEffect(getter, scheduler)
- 调用 ReactiveEffect 对象的 run 方法
- cleanEffect 清理依赖列表
- 将 activeEffects 置为 watch 创建的 ReactiveEffect 对象
- 调用 getter 方法,也就是
() => state.num
, 继而触发 state 响应式对象的getter 方法,将依赖收集到 num 属性的依赖列表里{num: [ReactiveEffect]}
- Reflect.get 返回值,成功初始化 oldValue。
- 返回 Unwatch 函数。
// initial run oldValue = effect.run() const unwatch = () => { effect.stop() if (instance && instance.scope) { remove(instance.scope.effects!, effect) } } ) return unwatch }
自此 挂载阶段完毕
更新阶段
第一:调用 setInterval(() => {
state.age++
}, 1000)
state.age 触发响应式的 setter 操作。
第二:
- setter 函数中, 通过 Reflect.set() 修改 state.age值。
- 继而触发 trigger 操作,不过这一次并没有执行该 ReactiveEffect 对象的run 方法,而是优先执行了 scheduler 方法。
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
第三:scheduler 方法中执行 job 函数(挂载的时候)
第四:job 函数中执行 effect.run() 函数.
- 再次触发 getter 拿到最新值 newValue。
- 调用 cleanUp 清理函数。
- 请用 cb 函数,传入 oldValue 和 newValue
等待下一次更新时再返回第一步。
const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // watch(source, cb) const newValue = effect.run() if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // cleanup before running cb again if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, onCleanup ]) oldValue = newValue } } }
自此更新完毕。
Ref
代码示例
<script src="./dist/reactivity.global.js"></script>
<body>
<div id="app"></div>
<script>
let { ref, effect } = VueReactivity
let state = ref(false)
effect(() => {
app.innerHTML = `ref is ${state.value} `
})
setTimeout(() => {
state.value = true
}, 2000)
</script>
执行第一句代码:ref(false)
, 调用 ref 函数
第一: ref函数,调用 createRef 函数。
export function ref(value?: unknown) {
return createRef(value, false)
}
第二:createRef 函数中, 通过 new RefImpl 实例化 ref对象。
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
第三:调用 RefImpl 的 constructor 构造函数。构造函数中初始化 ref 实例对象的 _value 和 _rawValue, 本例中是将 false 传给 toRective, 如果 value 是一个对象则将 通过 reactive 函数包装成 响应式对象,本例中直接返回 false。
第四:返回创建好的 ref 对象。
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
第四:开始执行第二句代码 effect(() => {
app.innerHTML = `ref is ${state.value} `
})
, effect 函数调用创建 ReactiveEffect 对象,将 activeEffect 对象置为当前的 ReactiveEffect 对象。调用 run 方法,调用 () => {
app.innerHTML = `ref is ${state.value}`
}
函数
第五:在该函数中触发 state 响应式 ref 对象 value 属性的 getter 操作。
通过 trackRefValue 函数 收集依赖到 ref.deps 当中。
{dep: [ReactiveEffect]}
返回_value 值,也就是 false
get value() { trackRefValue(this) return this._value }
app.innerHTML = -_value.
初始渲染完成。
更新阶段
执行第一句代码:setTimeout(() => {
state.value = true
}, 2000)
- 修改 _value 为新值
`` javascript this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) ```
triggerRefValue 来触发依赖列表
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) } }
triggerRefValue 函数中 触发一系列 trigger 操作,我们再前面的文章多次聊到过。这里直接执行 ref 对象依赖列表 dep属性 中 ReactiveEffec 对象的 run 方法。也就是
() => { app.innerHTML = ref is ${state.value} }
- 再次通过 state.value 触发 ref 对象 value 属性的 getter 操作,返回最新值,渲染,然后再次追踪依赖。
等待下一次的更新。
自此更新完毕。
emit
代码示例
<script>
let { createApp, reactive, toRefs, h } = Vue
const useCount = () => {
const state = reactive({ count: 0 })
const setCount = () => {
state.count++
}
return { state, setCount }
}
const MyCpn = {
props: {
count: {},
onChildUpdate: {}
},
setup(props, { emit }) {
return () =>
h(
'button',
{
onClick: () => {
emit('childUpdate')
}
},
[props.count]
)
}
}
const App = {
setup() {
let { state, setCount } = useCount()
return {
...toRefs(state),
setCount
}
},
render() {
return h(MyCpn, {
count: this.count,
onChildUpdate: () => {
this.setCount()
}
})
}
}
createApp(App).mount('#app')
</script>
emit 原理
第一:挂载逻辑可以看以前的文章:Vue 组件挂载原理, 这里我们聊关键的部分。
- 挂载子组件的时候,将 count, onChildUpdate 自定义事件函数 作为了子组件的 props, 根据上图也可以得知。
- 当我们在子组件中 emit 的时候,实际上调用调用的是 instance.emit 函数。
- 在 emit 函数中,对于本例子来说,emit("childUpdate"), emit 函数会将 childUpdate 进行 handleKey, camelize 函数处理得到 onChildUpdate 事件名字,然后去 Props 当中去寻找。并且调用。
- 另一种,情况是 v-model 的情况,也就是以update 开头的事件名字,Vue 同样也会对这种情况从 Props 中去找 update:modleValue 事件名, 同时也会找受控值 modelValue。
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
const props = instance.vnode.props || EMPTY_OBJ
let args = rawArgs
const isModelListener = event.startsWith('update:')
// for v-model update:xxx events, apply modifiers on args
const modelArg = isModelListener && event.slice(7)
if (modelArg && modelArg in props) {
const modifiersKey = `${
modelArg === 'modelValue' ? 'model' : modelArg
}Modifiers`
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
if (trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
if (number) {
args = rawArgs.map(looseToNumber)
}
}
let handlerName
let handler =
props[(handlerName = toHandlerKey(event))] ||
// also try camelCase event handler (#2249)
props[(handlerName = toHandlerKey(camelize(event)))]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
}
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
slots
代码示例
<script src="./dist/vue.global.js"></script>
<body>
<div id="app"></div>
<script>
let { createApp, reactive, Fragment, toRefs, h, getCurrentInstance } = Vue
const MyCpn = {
setup(props, { emit, slots }) {
console.log(getCurrentInstance(), 'child')
return () =>
h(Fragment, [
h('div', slots.header()),
h('div', slots.main()),
h('div', slots.footer())
])
}
}
const App = {
render() {
return h(MyCpn, null, {
header: () => {
return h('h1', 'header')
},
main: () => {
return h('h1', 'main')
},
footer: () => {
return h('h1', 'footer')
}
})
}
}
createApp(App).mount('#app')
</script>
第一:挂载子组件的时候,通过 h, createVNode 函数,创建虚拟 Dom 的时候,在 normalizeChildren 函数中 判断第三个参数是对象,则给子组件的示例上 ShapeFlags 标记了 SLOTS_CHILDREN。
type = ShapeFlags.SLOTS_CHILDREN
vnode.shapeFlag |= type
第二:挂载子组件的时候,调用 mountComponent 时,调用 initSlots 函数,来给 instance.slots 上赋值 children 属性。也就是:
{slots: {
header: () => {
return h('h1', 'header')
},
main: () => {
return h('h1', 'main')
},
footer: () => {
return h('h1', 'footer')
}
}}
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
// users can get the shallow readonly version of the slots object through `this.$slots`,
// we should avoid the proxy object polluting the slots of the internal instance
instance.slots = toRaw(children as InternalSlots)
// make compiler marker non-enumerable
def(children as InternalSlots, '_', type)
} else {
normalizeObjectSlots(
children as RawSlots,
(instance.slots = {}),
instance
)
}
} else {
instance.slots = {}
if (children) {
normalizeVNodeSlots(instance, children)
}
}
def(instance.slots, InternalObjectKey, 1)
}
第三:在子组件上通过 调用 slots.header(), 再调用 h 函数 获得虚拟 Dom,挂载的时候调用 render 函数进行 patch(这里没有 render 函数,就会将 setup 返回的函数当作 render 函数)。
自此挂载完毕。