本节主要内容
- 数据响应式的实现
- 依赖收集过程
- 手写实现网页响应式
响应式源码学习
vue3响应式的核心是reactive。这部分源码参见vue/packages/reactivity。
reactive会对对象所有嵌套属性做转换,返回对象的响应式副本。
ref本质上也是reactive实现,ref('test') = reactive({value: 'test'}),所以我们访问ref时都要使用.value
// 源码中reactive 由 createReactiveObject实现。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// reactive不支持直接传入原始数据类型的target。如果target不是引用类型,则直接返回原始值。
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// xxxx
// 核心:使用proxy代理
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
const mutableHandlers = {
get:(target: Target, key: string | symbol, receiver: object)=>{
// 1、拿到target上key对应的值
// 2、track跟踪一下,将activeEffect存储到target-key对应的副作用函数序列里
// 3、判断返回结果是否是对应类型,是的话往下递归,否则直接返回。
const res = Reflect.get(target, key, receiver)
track(target, key)
if (isObject(res)) {
// 如果是对象类型,则转换为Proxy返回。
// 这里的精妙之处在于,不在初始化的时候做拦截,等用户访问到了再做响应式处理,
// 一个lazy reactive,减少不必要的递归。有专业人士称之为渐进式响应式。
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
set: ( target: object, key: string | symbol, value: unknown, receiver: object)=>{
// 1、设置target的key值,
// 2、trigger一下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) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
},
has: function(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
},
deleteProperty: function(target: object, key: string | symbol): boolean {
// 删除属性,流程同set,相当于给set了一个undefined,delete target.key
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
},
}
const mutableCollectionHandlers = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
// 存储target与副作用函数之前的映射函数
// 这里使用WeakMap,是因为WeakMap的key值可以是对象类型
const reactiveMap = new WeakMap<Target, any>()
const reactive = function(target) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
依赖收集源码学习
上节我们介绍初始化流程的时候,提到setupComponent时,里面调用了一个函数setupRenderEffect(依赖收集),这里我们就来详细看下源码是怎么实现这部分功能的。
1、创建副作用更新函数
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
// 副作用更新函数,这里只摘取关键代码
if(!intance.isMounted) {
// 组件初次渲染
// 根据组件根结点的渲染函数拿到组件的children。renderComponentRoot函数主要逻辑为:
const {
type: Component,
vnode,
proxy,
withProxy,
props,
slots,
render,
renderCache,
data,
setupState,
ctx
} = instance
const proxyToUse = withProxy || proxy
// 1、触发beforeMount生命周期钩子
//根据实例的上下文,调用render函数,拿到子组件vnode
const subTree = (instance.subTree = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
))
// 2、调用patch,初始化子组件
patch(null, subTree,container, ...)
// 3、触发mounted生命周期钩子
}else {
// 更新组件
/** 组件更新有两种场景:
一是组件自身state发生变化,需要重新渲染;
另一种是父组件更新过程中遇到子组件,需要判断子组件是否需要更新,
如果需要更新则主动执行子组件的重新渲染方法,
这种情况下,next就是新的子组件vnode
*/
let { next, bu, u, parent, vnode } = instance
if (next) {
// 设置更新后vnode的el,因为下面第一次渲染新的组件vnode没有设置;
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
// 如果没有next,直接指向vnode
next = vnode
}
// 1、触发beforeUpdate或onBeforeUpdate
if (bu) {
invokeArrayFns(bu)
}
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
// 2、根据上下文、render函数拿到子组件新的vnode,执行patch更新
const nextTree = renderComponentRoot(instance)
// 更新实例的subTree
const prevTree = instance.subTree
instance.subTree = nextTree
// 执行patch,更新的时候patch再根据节点类型,执行不同的更新方法。
// 是数组类型的则遍历更新,是树结构的则递归遍历....
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
)
// 3、触发update hook 或 onUpdated
}
}
// 创建副作用函数并缓存下来
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.id = instance.uid
// xxx
// 缓存副作用函数后,立即执行一次
update()
}
瞧瞧上边的effect咋来的,new了一个ReactiveEffect。
// here it comes: packages/reactivity/src/effect.ts。这里仅摘取关键代码
const targetMap = new WeakMap<any, KeyToDepMap>()
export let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
// 注意下此处的写法
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
/**
*/
if (!this.active) {
// 当前effect不是激活状态,不需要进行依赖收集,直接调用this.fn()
return this.fn()
}
try {
// this.parent指向全局变量activeEffect,如果当前ReactiveEffect对象调用run方法时,是在其他ReactiveEffect的run方法里的,那么this.parent就会指向activeEffect,再将activeEffect指向当前effect,等当前的依赖收集完了,activeEffect再指回到this.parent
this.parent = activeEffect
activeEffect = this //
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent // 指回“依赖”层
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
}
effect函数嵌套:vue3使用树形结构解决该问题。
// 这里在parent effect执行的时候,会触发child effect执行
effect(()=> {
console.log('parent执行')
effect(()=> {
console.log('child 执行')
/** 当前effect执行的时候,将activeEffect赋值给child.parent,
activeEffect指向child,child执行完了,再将activeEffect指向parent。
不然等proxy.name执行的时候,activeEffect是指向child effect的
*/
proxy.id
})
proxy.name
})
2、track:收集target.key对应的副作用函数
// 我们上面提到的targetMap是一个存储target-key-effectFnSets的WeakMap
// 这部分目的就是将effectFn存储起来
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
// 获取target对应的depsMap,没有则新建
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取key对应的effectSets,没有则新建
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
extend(
{
effect: activeEffect!
},
debuggerEventExtraInfo!
)
)
}
}
}
3、trigger:触发target.key对应的副作用函数集
// 从targetMap中一次查找target->key->effect set
// 遍历effect set,依次run()
export function trigger
总结
- effect(fn):传入fn,返回响应式副作用函数。fn内部引用到了reactive的数据,proxy发生变化的时候,副作用函数会再次执行
- track(target, key): 建立target-key和副作用函数之间的映射关系
- trigger(target, key): 根据track建立的映射关系,找到对应的副作用函数并执行。
手写简易版数据响应式和依赖收集
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="app"></div>
<script>
const isObject = v => typeof v === 'object' && v !== null
const reactive = target => {
if(!isObject(target)) return target
return new Proxy(target, {
get: (target, key, receiver) => {
const res = Reflect.get(target, key, receiver)
track(target, key)
return isObject(res) ? reactive(res) : res
},
set: (target, key, value, receiver) => {
Reflect.set(target, key, value, receiver)
trigger(target, key)
return target
},
has: (target, key) => {
track(target, key)
return Reflect.has(target, key)
},
deleteProperty: (target, key) => {
const res = Reflect.deleteProperty(target, key)
trigger(target, key)
return res
}
})
}
const targetMap = new WeakMap()
const effectStack = []
const track = (target, key) => {
let dep = effectStack[effectStack.length - 1]
if (!dep) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(dep)
}
const trigger = (target, key) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach(fn => fn())
}
const state = reactive({
name: 'vue3'
})
const effect = fn => {
const e = createReactiveEffect(fn)
e()
return e
}
const createReactiveEffect = fn => {
effectStack.push(fn)
const effect = function (...args) {
let cur
try {
cur = effectStack[effectStack.length - 1]
cur(...args)
} finally {
cur = null
effectStack.pop()
}
}
return effect
}
const app = document.querySelector('#app')
effect(() => {
app.innerHTML = `hello: ${state.name}`
})
setTimeout(() => {
state.name = 'bobo'
}, 1000)
</script>
</body>
</html>