Vue3追本溯源(九)数据更新触发DOM更新

2,019 阅读7分钟

上篇主要解析了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是否为函数(本例fnmodifyMessage方法),所以执行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)

如果valueoldValue不相同,则将_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(messageRefImpl实例对象)对应的值。关于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方法,本例为全局变量activeEffectrun方法也是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置为falseisFlushing置为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 = nullisSVG = 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。🐶