上篇主要解析了patch
方法将VNode
对象转化为真实的DOM
节点。本文将详细解析改变数据之后的更新过程(点击按钮
-> 数据更新
-> DOM
更新)
点击button按钮执行回调
关于上篇button
节点的添加事件监听,可以详细看下[addEventListener
添加元素的事件监听]这段的内容( juejin.cn/post/701288… )。当点击按钮时会触发click
的事件监听,执行回调函数invoker
方法
// 方法属性赋值
invoker.value = initialValue
invoker.attached = getNow()
// invoker - 事件监听回调函数
const invoker: Invoker = (e: Event) => {
// ...
const timeStamp = e.timeStamp || _getNow()
if (timeStamp >= invoker.attached - 1) {
callWithAsyncErrorHandling(
patchStopImmediatePropagation(e, invoker.value),
instance,
ErrorCodes.NATIVE_EVENT_HANDLER,
[e]
)
}
}
可以看到invoker
方法内部执行了callWithAsyncErrorHandling
函数,第一个参数是patchStopImmediatePropagation
函数的返回值,看下此函数的内部实现
function patchStopImmediatePropagation(
e: Event,
value: EventValue
): EventValue {
if (isArray(value)) {
// ... value为数组
} else {
return value
}
}
patchStopImmediatePropagation
方法的内部,判断invoker.value
是否为数组,invoker.value
的值就是click
对应的属性值,本例为modifyMessage
方法。接着看下callWithAsyncErrorHandling
函数的内部实现
// callWithErrorHandling方法定义
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
// callWithAsyncErrorHandling方法定义
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
if (isFunction(fn)) {
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
return res
}
// ...
}
callWithAsyncErrorHandling
方法内部判断fn
是否为函数(本例fn
是modifyMessage
方法),所以执行callWithErrorHandling
方法,而callWithErrorHandling
函数内部就是执行fn
方法,即执行modifyMessage
方法,回归modifyMessage
方法的内部实现
const modifyMessage = () => {
message.value = '修改后的测试数据'
}
修改Proxy代理数据触发set钩子函数执行
modifyMessage
方法的内部是修改了message.value
的值,而message
是一个RefImpl
实例对象,RefImpl
类中对value
属性设置了set
钩子函数,当点击按钮修改message.value
的值时会触发set
函数的执行
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
set
钩子函数中首先调用hasChanged
方法,判断value
值是否发生变化
// NaN !== NaN
export const hasChanged = (value: any, oldValue: any): boolean =>
value !== oldValue && (value === value || oldValue === oldValue)
如果value
和oldValue
不相同,则将_rawValue
属性赋值改变之后的newVal
值,_value
赋值convert(newVal)
的返回结果(convert方法在之前也简单提到过,主要是判断newVal
是否为引用类型数据,引用类型数据返回reactive(newVal)
的返回值)。然后调用trigger
触发方法触发DOM
更新,参数为(RefImpl
实例对象,'set'
,'value'
,newVal
)。接下来解析下trigger
方法的内部实现
全局触发器trigger执行
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {}
if (type === TriggerOpTypes.CLEAR /* "clear" */) {}
else if (key === 'length' && isArray(target)) {}
else {
if (key !== void 0) {
add(depsMap.get(key))
}
switch (type) {
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
const run = (effect: ReactiveEffect) => {}
effects.forEach(run)
}
trigger
函数内部首先从全局的WeakMap
对象targetMap
中获取target
(message
的RefImpl
实例对象)对应的值。关于targetMap
对象的内容,可以看下依赖收集targetMap对象 。所以depsMap
的值是一个Map
对象,接着因为key
是'value'
字符串,不是空void 0
,所以调用函数内部定义的add
方法,传参是depsMap.get(key)
,是一个Set
对象(Set
对象中存放的是全局变量activeEffect
)。接着看下add
方法的内部实现
// add 方法
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse/* true */) {
effects.add(effect)
}
})
}
}
add
方法的作用是将effectsToAdd
Set
对象中的每个元素都添加到effects
中(trigger
函数内部定义的一个Set
变量)(关于effect.allowRecurse
的值是在setupRenderEffect
函数中执行effect
方法传入的第二个options
对象中定义的,可以看下全局依赖activeEffect )。再回归到trigger
函数中循环effects
中的每个元素作为参数执行run
方法,本例为全局变量activeEffect
。run
方法也是trigger
函数内部定义的方法,下面看下它的内部实现
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger/* void 0 */) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
首先判断effect.options.onTrigger
的值,就是setupRenderEffect
方法中调用effect
方法传入的options
参数,onTrigger
的值为void 0
。因为scheduler
的值为queueJob
方法,所以调用queueJob
方法,参数为activeEffect
全局变量就是reactiveEffect
函数。接下来看下queueJob
函数的内部实现
全局依赖存放到异步执行队列中
const queue: SchedulerJob[] = []
let flushIndex = 0
let currentPreFlushParentJob: SchedulerJob | null = null
// findInsertionIndex
function findInsertionIndex(job: SchedulerJob) {
let start = flushIndex + 1 // 0 + 1 = 1
let end = queue.length // queue = []
const jobId = getId(job)
while (start < end) {/* ... */}
return start // 1
}
// queueJob 定义
export function queueJob(job: SchedulerJob) {
if ((!queue.length ||
!queue.includes(job,isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
job !== currentPreFlushParentJob) {
const pos = findInsertionIndex(job)
if (pos > -1) {
queue.splice(pos, 0, job) // queue数组中添加job元素
} else {
queue.push(job)
}
queueFlush()
}
}
该函数内部的主要作用是将activeEffect
添加到全局的queue
队列中,然后调用queueFlush
函数
let isFlushing = false
let isFlushPending = false
const resolvedPromise: Promise<any> = Promise.resolve()
// queueFlush
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
queueFlush
方法内部首先将isFlushPending
变量设置为true
,表示等待中,因为后续会执行Promise
方法,然后执行Promise.resolve()
,并在then
中执行flushJobs
回调函数(异步)。看下flushJobs
方法的内部实现
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map() // seen = new Map()
}
flushPreFlushCbs(seen) // pendingPreFlushCbs 数据为空,所以函数直接返回
queue.sort((a, b) => getId(a) - getId(b))
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job) {
if (__DEV__) {
checkRecursiveUpdates(seen!, job)
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
flushJobs
方法中显示将isFlushPending
置为false
,isFlushing
置为true
,因为当flushJobs
方法开始执行时,Promise
已经是已完成状态了,所以将标识位置位。将queue
队列中的依赖根据id
的值进行升序排列。然后循环queue
队列中的元素(job
),首先调用checkRecursiveUpdates
方法,判断如果seen
(Map
对象)中不存在job
,则把它添加到seen
对象中(key: job, value: 1
)。最后调用callWithErrorHandling
执行job
元素,而job
就是全局的activeEffect
变量也就是reactiveEffect
方法。
执行异步队列中的全局依赖
因此当首次渲染完成之后,修改数据时会触发reactiveEffect
函数的再次执行。关于初始化挂在时执行reactiveEffect
函数的过程,可以看下全局依赖reactiveEffect 。从第一次执行reactiveEffect
方法的过程中,可以看到此方法内部主要是执行调用createReactiveEffect
函数传入的fn
函数,也就是调用setupRenderEffect
方法内effect
方法传入的componentEffect
函数。
关于第一次挂载时调用componentEffect
方法的过程可以回顾下render生成VNode对象,patch方法生成DOM节点。我们再此回归到此函数中
function componentEffect() {
if (!instance.isMounted) {}
else {
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (__DEV__) {
pushWarningContext(next || instance.vnode)
}
if (next) {/* next === null */}
else {
next = vnode
}
// ...BeforeUpdate
const nextTree = renderComponentRoot(instance)
// ...
const prevTree = instance.subTree
instance.subTree = nextTree
// ...
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
)
// ...
next.el = nextTree.el
// ...
}
}
因为初始化DOM
渲染完成之后,instance.isMounted
属性值会置为true
,所以当修改数据再次执行此方法时会执行else
分支。else
分支中主要是调用renderComponentRoot
重新执行render
方法,关于render
生成VNode
对象的过程可以回顾下前面的文章,这里不再赘述。修改完message
的值之后,生成的VNode
对象的改动点就在于type=Symbol(Text)
的子VNode
对象的children
属性值为改变之后的数据内容,本例为"修改后的测试数据 "
。
patch方法更新DOM
然后调用patch
方法对DOM
节点做出相应的改变。参数为:
1、
prevTree = instance.subTree
就是初始化调用render
方法生成的VNode
对象,下面统一称为旧VNode
2、nextTree
就是修改完数据之后重新生成的VNode
对象,下面称为新VNode
3、hostParentNode(prevTree.el!)!
函数的返回值,prevTree.el
属性就是空的文本节点,这个可以去看下patch解析VNode节点 。而这个空节点的父元素就是#app
节点
4、getNextHostNode(prevTree)
方法的返回值,这个方法获取的是prevTree.anchor.nextSibling
。就是获取prevTree.anchor
节点相邻的下个节点。通过patch解析VNode节点 可以知道prevTree.anchor
就是#app
节点最右边的子节点(空文本节点),所以prevTree.anchor.nextSibling
的值为null
5、最后三个参数:instance
对象、parentSuspense = null
、isSVG = false
然后看下当数据变化生成新VNode
对象时执行patch
方法的过程(初始化执行patch
方法生成DOM
节点的过程可以看下之前的文章)
首先根VNode
对象仍然是type=Symbol(Fragment)
的对象,所以调用processFragment
方法,看下方法内部的具体实现
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren } = n2
if (patchFlag > 0) {
optimized = true
}
// ...
if (n1 == null) {/* 数据改变时n1存在,是旧VNode */}
else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
isSVG
)
// ...
}
}
区别于初始化渲染DOM
,更新时会传入旧VNode
,所以n1
不等于null
,执行else
分支,判断旧VNode
和新VNode
对象都存在dynamicChildren
属性,执行patchBlockChildren
方法处理子对象,解析下它的内部实现
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
isSVG
) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
const container = oldVNode.type === Fragment ||
!isSameVNodeType(oldVNode, newVNode) ||
oldVNode.shapeFlag & ShapeFlags.COMPONENT /* 6 */ ||
oldVNode.shapeFlag & ShapeFlags.TELEPORT /* 64 */ ? hostParentNode(oldVNode.el!)! : fallbackContainer
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
true
)
}
}
patchBlockChildren
函数内部循环新VNode
对象的子节点数组,先解析type=Symbol(Text)
文本子节点,判断container
值为fallbackContainer
就是#app
节点(isSameVNodeType
判断是否是相同的VNode
类型,比较type
以及key
的值是否相同),接着将对应的旧新子节点对象以及container
对象等参数传入patch
方法,开始更新第一个文本DOM
元素
DOM.nodeValue修改节点内容
再次回归到patch
方法中,与初始化渲染一致,因为type=Symbol(Text)
,仍然是调用processText
方法
// processText
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {/* ...初始化渲染 */}
else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
首先el
的值等于n1.el
,也就是旧VNode
对象的el
属性,这里可以回顾下初始化processText解析文本对象。实际n1.el
就是初始化创建的文本DOM
对象,因为n2.children !== n1.children
(修改了message.value
的值),所以调用hostSetText
方法,对应的就是nodeOps
对象中的setText
方法
setText: (node, text) => {
node.nodeValue = text
}
就是将文本DOM
对象的nodeValue
属性设置为新的message.value
的值("修改后的测试数据 "
),此时页面的内容也会发生变化。
然后再解析type='button'
的元素对象,首先仍然是调用processElement
方法,因为是渲染更新,所以旧VNode
是存在的,调用patchElement
方法比较元素
1、首先是循环
新VNode
对象的dynamicProps
值(属性数组),比较新旧属性值是否发生变化,做出相应的属性改变
2、其次是比较元素节点的文本内容是否发生变化('button'
VNode
对象的children
值),如果内容变化则更新新内容
至此,当数据变化时patch
方法更新DOM
元素的过程就结束了(依本例解析)。
总结
本文主要解析了当数据改变时如何触发全局依赖的执行,重新更新DOM
元素。当点击按钮时会触发数据的修改,从而触发set
钩子函数的执行。set
钩子函数中将执行trigger
触发器,异步执行全局依赖方法,重新执行render
函数生成新的VNode
对象,然后调用patch
方法重新渲染DOM。🐶