The previous part mainly analyzed the patch method to convert VNode objects into real DOM nodes. This article dissects the update process after changing the data (click the button -> Data Update -> DOM Update)
Click the Button button to perform the callback
AddEventListener adds an element’s event listener to a button (juejin.cn/post/701288…) . An event listener for Click is triggered when a button is clicked, and the invoker method is executed
// Method attribute assignment
invoker.value = initialValue
invoker.attached = getNow()
// invoker - event listener callback function
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]
)
}
}
Copy the code
Can see inside the invoker method performed callWithAsyncErrorHandling function, the first parameter is the patchStopImmediatePropagation function return values, look at the internal implementation of this function
function patchStopImmediatePropagation(e: Event, value: EventValue) :EventValue {
if (isArray(value)) {
/ /... The value as an array
} else {
return value
}
}
Copy the code
Internal patchStopImmediatePropagation methods, judge the invoker. Whether the value as an array, invoker. The value of the value is to click the corresponding attribute values, this example for modifyMessage method. Then look at the internal implementation of the callWithAsyncErrorHandling function
// callWithErrorHandling method definition
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 method definition
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
}
// ...
}
Copy the code
CallWithAsyncErrorHandling within a method to determine whether a fn for function (in this case is fn modifyMessage method), so the execution callWithErrorHandling method, Inside the callWithErrorHandling function is the fn method, modifyMessage method, and the internal implementation of modifyMessage method is returned
const modifyMessage = () = > {
message.value = 'Modified test data'
}
Copy the code
Modifying Proxy data triggers the set hook function to execute
The modifyMessage method modifyMessage modifies the value of message.value. Message is a RefImpl instance object. The RefImpl class sets the set hook function for value. The set function is triggered when the button is clicked to modify the value of message.value
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)
}
}
}
Copy the code
The set hook function first calls the hasChanged method to determine whether the value hasChanged
// NaN ! == NaN
export const hasChanged = (value: any, oldValue: any): boolean= >value ! == oldValue && (value === value || oldValue === oldValue)Copy the code
If value and oldValue are different, the _rawValue attribute is assigned to the newVal value. _value is assigned to convert(newVal). The convert method is used to determine whether newVal is a reference. Reference type data returns the return value of Reactive (newVal). The trigger trigger method is then called to trigger a DOM update with arguments (RefImpl instance object, ‘set’, ‘value’, newVal). Next, I’ll examine the internal implementation of the trigger method
Global trigger trigger executes
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)
}
Copy the code
Trigger function internally first obtains the corresponding value of target(RefImpl instance object of message) from the global WeakMap targetMap object. For information about targetMap objects, see dependency collection targetMap objects. So the value of depsMap is a Map object, and since the key is a string ‘value’, not void 0, we call the add method defined inside the function and pass depsmap. get(key), ActiveEffect is a Set object that stores the global variable activeEffect. Take a look at the internal implementation of the Add method
/ / the add method
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
if(effect ! == activeEffect || effect.allowRecurse/* true */) {
effects.add(effect)
}
})
}
}
Copy the code
The add method is used to add effectstos Each element in the Set object is added to effects (a Set variable defined inside the trigger function). (The value for effect.allowRecurse is defined in the second Options object passed in by the effect method in the setupRenderEffect function See global dependency ActiveEffects. Then regress to the trigger function and execute the run method for each element in the cyclic effects as a parameter. In this case, it is the global variable activeEffect. The run method is also defined inside the trigger function, so let’s look at its internal implementation
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()
}
}
Copy the code
First, determine the value of effect.options.onTrigger, which is the options parameter passed in by calling effect method in setupRenderEffect method. The value of onTrigger is void 0. The global variable is a reactiveEffect function because queueJob is called from the scheduler with an activeEffect argument. Let’s take a look at the internal implementation of the queueJob function
Global dependencies are placed in asynchronous execution queues
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 definition
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) // Add the job element to queue
} else {
queue.push(job)
}
queueFlush()
}
}
Copy the code
The main function inside this function is to add activeEffect to the global queue and then call 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)
}
}
Copy the code
Internally, the queueFlush method first sets the isFlushPending variable to true, indicating that it is waiting, because the Promise method is subsequently executed, then promise.resolve (), and the flushJobs callback function (asynchronous) is executed in then. Take a look at the internal implementation of the flushJobs method
function flushJobs(seen? : CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map(a)// seen = new Map()
}
flushPreFlushCbs(seen) PendingPreFlushCbs data is empty, so the function returns directly
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)
}
}
}
Copy the code
The flushJobs method shows that isFlushPending is set to false and isFlushing is set to true. Because the Promise is already in the completed state when the flushJobs method starts executing, the position is identified. Order the dependencies in a queue in ascending order by the value of their ID. We then loop through the element (job) in the queue, first calling the checkRecursiveUpdates method to determine if the seen(Map) job does not exist, and then add it to the SEEN (key: job, value: 1). Finally, callWithErrorHandling is called to execute the job element, which is the global activeEffect variable known as the reactiveEffect method.
Perform global dependencies in asynchronous queues
So when the first rendering is complete, modifying the data triggers the reactiveEffect function to execute again. For instructions on how to execute a reactiveEffect function while initializing a hang, see global dependency reactiveEffect. From the first execution of the reactiveEffect method, you can see that this method is mainly executing the FN function passed in by calling the createReactiveEffect function. That’s the componentEffect function passed in by the Effect method inside the setupRenderEffect method.
Recall that Render generates VNode objects and Patch generates DOM nodes when calling componentEffect for the first time. Let’s go back to this function here
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 teleporthostParentNode(prevTree.el!) ! .// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
// ...
next.el = nextTree.el
// ...}}Copy the code
Because the instance.isMounted property is set to true after the initial DOM rendering is complete, the else branch will be performed when the method is executed again after modifying the data. RenderComponentRoot is called in the else branch to re-execute the Render method. The render generated VNode object process can be reviewed in the previous article, which is not described here. After modifying the value of message, the generated VNode object changes because the children property value of type=Symbol(Text) VNode object is the changed data content, in this case, “modified test data”.
Patch method updates DOM
Then the patch method is called to change the DOM node accordingly. Parameters as follows:
PrevTree = instance. SubTree = prevTree = instance. SubTree = prevTree = instance. New VNode 3, hostParentNode(prevtree.el!) ! The return value of the prevtree. el property is the empty text node, which can be seen in the patch parse VNode. And the empty of the parent element is # 4, getNextHostNode app node (prevTree) method return values, the method to obtain is prevTree anchor. NextSibling. Retrieves the next node adjacent to the Prevtree. anchor node. Through analytical VNode node can know prevTree patch. The anchor is # app node’s right child nodes (blank text nodes), so prevTree. Anchor. NextSibling value is null, the last three parameters: Instance object, parentSuspense = null, isSVG = false
Then take a look at the process of executing the patch method when the data changes to generate a new VNode object (see the previous article for initializing the process of executing the patch method to generate a DOM node).
First of all, the root VNode object is still a Type =Symbol(Fragment) object, so call the processFragment method to see the implementation inside the method
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) {/* When the data changes, n1 exists and is the old 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
)
// ...}}Copy the code
Different from the initial render DOM, the old VNode will be passed in the update, so n1 is not equal to null. Execute the else branch to check that both the old VNode and the new VNode object have dynamicChildren property, and execute the patchBlockChildren method to process the child object. Take a look at its internal implementation
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]
constcontainer = 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)}}Copy the code
The patchBlockChildren function loops the array of new VNode objects, first parses the type=Symbol(Text) Text child nodes, IsSameVNodeType specifies whether the VNode type is the same as the VNode type and whether the type and key are the same. Then pass the corresponding old and new child node objects and container objects and other parameters into the Patch method to start updating the first text DOM element
Dom. nodeValue Modifies node content
Return to the patch method again, consistent with the initial rendering, because type=Symbol(Text), again calling the processText method
// processText
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) = > {
if (n1 == null) {/ *... Initialize render */}
else {
const el = (n2.el = n1.el!)
if(n2.children ! == n1.children) { hostSetText(el, n2.childrenas string)
}
}
}
Copy the code
El equals n1.el, the el property of the old VNode object. Recall that initializing processText parses the text object. The actual N1.el is the text DOM object initialized because n2.children! == N1.children (message.value changed), so call hostSetText, which corresponds to setText in the nodeOps object
setText: (node, text) = > {
node.nodeValue = text
}
Copy the code
Setting the nodeValue property of the text DOM object to the new message.value value (” modified test data “) changes the content of the page.
The element object of type=’button’ is then parsed. First of all, the processElement method is called, since the old VNode exists because of the render update, and the patchElement method is called to compare the elements
< div style = “max-width: 100%; clear: both; min-max-width: 100%; If the content changes, update the new content
At this point, the patch method is done updating the DOM element when the data changes (as resolved in this example).
conclusion
This article focuses on how to trigger the execution of global dependencies to re-update DOM elements when data changes. When the button is clicked, the data is modified, triggering the execution of the set hook function. The set hook function executes trigger triggers, asynchronously executes globally dependent methods, re-executes the render function to generate a new VNode object, and then calls the Patch method to re-render the DOM. 🐶