上篇主要解析了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。🐶