Vue3源码解析之 render(三)

760 阅读13分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 10 篇,关注专栏

前言

前面两篇我们分析了 render 函数是如何对 DOM 进行渲染、更新、删除的。除此之外,HTML 标签属性、DOM 属性,比如 typevalue 等,以及 Style 样式、Event 事件的挂载及更新,Vue 是如何处理的呢?那么本篇就来对这些情况一探究竟。

案例一

首先引入 hrender 两个函数,设置 vnodetextarea 类型,并设置了 classtypevalue 三个 props 属性,最后通过 render 函数渲染。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
</head>

<body>
    <div id="app"></div>
    <script>
        const { h, render } = Vue

        const vnode = h(
            'textarea',
            {
                class: 'test-class',
                type: 'text',
                value: 'textarea value'
            },
        )

        render(vnode, document.querySelector('#app'))
    </script>
</body>

</html>

渲染结果:

render-textarea.png

render HTML 标签属性和 DOM 属性

我们知道 render 函数属性的挂载更新执行的是 patchProp 方法,该方法定义在 packages/runtime-dom/src/patchProp.ts 文件中:

export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
  if (key === 'class') {
    // runtime-dom/src/modules/class.ts 中
    patchClass(el, nextValue, isSVG)
  } else if (key === 'style') {
    patchStyle(el, prevValue, nextValue)
  } else if (isOn(key)) {
    // ignore v-model listeners
    // 是否为 v-model事件 onUpdate:
    if (!isModelListener(key)) {
      patchEvent(el, key, prevValue, nextValue, parentComponent)
    }
  } else if (
    key[0] === '.'
      ? ((key = key.slice(1)), true)
      : key[0] === '^'
      ? ((key = key.slice(1)), false)
      : shouldSetAsProp(el, key, nextValue, isSVG)
  ) {
    patchDOMProp(
      el,
      key,
      nextValue,
      prevChildren,
      parentComponent,
      parentSuspense,
      unmountChildren
    )
  } else {
    // special case for <input v-model type="checkbox"> with
    // :true-value & :false-value
    // store value as dom properties since non-string values will be
    // stringified.
    if (key === 'true-value') {
      ;(el as any)._trueValue = nextValue
    } else if (key === 'false-value') {
      ;(el as any)._falseValue = nextValue
    }
    patchAttr(el, key, nextValue, isSVG, parentComponent)
  }
}

根据案例一,当前设置了 classtypevalue 三个属性,那么 patchProp 方法则会调用三次。第一次 class 属性执行的是 patchClass 方法,前面我们已经分析过,该方法实际是执行 el.className 对其赋值。第二次 type 属性,根据判断执行 patchAttr 方法,该方法定义在 packages/runtime-dom/src/modules/attrs.ts 文件中:

export function patchAttr(
  el: Element,
  key: string,
  value: any,
  isSVG: boolean,
  instance?: ComponentInternalInstance | null
) {
  // 是否为 svg
  if (isSVG && key.startsWith('xlink:')) {
    if (value == null) {
      el.removeAttributeNS(xlinkNS, key.slice(6, key.length))
    } else {
      el.setAttributeNS(xlinkNS, key, value)
    }
  } else {
    if (__COMPAT__ && compatCoerceAttr(el, key, value, instance)) {
      return
    }

    // note we are only checking boolean attributes that don't have a
    // corresponding dom prop of the same name here.
    const isBoolean = isSpecialBooleanAttr(key)
    if (value == null || (isBoolean && !includeBooleanAttr(value))) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, isBoolean ? '' : value)
    }
  }
}

根据判断执行 el.setAttribute 来设置 type 属性的值,此时 el 就包含了 classtype 两个属性:

render-type.png

第三次 value 属性,这里我们要关注下这么一段判断逻辑:

else if (
    key[0] === '.'
      ? ((key = key.slice(1)), true)
      : key[0] === '^'
      ? ((key = key.slice(1)), false)
      : shouldSetAsProp(el, key, nextValue, isSVG)
  )

再看下 shouldSetAsProp 方法:

function shouldSetAsProp(
  el: Element,
  key: string,
  value: unknown,
  isSVG: boolean
) {
  if (isSVG) {
    // most keys must be set as attribute on svg elements to work
    // ...except innerHTML & textContent
    if (key === 'innerHTML' || key === 'textContent') {
      return true
    }
    // or native onclick with function values
    if (key in el && nativeOnRE.test(key) && isFunction(value)) {
      return true
    }
    return false
  }

  // these are enumerated attrs, however their corresponding DOM properties
  // are actually booleans - this leads to setting it with a string "false"
  // value leading it to be coerced to `true`, so we need to always treat
  // them as attributes.
  // Note that `contentEditable` doesn't have this problem: its DOM
  // property is also enumerated string values.
  if (key === 'spellcheck' || key === 'draggable' || key === 'translate') {
    return false
  }

  // #1787, #2840 form property on form elements is readonly and must be set as
  // attribute.
  if (key === 'form') {
    return false
  }

  // #1526 <input list> must be set as attribute
  if (key === 'list' && el.tagName === 'INPUT') {
    return false
  }

  // #2766 <textarea type> must be set as attribute
  if (key === 'type' && el.tagName === 'TEXTAREA') {
    return false
  }

  // native onclick with string value, must be set as attribute
  if (nativeOnRE.test(key) && isString(value)) {
    return false
  }

  return key in el
}

该方法主要根据 key 判断是否为 DOM 的属性,我们调试看下:

render-value.png

之后执行 patchDOMProp 方法,该方法定义在 packages/runtime-dom/src/modules/props.ts 文件中:

export function patchDOMProp(
  el: any,
  key: string,
  value: any,
  // the following args are passed only due to potential innerHTML/textContent
  // overriding existing VNodes, in which case the old tree must be properly
  // unmounted.
  prevChildren: any,
  parentComponent: any,
  parentSuspense: any,
  unmountChildren: any
) {
  if (key === 'innerHTML' || key === 'textContent') {
    if (prevChildren) {
      unmountChildren(prevChildren, parentComponent, parentSuspense)
    }
    el[key] = value == null ? '' : value
    return
  }

  if (
    key === 'value' &&
    el.tagName !== 'PROGRESS' &&
    // custom elements may use _value internally
    !el.tagName.includes('-')
  ) {
    // store value as _value as well since
    // non-string values will be stringified.
    el._value = value
    const newValue = value == null ? '' : value
    if (
      el.value !== newValue ||
      // #4956: always set for OPTION elements because its value falls back to
      // textContent if no value attribute is present. And setting .value for
      // OPTION has no side effect
      el.tagName === 'OPTION'
    ) {
      el.value = newValue
    }
    if (value == null) {
      el.removeAttribute(key)
    }
    return
  }

根据判断执行 el.value = newValue 来设置 DOMvalue 值,最后执行 insert 方法挂载 DOM,此时页面呈现:

render-textarea.png

我们可以看到,设置属性值有的通过 setAttribute 方法,有的通过 .className.value 等方式,那这个有什么区别呢?这里就需要了解下 HTML 标签属性DOM 属性,大家可以点击跳转对应的文档进行查看。el.setAttribute 是设置指定元素上的某个属性,而 dom.xx 是直接修改指定对象的属性。

案例二

首先引入 hrender 两个函数,先渲染 vnode1 元素,style 样式设置为 color: 'red',两秒后更新为相同类型和文本的 vnode2 元素,style 样式设置为 fontSize: '32px'

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue

      const vnode1 = h(
        'div',
        {
          style: {
            color: 'red'
          }
        },
        'hello world'
      )

      render(vnode1, document.querySelector('#app'))

      setTimeout(() => {
        const vnode2 = h(
          'div',
          {
            style: {
              fontSize: '32px'
            }
          },
          'hello world'
        )

        render(vnode2, document.querySelector('#app'))
      }, 2000)
    </script>
  </body>
</html>

render Style 属性

上面讲解到属性的挂载更新都会执行 patchProp 方法,根据判断:

else if (key === 'style') {
    patchStyle(el, prevValue, nextValue)
  }

执行 patchStyle 方法,它定义在 packages/runtime-dom/src/modules/style.ts 文件中:

export function patchStyle(el: Element, prev: Style, next: Style) {
  const style = (el as HTMLElement).style // 从 el 获取 style 属性
  const isCssString = isString(next) // next { color: red } vue 对其增强 可接收 数组 对象 字符串
  if (next && !isCssString) {
    for (const key in next) {
      setStyle(style, key, next[key])
    }
    // 该逻辑为了处理更新时 旧的有没存在 做删除处理
    if (prev && !isString(prev)) {
      for (const key in prev) {
        if (next[key] == null) {
          setStyle(style, key, '')
        }
      }
    }
  } else {
    const currentDisplay = style.display
    if (isCssString) {
      if (prev !== next) {
        style.cssText = next as string
      }
    } else if (prev) {
      el.removeAttribute('style')
    }
    // indicates that the `display` of the element is controlled by `v-show`,
    // so we always keep the current `display` value regardless of the `style`
    // value, thus handing over control to `v-show`.
    if ('_vod' in el) {
      style.display = currentDisplay
    }
  }
}

该方法先从 el 元素中获取 style 属性,然后判断新节点的属性是否为字符串,当前传入的属性为 { color: 'red' },所以为 false。由于 Vueprops 属性增强,可以接收对象、数组、字符串等类型。根据判断遍历执行 setStyle 方法:

function setStyle(
  style: CSSStyleDeclaration,
  name: string,
  val: string | string[]
) {
  if (isArray(val)) {
    val.forEach(v => setStyle(style, name, v)) // 递归处理 当前 val 为 red
  } else {
    if (val == null) val = ''
    if (name.startsWith('--')) {
      // custom property definition
      style.setProperty(name, val)
    } else {
      const prefixed = autoPrefix(style, name) // 缓存处理
      if (importantRE.test(val)) {
        // !important
        style.setProperty(
          hyphenate(prefixed),
          val.replace(importantRE, ''),
          'important'
        )
      } else {
        style[prefixed as any] = val // 即 style.color = red
      }
    }
  }
}

当前 namecolorval 值为 red,之后执行 const prefixed = autoPrefix(style, name) 逻辑,我们再看下 autoPrefix 方法:

const prefixes = ['Webkit', 'Moz', 'ms']
const prefixCache: Record<string, string> = {}

function autoPrefix(style: CSSStyleDeclaration, rawName: string): string {
  const cached = prefixCache[rawName]
  if (cached) {
    return cached
  }
  let name = camelize(rawName)
  if (name !== 'filter' && name in style) {
    return (prefixCache[rawName] = name)
  }
  name = capitalize(name)
  for (let i = 0; i < prefixes.length; i++) {
    const prefixed = prefixes[i] + name
    if (prefixed in style) {
      return (prefixCache[rawName] = prefixed)
    }
  }
  return rawName
}

const camelizeRE = /-(\w)/g
/**
 * @private
 */
export const camelize = cacheStringFunction((str: string): string => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})

该方法主要做了 缓存处理 以及通过 camelize 方法将 style 属性带有 - 的转换为驼峰形式,比如 font-size 转换为 fontSize,最后返回处理后的 name

之后执行 style[prefixed as any] = valstyle['color'] = 'red' 设置样式,render 函数执行完毕,页面呈现:

render-style.png

两秒后更新节点,重新修改 style 样式,又会再次触发 patchStyle 方法,其中有这么一段逻辑:

export function patchStyle(el: Element, prev: Style, next: Style) {
  const style = (el as HTMLElement).style // 从 el 获取 style 属性
  const isCssString = isString(next) // next { color: red } vue 对其增强 可接收 数组 对象 字符串
  if (next && !isCssString) {
    for (const key in next) {
      setStyle(style, key, next[key])
    }
    
    // 该逻辑为了处理更新时 旧的有没存在 做删除处理
    if (prev && !isString(prev)) {
      for (const key in prev) {
        if (next[key] == null) {
          setStyle(style, key, '')
        }
      }
    }
  } else {
    // 省略
  }
}

先遍历执行 setStyle 方法设置属性 fontSize:'32px'

render-patch-style.png

之后执行 if (prev && !isString(prev)) 判断逻辑,由于存在旧节点,且旧节点 style 中的 color 属性不存在新节点的 style 中,则执行 setStyle 方法对 color 属性做清空处理,最终页面呈现:

render-patch-style2.png

案例三

首先引入 hrender 两个函数,先渲染类型为 buttonvnode1 元素,添加 onClick 点击事件,两秒后更新渲染相同类型的 vnode2 元素,事件修改为 onDblclick 双击事件。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue

      const vnode1 = h(
        'button',
        {
          onClick() {
            alert('点击')
          }
        },
        '点击'
      )

      render(vnode1, document.querySelector('#app'))

      setTimeout(() => {
        const vnode2 = h(
          'button',
          {
            onDblclick() {
              alert('双击')
            }
          },
          '双击'
        )

        render(vnode2, document.querySelector('#app'))
      }, 2000)
    </script>
  </body>
</html>

render Event 事件

我们知道属性事件的挂载更新都会执行 patchProp 方法,根据判断:

else if (isOn(key)) {
    // ignore v-model listeners
    // 是否为 v-model事件 onUpdate:
    if (!isModelListener(key)) {
      patchEvent(el, key, prevValue, nextValue, parentComponent)
    }
  } 

isModelListener 方法判断是否为 v-model 双向绑定事件:

export const isModelListener = (key: string) => key.startsWith('onUpdate:')

之后执行 patchEvent 方法,该方法定义在 packages/runtime-dom/src/modules/events.ts 文件中:

export function patchEvent(
  el: Element & { _vei?: Record<string, Invoker | undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {})
  const existingInvoker = invokers[rawName] // 缓存
  if (nextValue && existingInvoker) {
    // patch
    existingInvoker.value = nextValue
  } else {
    // 拆解为小写 是为了 addEventListener 添加事件中 type 是小写
    const [name, options] = parseName(rawName) 
    if (nextValue) {
      // add 事件进行缓存 invokers[rawName]
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      addEventListener(el, name, invoker, options) // 添加事件
    } else if (existingInvoker) {
      // remove
      removeEventListener(el, name, existingInvoker, options)
      invokers[rawName] = undefined
    }
  }
}

这里引入了 vei 缓存概念,我们稍后再来分析。第一次渲染,缓存不存在,执行 else 逻辑,之后对 rawName 进行拆解,当前 rawNameonClick,再看下 parseName 方法:

const optionsModifierRE = /(?:Once|Passive|Capture)$/

function parseName(name: string): [string, EventListenerOptions | undefined] {
  let options: EventListenerOptions | undefined
  if (optionsModifierRE.test(name)) {
    options = {}
    let m
    while ((m = name.match(optionsModifierRE))) {
      name = name.slice(0, name.length - m[0].length)
      ;(options as any)[m[0].toLowerCase()] = true
    }
  }
  return [hyphenate(name.slice(2)), options]
}

const hyphenateRE = /\B([A-Z])/g
/**
 * @private
 */
export const hyphenate = cacheStringFunction((str: string) =>
  str.replace(hyphenateRE, '-$1').toLowerCase()
)

该方法主要是对 事件名 前两位进行截取,并将剩余的转为小写,即 onClick 拆解为 click,因为 addEventListener 注册事件中 type 参数对大小写敏感,一般为小写。

之后执行 invokers[rawName] = createInvoker() 来缓存事件,我们再看下 createInvoker 方法:

function createInvoker(
  initialValue: EventValue,
  instance: ComponentInternalInstance | null
) {
  const invoker: Invoker = (e: Event) => {
    // async edge case #6566: inner click event triggers patch, event handler
    // attached to outer element during patch, and triggered again. This
    // happens because browsers fire microtask ticks between event propagation.
    // the solution is simple: we save the timestamp when a handler is attached,
    // and the handler would only fire if the event passed to it was fired
    // AFTER it was attached.
    const timeStamp = e.timeStamp || _getNow()

    if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
      // 点击后触发该事件 本质上还是触发事件,只是做了 try catch 错误拦截
      callWithAsyncErrorHandling(
        patchStopImmediatePropagation(e, invoker.value),
        instance,
        ErrorCodes.NATIVE_EVENT_HANDLER,
        [e]
      )
    }
  }
  // 为传入的点击函数 onClick() { alert('点击')} 相当于 invoker.value被触发 即点击行为被触发
  invoker.value = initialValue 
  invoker.attached = getNow()
  return invoker // 返回 invoker 函数
}

该方法创建了一个 invoker 函数,并将传入的函数即 onClick() { alert('点击') } 赋值给了 invoker.value 上,之后执行 addEventListener 方法注册事件:

export function addEventListener(
  el: Element,
  event: string,
  handler: EventListener,
  options?: EventListenerOptions
) {
  // 这里的 handler 为 invoker 函数,并没有把真正事件传入,而是通过 invoker.value 来触发
  el.addEventListener(event, handler, options) 
}

此时 el._veiinvokers 对象中就存在 onClick 注册事件的缓存:

render-event-invokers.png

按钮渲染完毕,点击触发 createInvoker 方法中 invoker 函数的 callWithAsyncErrorHandling 方法:

render-event-invoker.png

该方法我们在前面的文章中也提到过,主要是对事件执行 try catch 错误的拦截:

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
  }

  const values = []
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
  }
  return values
}

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
}

我们再看下 patchStopImmediatePropagation 方法,这里将 invoker.value 参数传入,最终又返回了该方法,即 onClick() { alert('点击') }

function patchStopImmediatePropagation(
  e: Event,
  value: EventValue
): EventValue {
  if (isArray(value)) {
    const originalStop = e.stopImmediatePropagation
    e.stopImmediatePropagation = () => {
      originalStop.call(e)
      ;(e as any)._stopped = true
    }
    return value.map(fn => (e: Event) => !(e as any)._stopped && fn && fn(e))
  } else {
    return value
  }
}

综上,我们可以理解为事件的触发,实际执行的是 invoker.value,即传入的事件 onClick() { alert('点击') }

Vue 为什么要这么处理呢?和 el._vei 有什么关联呢?在讲解之前,我们先看下事件的更新。

根据案例三,两秒后重新更新节点事件,执行 patchEvent 方法。我们发现这里和 classstyle 有点区别,没有卸载事件,那么在哪里触发的呢?我们回过来再看下 renderer.ts 文件中 patchProps 方法:

const patchProps = (
    el: RendererElement,
    vnode: VNode,
    oldProps: Data,
    newProps: Data,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean
  ) => {
    if (oldProps !== newProps) {
      for (const key in newProps) {
        // empty string is not valid prop
        if (isReservedProp(key)) continue
        const next = newProps[key]
        const prev = oldProps[key]
        // defer patching value
        if (next !== prev && key !== 'value') {
          hostPatchProp(
            el,
            key,
            prev,
            next,
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
      if (oldProps !== EMPTY_OBJ) {
        for (const key in oldProps) {
          if (!isReservedProp(key) && !(key in newProps)) {
            hostPatchProp(
              el,
              key,
              oldProps[key],
              null,
              isSVG,
              vnode.children as VNode[],
              parentComponent,
              parentSuspense,
              unmountChildren
            )
          }
        }
      }
      if ('value' in newProps) {
        hostPatchProp(el, 'value', oldProps.value, newProps.value)
      }
    }
  }

根据判断 if (oldProps !== EMPTY_OBJ) 会再次触发 patchProp 方法,也就是我们说的卸载行为,之后执行 patchEvent 方法:

export function patchEvent(
  el: Element & { _vei?: Record<string, Invoker | undefined> },
  rawName: string,
  prevValue: EventValue | null,
  nextValue: EventValue | null,
  instance: ComponentInternalInstance | null = null
) {
  // vei = vue event invokers
  const invokers = el._vei || (el._vei = {})
  const existingInvoker = invokers[rawName] // 缓存
  if (nextValue && existingInvoker) {
    // patch
    existingInvoker.value = nextValue
  } else {
    // 拆解为小写 是为了 addEventListener 添加事件中 type 是小写
    const [name, options] = parseName(rawName) 
    if (nextValue) {
      // add 事件进行缓存 invokers[rawName]
      const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
      addEventListener(el, name, invoker, options) // 添加事件
    } else if (existingInvoker) {
      // remove
      removeEventListener(el, name, existingInvoker, options)
      invokers[rawName] = undefined
    }
  }
}

此时 el._vei 存在 onClickonDblClick 两个事件缓存:

render-patch-event.png

当前旧节点的事件为 onClick,由于此时 existingInvoker 存在且 nextValue 不存在,走 else if (existingInvoker) 判断逻辑,执行 removeEventListener 移除旧节点 onClick 事件,并将缓存事件对应的值清空 invokers[rawName] = undefined,至此事件更新完毕。

render 缓存事件

上面我们还遗留了一个问题,事件的触发实际执行的是 invoker.value,为什么不是直接执行 invoker 函数呢?我们先看下正常情况下是如何添加和移除事件的:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button>点击</button>
    <script>
      // 获取 button 元素
      const btnEle = document.querySelector('button')
      // 声明 invoker 函数
      const invoker = () => {
        alert('hello')
      }
      // 注册 click 事件
      btnEle.addEventListener('click', invoker)
      // 两秒后更新事件
      setTimeout(() => {
        // 移除 click 事件 
        btnEle.removeEventListener('click', invoker)
        // 注册新事件
        btnEle.addEventListener('click', () => {
          alert('你好')
        })
      }, 2000)
    </script>
  </body>
</html>

再看下使用 value 形式添加和移除事件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button>点击</button>
    <script>
      // 获取 button 元素
      const btnEle = document.querySelector('button')
      // 声明 invoker 函数 调用执行 invoker.value
      const invoker = () => {
        invoker.value()
      }
      // value 赋值事件
      invoker.value = () => {
        alert('hello')
      }
      // 注册事件
      btnEle.addEventListener('click', invoker)
      // 两秒后重新修改 value 值
      setTimeout(() => {
        invoker.value = () => {
          alert('你好')
        }
      }, 2000)
    </script>
  </body>
</html>

我们知道添加移除事件都是比较耗性能的,而 vei 对象的作用通过调用和修改 value 属性值从而提升性能,它充当着一个事件缓存器。

总结

  1. 属性和事件的挂载更新都会执行 patchProp 方法。
  2. class 属性的挂载更新执行的是 patchClass 方法。
  3. style 属性的挂载更新执行的是 patchStyle 方法。
  4. HTML 标签 属性的挂载更新执行的是 patchAttr 方法。
  5. DOM 属性的挂载更新执行的是 patchDOMProp 方法。
  6. Event 事件 的挂载更新执行的是 patchEvent 方法。
  7. Event 事件 的触发实际执行的是 invoker.value
  8. vei 对象充当着一个事件缓存器,提升性能的作用。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
  12. Vue3源码解析之 render component(一)
  13. Vue3源码解析之 render component(二)
  14. Vue3源码解析之 render component(三)
  15. Vue3源码解析之 render component(四)
  16. Vue3源码解析之 render component(五)
  17. Vue3源码解析之 diff(一)
  18. Vue3源码解析之 diff(二)
  19. Vue3源码解析之 compiler(一)
  20. Vue3源码解析之 compiler(二)
  21. Vue3源码解析之 compiler(三)
  22. Vue3源码解析之 createApp