【译】Vue.js 下一代实现指南 - 下卷

前言

vue 有个社区贡献者 ubugeeei(推特/X ID: @ubugeeei)是 vue 官方 Discord 的长期活跃者,经常解答新特性、源码实现相关问题,他对 vue 的源码非常熟悉,也是 Vue Vapor(Vue 无虚拟 DOM 草案)的早期研究者之一,他发布的 "Reading vuejs/core-vapor" 阅读指南站点旨在帮助读者完整梳理 Vapor 模式的编译输出与实现细节,由于 ubugeeei 是一个日本开发者,这个站点只有日语和英语,并且中文互联网上关于这本书的介绍很少,而且我觉得内容很好,因此这里我将这个网站内容翻译成 中文 供大家阅读,由于掘金单篇文章字符限制问题,这里拆分成三部曲进行发布,本篇为下卷

原文链接:ubugeeei.github.io/reading-vue...

关于作者

  • ubugeeei
  • Vue.js 成员和 Vue.js 日本用户组的核心成员。

  • 从 2023 年 11 月起参与 Vapor 模式的发展。

  • 2023 年 12 月成为 vuejs/core-vapor 的外部合作者。

  • 2024 年 4 月加入 Vue.js 组织,成为 Vapor 团队的一员。

作者博客:ublog.dev/

Vue Vapor 的实现最初始于一个名为 vuejs/core-vapor 的仓库,但在 2024 年 10 月更名为 vuejs/vue-vapor。在本文档中,链接已更改为 vuejs/vue-vapor,但由于本文档的项目名称和页面,文本已统一为 vuejs/core-vapor。注意,在你阅读时 vuejs/core-vapor = vuejs/vue-vapor。它们在时间线上是不同的名称,但指的是完全相同的事物。

v-on 指令

现在让我们看一下 v-on 指令。
v-on 有两种类型:一种用于原生元素,一种用于组件。由于我还不知道如何处理组件,所以我将在这里解释用于原生元素的那种。

(顺便说一下,当涉及到组件时,它主要是 props,所以没有太多需要解释的。)

让我们考虑一个像下面这样的组件。

vue 复制代码
<script setup>
import { ref } from "vue";

const count = ref(0);
function increment() {
  count.value++;
}
</script>

<template>
  <button type="button" @click="increment">{{ count }}</button>
</template>

这是一个常见的计数器组件。

编译结果

编译结果如下所示。

js 复制代码
const _sfc_main = {
  vapor: true,
  name: "App",
  setup(props, { expose: expose }) {
    expose();

    const count = ref(0);
    function increment() {
      count.value++;
    }

    const __returned__ = { count, increment, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import {
  delegate as _delegate,
  renderEffect as _renderEffect,
  setText as _setText,
  delegateEvents as _delegateEvents,
  template as _template,
} from "vue/vapor";

const t0 = _template('<button type="button"></button>');

_delegateEvents("click");

function _sfc_render(_ctx) {
  const n0 = t0();
  _delegate(n0, "click", () => _ctx.increment);
  _renderEffect(() => _setText(n0, _ctx.count));
  return n0;
}

像往常一样,脚本部分不是很重要,所以让我们专注于以下部分。

js 复制代码
import {
  delegate as _delegate,
  renderEffect as _renderEffect,
  setText as _setText,
  delegateEvents as _delegateEvents,
  template as _template,
} from "vue/vapor";

const t0 = _template('<button type="button"></button>');

_delegateEvents("click");

function _sfc_render(_ctx) {
  const n0 = t0();
  _delegate(n0, "click", () => _ctx.increment);
  _renderEffect(() => _setText(n0, _ctx.count));
  return n0;
}

理解概述

模板的生成和 renderEffect ~ setText 像往常一样。

这次,主要部分是

js 复制代码
_delegateEvents("click");

js 复制代码
_delegate(n0, "click", () => _ctx.increment);

正如预期的那样,后者可能是向 n0 添加 click 事件。

然而,我不理解"delegate"是什么意思,或者前面的 _delegateEvents 在做什么。

现在,让我们暂时将这个作为一个谜,看看编译器的实现。

当我们继续阅读运行时时,我们将理解这个谜。

阅读编译器

IR

像往常一样,让我们看一下 IR

有一个可疑的东西叫做 SET_EVENT,但我没有看到其他东西。

让我们看一看。

ts 复制代码
export interface SetEventIRNode extends BaseIRNode {
  type: IRNodeTypes.SET_EVENT
  element: number
  key: SimpleExpressionNode
  value?: SimpleExpressionNode
  modifiers: {
    // modifiers for addEventListener() options, e.g. .passive & .capture
    options: string[]
    // modifiers that needs runtime guards, withKeys
    keys: string[]
    // modifiers that needs runtime guards, withModifiers
    nonKeys: string[]
  }
  keyOverride?: KeyOverride
  delegate: boolean
  /** Whether it's in effect */
  effect: boolean
}

似乎这个 Node 有一个 delegate 标志。

然后,让我们寻找生成这个 Node 的转换器。

找到了。它是 packages/compiler-vapor/src/transforms/vOn.ts

ts 复制代码
  const operation: SetEventIRNode = {
    type: IRNodeTypes.SET_EVENT,
    element: context.reference(),
    key: arg,
    value: exp,
    modifiers: {
      keys: keyModifiers,
      nonKeys: nonKeyModifiers,
      options: eventOptionModifiers,
    },
    keyOverride,
    delegate,
    effect: !arg.isStatic,
  }

DirectiveTransform

由于这是 DirectiveTransform 第一次出现,让我们看看它是如何被调用的。

DirectiveTransform 是从 transformElement 调用的。

具体来说,它是在处理元素属性期间被调用的。

ts 复制代码
export const transformElement: NodeTransform = (node, context) => {

ts 复制代码
    const propsResult = buildProps(
      node,
      context as TransformContext<ElementNode>,
      isComponent,
    )

ts 复制代码
export function buildProps(
  node: ElementNode,
  context: TransformContext<ElementNode>,
  isComponent: boolean,
): PropsResult {

ts 复制代码
    const result = transformProp(prop, node, context)

ts 复制代码
function transformProp(
  prop: VaporDirectiveNode | AttributeNode,
  node: ElementNode,
  context: TransformContext<ElementNode>,
): DirectiveTransformResult | void {

ts 复制代码
  const directiveTransform = context.options.directiveTransforms[name]
  if (directiveTransform) {
    return directiveTransform(prop, node, context)
  }

在这种情况下,它从像 on 这样的名称中获取 v-on 转换器,并调用 transformVOn

然后,在 transformVOn 中,在最后调用 context.registerEffect,注册效果。

ts 复制代码
  context.registerEffect([arg], operation)

transformVOn

现在,让我们看一下 transformVOn

dir 是指令的 AST。 这是在 runtime-core 中实现的,并在解析阶段创建的。

从这里,我们提取 argexprmodifiers 等。

ts 复制代码
  let { arg, exp, loc, modifiers } = dir

简单解释一下,在 v-on:click.stop="handler" 中,click 对应于 argstop 对应于 modifiershandler 对应于 expr

首先,我们按类型解析和组织 modifiers

ts 复制代码
  const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
    resolveModifiers(
      arg.isStatic ? `on${arg.content}` : arg,
      modifiers,
      null,
      loc,
    )

resolveModifiers 是在 compiler-dom 中实现的函数,用于对修饰符进行分类。

ts 复制代码
export const resolveModifiers = (
  key: ExpressionNode | string,
  modifiers: string[],
  context: TransformContext | null,
  loc: SourceLocation,
): {
  keyModifiers: string[]
  nonKeyModifiers: string[]
  eventOptionModifiers: string[]
} => {

接下来,我们确定是否启用 delegate。(现在,让我们暂时不考虑 delegate 是什么。)

ts 复制代码
  const delegate =
    arg.isStatic && !eventOptionModifiers.length && delegatedEvents(arg.content)

当满足以下所有条件时,它被启用:

  • arg 是静态的
    这是当它不是像 v-on[eventName]="handler" 这样的东西时。
  • modifiers 为空。
  • 它是一个委托目标
    这是对它是否是这里定义的事件的判断。
ts 复制代码
const delegatedEvents = /*#__PURE__*/ makeMap(
  'beforeinput,click,dblclick,contextmenu,focusin,focusout,input,keydown,' +
    'keyup,mousedown,mousemove,mouseout,mouseover,mouseup,pointerdown,' +
    'pointermove,pointerout,pointerover,pointerup,touchend,touchmove,' +
    'touchstart',
)

之后,基于到目前为止获得的信息,用 registerEffect 注册效果完成了这个过程。

ts 复制代码
  const operation: SetEventIRNode = {
    type: IRNodeTypes.SET_EVENT,
    element: context.reference(),
    key: arg,
    value: exp,
    modifiers: {
      keys: keyModifiers,
      nonKeys: nonKeyModifiers,
      options: eventOptionModifiers,
    },
    keyOverride,
    delegate,
    effect: !arg.isStatic,
  }

  context.registerEffect([arg], operation)

阅读代码生成

让我们主要关注 delegate 标志如何影响事物,并略过其余部分。

ts 复制代码
export function genOperation(
  oper: OperationNode,
  context: CodegenContext,
): CodeFragment[] {
ts 复制代码
    case IRNodeTypes.SET_EVENT:
      return genSetEvent(oper, context)
ts 复制代码
export function genSetEvent(
  oper: SetEventIRNode,
  context: CodegenContext,
): CodeFragment[] {
  const { vaporHelper } = context
  const { element, key, keyOverride, value, modifiers, delegate, effect } = oper

  const name = genName()
  const handler = genEventHandler(context, value)
  const eventOptions = genEventOptions()

  if (delegate) {
    // key is static
    context.delegates.add(key.content)
  }

  return [
    NEWLINE,
    ...genCall(
      vaporHelper(delegate ? 'delegate' : 'on'),
      `n${element}`,
      name,
      handler,
      eventOptions,
    ),
  ]

这个流程现在很熟悉,应该没有困难的部分。

我特别想强调的是这里。

ts 复制代码
      vaporHelper(delegate ? 'delegate' : 'on'),

IR 中的 delegate 被启用时,它生成一个 delegate 辅助函数;否则,它生成一个 on 辅助函数。

换句话说,当接下来阅读运行时时,比较这两者应该有助于你理解 delegate 的作用。

你还可以看到,就在这之前,事件被注册在 context.delegates 中。

你也可以理解,这可能是提升的 _delegateEvents("click"); 部分。

ts 复制代码
  if (delegate) {
    // key is static
    context.delegates.add(key.content)
  }

阅读运行时

现在,有三个我想阅读的函数。

它们是 delegateEventsdelegateon

首先,让我们按照执行顺序看一下 delegateEvents

delegateEvents

实现如下。

ts 复制代码
/**
 * Event delegation borrowed from solid
 */
const delegatedEvents = Object.create(null)

export const delegateEvents = (...names: string[]): void => {
  for (const name of names) {
    if (!delegatedEvents[name]) {
      delegatedEvents[name] = true
      // eslint-disable-next-line no-restricted-globals
      document.addEventListener(name, delegatedEventHandler)
    }
  }
}

从注释中可以看出,这个概念似乎是从 Solid 借鉴的。

查看 Solid 的文档,有关于 delegate 的解释。

docs.solidjs.com/concepts/co...

Solid 提供了两种向浏览器添加事件监听器的方式:

  • on:__:向元素添加事件监听器。这也被称为原生事件。

  • on__:向文档添加事件监听器并将其分派到元素。这可以称为委托事件。

委托事件流经组件树,通过在常用事件上表现更好来节省一些资源。然而,原生事件流经 DOM 树,并提供对事件行为的更多控制。

delegateEvents 在文档上监听传递的事件的 delegatedEventHandler

让我们看一下 delegatedEventHandler

ts 复制代码
const delegatedEventHandler = (e: Event) => {
  let node = ((e.composedPath && e.composedPath()[0]) || e.target) as any
  if (e.target !== node) {
    Object.defineProperty(e, 'target', {
      configurable: true,
      value: node,
    })
  }
  Object.defineProperty(e, 'currentTarget', {
    configurable: true,
    get() {
      // eslint-disable-next-line no-restricted-globals
      return node || document
    },
  })
  while (node !== null) {
    const handlers = getMetadata(node)[MetadataKind.event][e.type]
    if (handlers) {
      for (const handler of handlers) {
        if (handler.delegate && !node.disabled) {
          handler(e)
          if (e.cancelBubble) return
        }
      }
    }
    node =
      node.host && node.host !== node && node.host instanceof Node
        ? node.host
        : node.parentNode
  }
}

e.composedPath 是一个方法,它以数组形式返回事件的路径(EventTarget)。

developer.mozilla.org/en-US/docs/...

js 复制代码
// 示例

e.composedPath(); // [button, div, body, html, document]

首先,使用一个名为 getMetadata 的函数,它从 node 中检索元数据,并从那里的事件信息中获取处理程序。\

ts 复制代码
    const handlers = getMetadata(node)[MetadataKind.event][e.type]

然后,它执行所有这些处理程序。

ts 复制代码
    if (handlers) {
      for (const handler of handlers) {
        if (handler.delegate && !node.disabled) {
          handler(e)

之后,它通过遍历主机和父级来传播这个流程。

让我们也在这个流程中阅读 delegate

delegate

ts 复制代码
export function delegate(
  el: HTMLElement,
  event: string,
  handlerGetter: () => undefined | ((...args: any[]) => any),
  options: ModifierOptions = {},
): void {
  const handler: DelegatedHandler = eventHandler(handlerGetter, options)
  handler.delegate = true
  recordEventMetadata(el, event, handler)
}

delegate 创建一个处理程序,并在元数据中注册它,设置委托标志。

recordEventMetadata 在另一个文件 packages/runtime-vapor/src/componentMetadata.ts 中实现。

ts 复制代码
export function recordPropMetadata(el: Node, key: string, value: any): any {
  const metadata = getMetadata(el)[MetadataKind.prop]
  const prev = metadata[key]
  metadata[key] = value
  return prev
}

从这里可以看出,元数据直接注册到元素的一个名为 $$metadata 的属性中。它具有以下类型。

ts 复制代码
export enum MetadataKind {
  prop,
  event,
}

export type ComponentMetadata = [
  props: Data,
  events: Record<string, DelegatedHandler[]>,
]

它似乎有事件处理程序和 Props。

换句话说,它不是直接在这里注册事件处理程序,而只是持有处理程序,实际上,当由 delegateEventsdocument 中注册的处理程序被调用时,它参考这个元数据来执行处理程序。

on

现在,当 IR 中的 delegate 标志未设置时,调用 on

这非常简单,因为它使用 queuePostFlushCb 调用 addEventListener

ts 复制代码
export function on(
  el: Element,
  event: string,
  handlerGetter: () => undefined | ((...args: any[]) => any),
  options: AddEventListenerOptions &
    ModifierOptions & { effect?: boolean } = {},
): void {
  const handler: DelegatedHandler = eventHandler(handlerGetter, options)
  let cleanupEvent: (() => void) | undefined
  queuePostFlushCb(() => {
    cleanupEvent = addEventListener(el, event, handler, options)
  })

  if (options.effect) {
    onEffectCleanup(cleanup)
  } else if (getCurrentScope()) {
    onScopeDispose(cleanup)
  }

  function cleanup() {
    cleanupEvent && cleanupEvent()
  }
}
ts 复制代码
export function addEventListener(
  el: Element,
  event: string,
  handler: (...args: any) => any,
  options?: AddEventListenerOptions,
) {
  el.addEventListener(event, handler, options)
  return (): void => el.removeEventListener(event, handler, options)
}

清理处理也已实现。

delegate vs on

现在,我们理解了每个实现的差异,但为什么它们被不同地使用?

从 Solid 的文档中可以看出,在 delegate 的情况下,它似乎节省了资源。

更具体地解释一下,"将事件附加到元素"的行为会产生成本(时间和内存)。

虽然 on 单独地将事件附加到每个元素,但 delegate 只将事件附加到文档,当事件发生时,确定事件源自哪个元素,并在相应的元素上触发事件。

这似乎有助于性能。(我自己没有进行基准测试,所以我不知道它在 Vapor 中的效果如何。如果你知道,请告诉我。)

v-bind 指令

现在,让我们继续阅读和进步。

考虑以下组件。

vue 复制代码
<script setup>
import { ref } from "vue";
const dynamicData = ref("a");
</script>

<template>
  <div :data-dynamic="dynamicData">Hello, World!</div>
</template>

编译结果和概述

编译结果如下。(我们已经习惯了这个,所以解释变得有点粗略(笑))。

js 复制代码
const _sfc_main = {
  vapor: true,
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();

    const dynamicData = ref("a");

    const __returned__ = { dynamicData, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import {
  renderEffect as _renderEffect,
  setDynamicProp as _setDynamicProp,
  template as _template,
} from "vue/vapor";

const t0 = _template("<div>Hello, World!</div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _renderEffect(() => _setDynamicProp(n0, "data-dynamic", _ctx.dynamicData));
  return n0;
}

特别是,

js 复制代码
function _sfc_render(_ctx) {
  const n0 = t0();
  _renderEffect(() => _setDynamicProp(n0, "data-dynamic", _ctx.dynamicData));
  return n0;
}

_setDynamicProp 部分。

正如预期的那样,您现在可以预测实现方法。

总之,这是一个效果,将 _ctx.dynamicData 设置为 n0data-dynamic 属性,这是立即可以理解的。

阅读编译器

熟悉的路线:transformElement -> buildProps -> transformProps -> directiveTransform -> transformVBind

packages/compiler-vapor/src/transforms/vBind.ts

...是这样吗?

实际上,这只处理简写并真正转换 v-bind,而不注册效果等。

事实上,关于这一点,它直接在 transformElementbuildProps 中实现。

实现在这里附近。

ts 复制代码
            context.registerEffect(
              [prop.exp],

              {
                type: IRNodeTypes.SET_DYNAMIC_EVENTS,
                element: context.reference(),
                event: prop.exp,
              },
            )

再往上一点,还有当 v-bind 没有 arg(例如,v-bind="obj")时的处理。

ts 复制代码
    if (prop.type === NodeTypes.DIRECTIVE && !prop.arg) {
      if (prop.name === 'bind') {
        // v-bind="obj"
        if (prop.exp) {
          dynamicExpr.push(prop.exp)
          pushMergeArg()
          dynamicArgs.push({
            kind: IRDynamicPropsKind.EXPRESSION,
            value: prop.exp,
          })
        } else {

无论如何,既然我们能够看到 SET_DYNAMIC_EVENTS 注册的地方,那就没问题了。

让我们也按原样阅读代码生成。

ts 复制代码
export function genOperation(
  oper: OperationNode,
  context: CodegenContext,
): CodeFragment[] {
ts 复制代码
    case IRNodeTypes.SET_DYNAMIC_PROPS:
      return genDynamicProps(oper, context)
ts 复制代码
export function genDynamicProps(
  oper: SetDynamicPropsIRNode,
  context: CodegenContext,
): CodeFragment[] {
  const { vaporHelper } = context
  return [
    NEWLINE,
    ...genCall(
      vaporHelper('setDynamicProps'),
      `n${oper.element}`,
      ...oper.props.map(
        props =>
          Array.isArray(props)
            ? genLiteralObjectProps(props, context) // static and dynamic arg props
            : props.kind === IRDynamicPropsKind.ATTRIBUTE
              ? genLiteralObjectProps([props], context) // dynamic arg props
              : genExpression(props.value, context), // v-bind=""
      ),
    ),
  ]
}

应该没有特别困难的部分。

阅读运行时

这里也几乎没有什么可读的。

key"class""style" 时,它只做一点格式化。

v-model 指令

考虑以下组件。

vue 复制代码
<script setup>
import { ref } from "vue";
const text = ref("");
</script>

<template>
  <input v-model="text" />
</template>

编译结果和概述

编译结果如下。

js 复制代码
const _sfc_main = {
  vapor: true,
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();

    const text = ref("");

    const __returned__ = { text, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import {
  vModelText as _vModelText,
  withDirectives as _withDirectives,
  delegate as _delegate,
  template as _template,
} from "vue/vapor";

const t0 = _template("<input>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _withDirectives(n0, [[_vModelText, () => _ctx.text]]);
  _delegate(n0, "update:modelValue", () => ($event) => (_ctx.text = $event));
  return n0;
}

最突出的是

js 复制代码
_withDirectives(n0, [[_vModelText, () => _ctx.text]]);
_delegate(n0, "update:modelValue", () => ($event) => (_ctx.text = $event));

由于 delegate 再次出现,它在某种程度上表明这是用于注册事件处理程序的,但出现了像 withDirectives_vModelText 这样的神秘元素。

虽然稍后会阅读详细信息,但让我们先阅读编译器。

阅读编译器

遵循路径:transformElement -> buildProps -> transformProps -> directiveTransform -> transformVModel

packages/compiler-vapor/src/transforms/vModel.ts 首先,从 context 中提取 bindingMetadata

ts 复制代码
  const bindingType = context.options.bindingMetadata[rawExp]

这是由 compiler-sfc 收集的,包含有关 SFC 中定义的变量的元数据,例如是否在 setup 中定义了 let 变量,或者它是否是 prop、data 等。

具体来说,它枚举如下。

ts 复制代码
export enum BindingTypes {
  /**
   * returned from data()
   */
  DATA = 'data',
  /**
   * declared as a prop
   */
  PROPS = 'props',
  /**
   * a local alias of a `<script setup>` destructured prop.
   * the original is stored in __propsAliases of the bindingMetadata object.
   */
  PROPS_ALIASED = 'props-aliased',
  /**
   * a let binding (may or may not be a ref)
   */
  SETUP_LET = 'setup-let',
  /**
   * a const binding that can never be a ref.
   * these bindings don't need `unref()` calls when processed in inlined
   * template expressions.
   */
  SETUP_CONST = 'setup-const',
  /**
   * a const binding that does not need `unref()`, but may be mutated.
   */
  SETUP_REACTIVE_CONST = 'setup-reactive-const',
  /**
   * a const binding that may be a ref.
   */
  SETUP_MAYBE_REF = 'setup-maybe-ref',
  /**
   * bindings that are guaranteed to be refs
   */
  SETUP_REF = 'setup-ref',
  /**
   * declared by other options, e.g. computed, inject
   */
  OPTIONS = 'options',
  /**
   * a literal constant, e.g. 'foo', 1, true
   */
  LITERAL_CONST = 'literal-const',
}

如何收集它将在其他地方追踪。

如果 expbindingType 是 props,则抛出错误。非常周到。

ts 复制代码
  if (
    bindingType === BindingTypes.PROPS ||
    bindingType === BindingTypes.PROPS_ALIASED
  ) {
    context.options.onError(
      createCompilerError(ErrorCodes.X_V_MODEL_ON_PROPS, exp.loc),
    )
    return
  }

然后,以下分支是主要话题。

ts 复制代码
  if (

首先,如果标签是 inputtextareaselect 之一。

在这种情况下,它是 input,所以它匹配这里。

ts 复制代码
  if (
    tag === 'input' ||
    tag === 'textarea' ||
    tag === 'select' ||

对于 input,它读取 type 属性并确定 runtimeDirective

ts 复制代码
    if (tag === 'input' || isCustomElement) {
      const type = findProp(node, 'type')
      if (type) {
        if (type.type === NodeTypes.DIRECTIVE) {
          // :type="foo"
          runtimeDirective = 'vModelDynamic'
        } else if (type.value) {
          switch (type.value.content) {
            case 'radio':
              runtimeDirective = 'vModelRadio'
              break
            case 'checkbox':
              runtimeDirective = 'vModelCheckbox'
              break
            case 'file':
              runtimeDirective = undefined
              context.options.onError(
                createDOMCompilerError(
                  DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
                  dir.loc,
                ),
              )
              break
            default:
              // text type
              __DEV__ && checkDuplicatedValue()
              break
          }
        }

之前输出的 vModelText 似乎是这个变量的初始值。

ts 复制代码
  let runtimeDirective: VaporHelper | undefined = 'vModelText'

此时,它注册了一个名为 SET_MODEL_VALUE 的操作,并且

ts 复制代码
  context.registerOperation({
    type: IRNodeTypes.SET_MODEL_VALUE,
    element: context.reference(),
    key: arg || createSimpleExpression('modelValue', true),
    value: exp,
    isComponent,
  })

使用之前计算的 runtimeDirective 注册 withDirectives

ts 复制代码
    context.registerOperation({
      type: IRNodeTypes.WITH_DIRECTIVE,
      element: context.reference(),
      dir,
      name: runtimeDirective,
      builtin: true,
    })

它出奇地简单。

至于代码生成,一旦到达这一点,就很轻松了。

这是常规流程。不需要特别解释。

ts 复制代码
export function genOperation(
  oper: OperationNode,
  context: CodegenContext,
): CodeFragment[] {
ts 复制代码
    case IRNodeTypes.SET_MODEL_VALUE:
      return genSetModelValue(oper, context)
ts 复制代码
export function genSetModelValue(
  oper: SetModelValueIRNode,
  context: CodegenContext,
): CodeFragment[] {
  const { vaporHelper } = context
  const name = oper.key.isStatic
    ? [JSON.stringify(`update:${camelize(oper.key.content)}`)]
    : ['`update:${', ...genExpression(oper.key, context), '}`']

  const handler = genModelHandler(oper.value, context)

  return [
    NEWLINE,
    ...genCall(vaporHelper('delegate'), `n${oper.element}`, name, handler),
  ]
}

export function genModelHandler(
  value: SimpleExpressionNode,
  context: CodegenContext,
): CodeFragment[] {
  const {
    options: { isTS },
  } = context

  return [
    `() => ${isTS ? `($event: any)` : `$event`} => (`,
    ...genExpression(value, context, '$event'),
    ')',
  ]
}

withDirectives 部分有一个稍微不同的代码生成流程。

它遵循 genBlockContent -> genChildren -> genDirectivesForElement -> genWithDirective

ts 复制代码
export function genBlockContent(
  block: BlockIRNode,
  context: CodegenContext,
  root?: boolean,
  customReturns?: (returns: CodeFragment[]) => CodeFragment[],
): CodeFragment[] {

ts 复制代码
  for (const child of dynamic.children) {
    push(...genChildren(child, context, child.id!))
  }

ts 复制代码
export function genChildren(
  dynamic: IRDynamicInfo,
  context: CodegenContext,
  from: number,
  paths: number[] = [],
): CodeFragment[] {

ts 复制代码
    push(...genDirectivesForElement(id, context))

ts 复制代码
    push(...genDirectivesForElement(id, context))

ts 复制代码
export function genDirectivesForElement(
  id: number,
  context: CodegenContext,
): CodeFragment[] {
  const dirs = filterDirectives(id, context.block.operation)
  return dirs.length ? genWithDirective(dirs, context) : []
}

ts 复制代码
export function genWithDirective(
  opers: WithDirectiveIRNode[],
  context: CodegenContext,
): CodeFragment[] {

是路径。

阅读运行时

虽然 _delegate(n0, "update:modelValue", () => ($event) => (_ctx.text = $event)); 部分没问题,但问题在于 withDirectives_vModelText

withDirectives

让我们阅读 withDirectives。 实现在 packages/runtime-vapor/src/directives.ts 中。

ts 复制代码
export function withDirectives<T extends ComponentInternalInstance | Node>(
  nodeOrComponent: T,
  directives: DirectiveArguments,
): T {

withDirectives 接收 nodecomponent,以及 directives

DirectiveArguments

DirectiveArguments 的定义如下。

ts 复制代码
export type DirectiveArguments = Array<
  | [Directive | undefined]
  | [Directive | undefined, () => any]
  | [Directive | undefined, () => any, argument: string]
  | [
      Directive | undefined,
      value: () => any,
      argument: string,
      modifiers: DirectiveModifiers,
    ]
>
ts 复制代码
export type Directive<T = any, V = any, M extends string = string> =
  | ObjectDirective<T, V, M>
  | FunctionDirective<T, V, M>
ts 复制代码
export type FunctionDirective<
  T = any,
  V = any,
  M extends string = string,
> = DirectiveHook<T, V, M>
ts 复制代码
export type ObjectDirective<T = any, V = any, M extends string = string> = {
  [K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
} & {
  /** Watch value deeply */
  deep?: boolean | number
}
ts 复制代码
export type DirectiveHook<
  T = any | null,
  V = any,
  M extends string = string,
> = (node: T, binding: DirectiveBinding<T, V, M>) => void

// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
// effect update -> `beforeUpdate` -> node updated -> `updated`
// `beforeUnmount`-> node unmount -> `unmounted`
export type DirectiveHookName =
  | 'created'
  | 'beforeMount'
  | 'mounted'
  | 'beforeUpdate'
  | 'updated'
  | 'beforeUnmount'
  | 'unmounted'

它有点复杂,但简单来说,它为每个生命周期定义了行为。

(这似乎是指令的实际行为。)

withDirectives 内部

首先,有一个称为 DirectiveBinding 的概念。

这是一个捆绑必要信息的对象,例如旧值和新值、修饰符、指令本身(ObjectDirective),以及在组件的情况下,实例。

ts 复制代码
export interface DirectiveBinding<T = any, V = any, M extends string = string> {
  instance: ComponentInternalInstance
  source?: () => V
  value: V
  oldValue: V | null
  arg?: string
  modifiers?: DirectiveModifiers<M>
  dir: ObjectDirective<T, V, M>
}

然后,这个函数 withDirectives,顾名思义,可以应用多个指令。

它处理作为参数接收的指令数组中的每个指令。

ts 复制代码
  for (const directive of directives) {

让我们看看在这个 for 循环中做了什么。

首先,从定义中提取各种信息。

ts 复制代码
    let [dir, source, arg, modifiers] = directive

同时,进行规范化。

ts 复制代码
    if (isFunction(dir)) {
      dir = {
        mounted: dir,
        updated: dir,
      } satisfies ObjectDirective
    }

定义基本绑定。

ts 复制代码
    const binding: DirectiveBinding = {
      dir,
      instance,
      value: null, // set later
      oldValue: undefined,
      arg,
      modifiers,
    }

ReactiveEffect 包装 source,并设置该 effectscheduler 以包含更新触发器。

ts 复制代码
    if (source) {
      if (dir.deep) {
        const deep = dir.deep === true ? undefined : dir.deep
        const baseSource = source
        source = () => traverse(baseSource(), deep)
      }

      const effect = new ReactiveEffect(() =>
        callWithErrorHandling(
          source!,
          instance,
          VaporErrorCodes.RENDER_FUNCTION,
        ),
      )
      const triggerRenderingUpdate = createRenderingUpdateTrigger(
        instance,
        effect,
      )
      effect.scheduler = () => queueJob(triggerRenderingUpdate)

      binding.source = effect.run.bind(effect)
    }

更新触发器只是一个执行生命周期的 beforeUpdateupdated 的触发器。

ts 复制代码
export function createRenderingUpdateTrigger(
  instance: ComponentInternalInstance,
  effect: ReactiveEffect,
): SchedulerJob {
  job.id = instance.uid
  return job
  function job() {
    if (!(effect.flags & EffectFlags.ACTIVE) || !effect.dirty) {
      return
    }

    if (instance.isMounted && !instance.isUpdating) {
      instance.isUpdating = true
      const reset = setCurrentInstance(instance)

      const { bu, u, scope } = instance
      const { dirs } = scope
      // beforeUpdate hook
      if (bu) {
        invokeArrayFns(bu)
      }
      invokeDirectiveHook(instance, 'beforeUpdate', scope)

      queuePostFlushCb(() => {
        instance.isUpdating = false
        const reset = setCurrentInstance(instance)
        if (dirs) {
          invokeDirectiveHook(instance, 'updated', scope)
        }
        // updated hook
        if (u) {
          queuePostFlushCb(u)
        }
        reset()
      })
      reset()
    }
  }
}

最后,执行创建钩子。

ts 复制代码
    callDirectiveHook(node, binding, instance, 'created')

vModelText

现在我们已经读到这里,接下来让我们阅读特定指令的实现。

与 v-model 相关的 runtimeDirectivepackages/runtime-vapor/src/directives/vModel.ts 中实现。

这次,vModelText 如下。

ts 复制代码
export const vModelText: ObjectDirective<
  HTMLInputElement | HTMLTextAreaElement,
  any,
  'lazy' | 'trim' | 'number'
> = {

在这里,定义了与此指令相关的每个生命周期的行为,例如 beforeMountmountedbeforeUpdate 等。 让我们逐一查看。

beforeMount
ts 复制代码
  beforeMount(el, { modifiers: { lazy, trim, number } = {} }) {

注册事件处理程序。

在更新值时执行值的修剪和转换为数字。

ts 复制代码
    addEventListener(el, lazy ? 'change' : 'input', e => {
      if ((e.target as any).composing) return
      let domValue: string | number = el.value
      if (trim) {
        domValue = domValue.trim()
      }
      if (castToNumber) {
        domValue = looseToNumber(domValue)
      }
      assigner(domValue)
    })

值更新使用从委托事件处理程序获得的赋值器。

ts 复制代码
    addEventListener(el, lazy ? 'change' : 'input', e => {
      if ((e.target as any).composing) return
      let domValue: string | number = el.value
      if (trim) {
        domValue = domValue.trim()
      }
      if (castToNumber) {
        domValue = looseToNumber(domValue)
      }
      assigner(domValue)
    })
ts 复制代码
function getModelAssigner(el: Element): AssignerFn {
  const metadata = getMetadata(el)
  const fn = metadata[MetadataKind.event]['update:modelValue'] || []
  return value => invokeArrayFns(fn, value)
}

虽然官方文档提到 v-model 处理诸如 IME 之类的组合,但这正是这个过程。

mounted

在挂载时,它只是设置初始值。

ts 复制代码
  mounted(el, { value }) {
    el.value = value == null ? '' : value
  },
beforeUpdate

处理诸如 IME 之类的组合,并跳过不必要的更新直到更新。

ts 复制代码
  beforeUpdate(el, { value, modifiers: { lazy, trim, number } = {} }) {
    assignFnMap.set(el, getModelAssigner(el))

    // avoid clearing unresolved text. #2302
    if ((el as any).composing) return

    const elValue =
      number || el.type === 'number' ? looseToNumber(el.value) : el.value
    const newValue = value == null ? '' : value

    if (elValue === newValue) {
      return
    }

    // eslint-disable-next-line no-restricted-globals
    if (document.activeElement === el && el.type !== 'range') {
      if (lazy) {
        return
      }
      if (trim && el.value.trim() === newValue) {
        return
      }
    }

    el.value = newValue
  },

有了这个,v-model 的操作应该被理解了。

除了 vModelText 之外,还有各种其他指令定义,但您应该能够基于此以相同的方式进行。

而且,由于这次出现了诸如 runtimeDirectivewithDirectives 之类的新概念,所以变得有点冗长,但您应该能够基于此继续阅读其他指令。(您的速度也应该会提高。)

让我们以这种方式继续阅读。

v-show 指令

考虑以下组件。

vue 复制代码
<script setup>
import { ref } from "vue";
const flag = ref("");
</script>

<template>
  <p v-show="flag">Hello, v-show!</p>
</template>

编译结果和概述

编译结果如下。

js 复制代码
const _sfc_main = {
  vapor: true,
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();

    const flag = ref("");

    const __returned__ = { flag, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import {
  vShow as _vShow,
  withDirectives as _withDirectives,
  template as _template,
} from "vue/vapor";

const t0 = _template("<p>Hello, v-show!</p>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _withDirectives(n0, [[_vShow, () => _ctx.flag]]);
  return n0;
}

突出的是 _withDirectives(n0, [[_vShow, () => _ctx.flag]]); 部分。

继续上次的内容,它再次使用 withDirectives 函数,但这次它使用 vShow 函数作为运行时指令。

阅读编译器

我们将遵循 transformElement -> buildProps -> transformProps -> directiveTransform -> transformVShow

它非常简单,所以我将包含整个文本。

ts 复制代码
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import type { DirectiveTransform } from '../transform'
import { IRNodeTypes } from '../ir'

export const transformVShow: DirectiveTransform = (dir, node, context) => {
  const { exp, loc } = dir
  if (!exp) {
    context.options.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_SHOW_NO_EXPRESSION, loc),
    )
  }

  context.registerOperation({
    type: IRNodeTypes.WITH_DIRECTIVE,
    element: context.reference(),
    dir,
    name: 'vShow',
    builtin: true,
  })
}

它只是注册带有 name: 'vShow'WITH_DIRECTIVE

阅读运行时

这也很简单,所以我将包含整个文本。

它只是在 beforeMountupdatedbeforeUnmount 中将 el.style.display 设置为 none""

ts 复制代码
import type { ObjectDirective } from '../directives'

const vShowMap = new WeakMap<HTMLElement, string>()

export const vShow: ObjectDirective<HTMLElement> = {
  beforeMount(node, { value }) {
    vShowMap.set(node, node.style.display === 'none' ? '' : node.style.display)
    setDisplay(node, value)
  },

  updated(node, { value, oldValue }) {
    if (!value === !oldValue) return
    setDisplay(node, value)
  },

  beforeUnmount(node, { value }) {
    setDisplay(node, value)
  },
}

function setDisplay(el: HTMLElement, value: unknown): void {
  el.style.display = value ? vShowMap.get(el)! : 'none'
}

这就是全部内容!

v-once 指令

考虑以下组件。

vue 复制代码
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<template>
  <p v-once>{{ count }}</p>
</template>

编译结果和概述

编译结果如下。

js 复制代码
const _sfc_main = {
  vapor: true,
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();

    const count = ref(0);

    const __returned__ = { count, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import { setText as _setText, template as _template } from "vue/vapor";

const t0 = _template("<p></p>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _setText(n0, _ctx.count);
  return n0;
}

突出的是 setText 部分没有被包装在 renderEffect 中。

由于 v-once 只渲染一次,所以不需要将其包装在 renderEffect 中。

阅读编译器

我们将遵循 transformElement -> buildProps -> transformProps -> directiveTransform -> transformVOnce

它非常简单,所以我将包含整个文本。

ts 复制代码
import { NodeTypes, findDir } from '@vue/compiler-dom'
import type { NodeTransform } from '../transform'

export const transformVOnce: NodeTransform = (node, context) => {
  if (
    // !context.inSSR &&
    node.type === NodeTypes.ELEMENT &&
    findDir(node, 'once', true)
  ) {
    context.inVOnce = true
  }
}

它只是启用 context 持有的 inVOnce 标志。

inVOnce 为 true 时,它调用带有 registerEffectregisterOperation 并完成,这意味着不生成效果。

ts 复制代码
  registerEffect(
    expressions: SimpleExpressionNode[],
    ...operations: OperationNode[]
  ): void {
    expressions = expressions.filter(exp => !isConstantExpression(exp))
    if (this.inVOnce || expressions.length === 0) {
      return this.registerOperation(...operations)
    }

由于运行时中没有特别需要阅读的内容,所以暂时就到这里。

v-text 指令

考虑以下组件。

vue 复制代码
<script setup>
import { ref } from "vue";
const message = ref("Hello, v-text!");
</script>

<template>
  <p v-text="message" />
</template>

编译结果和概述

编译结果如下。

js 复制代码
const _sfc_main = {
  vapor: true,
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();

    const message = ref("Hello, v-text!");

    const __returned__ = { message, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import {
  renderEffect as _renderEffect,
  setText as _setText,
  template as _template,
} from "vue/vapor";

const t0 = _template("<p></p>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _renderEffect(() => _setText(n0, _ctx.message));
  return n0;
}

使用 v-text 指定的元素是使用 renderEffectsetText 构建的。

阅读编译器

我们将遵循路径 transformElement -> buildProps -> transformProps -> directiveTransform -> transformVText

它非常简单,所以我将包含整个文本。

ts 复制代码
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import { IRNodeTypes } from '../ir'
import { EMPTY_EXPRESSION } from './utils'
import type { DirectiveTransform } from '../transform'
import { getLiteralExpressionValue } from '../utils'

export const transformVText: DirectiveTransform = (dir, node, context) => {
  let { exp, loc } = dir
  if (!exp) {
    context.options.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc),
    )
    exp = EMPTY_EXPRESSION
  }
  if (node.children.length) {
    context.options.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_TEXT_WITH_CHILDREN, loc),
    )
    context.childrenTemplate.length = 0
  }

  const literal = getLiteralExpressionValue(exp)
  if (literal != null) {
    context.childrenTemplate = [String(literal)]
  } else {
    context.registerEffect([exp], {
      type: IRNodeTypes.SET_TEXT,
      element: context.reference(),
      values: [exp],
    })
  }
}

它只是注册 registerEffect

它执行类似于调用 transformText 时的处理。

在运行时中没有特别需要阅读的内容,所以暂时就到这里。

v-html 指令

考虑以下组件。

vue 复制代码
<script setup>
import { ref } from "vue";
const inner = ref("<p>Hello, v-html</p>");
</script>

<template>
  <div v-html="inner" />
</template>

编译结果和概述

编译结果如下。

js 复制代码
const _sfc_main = {
  vapor: true,
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();

    const inner = ref("<p>Hello, v-html</p>");

    const __returned__ = { inner, ref };
    Object.defineProperty(__returned__, "__isScriptSetup", {
      enumerable: false,
      value: true,
    });
    return __returned__;
  },
};

import {
  renderEffect as _renderEffect,
  setHtml as _setHtml,
  template as _template,
} from "vue/vapor";

const t0 = _template("<div></div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _renderEffect(() => _setHtml(n0, _ctx.inner));
  return n0;
}

出现了一个名为 setHtml 的新辅助函数。

它的实现并没有特别改变。

阅读编译器

我们将遵循路径 transformElement -> buildProps -> transformProps -> directiveTransform -> transformVHtml

它非常简单,所以我将包含整个文本。

ts 复制代码
import { IRNodeTypes } from '../ir'
import type { DirectiveTransform } from '../transform'
import { DOMErrorCodes, createDOMCompilerError } from '@vue/compiler-dom'
import { EMPTY_EXPRESSION } from './utils'

export const transformVHtml: DirectiveTransform = (dir, node, context) => {
  let { exp, loc } = dir
  if (!exp) {
    context.options.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
    )
    exp = EMPTY_EXPRESSION
  }
  if (node.children.length) {
    context.options.onError(
      createDOMCompilerError(DOMErrorCodes.X_V_HTML_WITH_CHILDREN, loc),
    )
    context.childrenTemplate.length = 0
  }

  context.registerEffect([exp], {
    type: IRNodeTypes.SET_HTML,
    element: context.reference(),
    value: exp,
  })
}

它只是用 SET_HTML 注册一个效果。

它执行类似于调用 transformText 时的处理。

让我们看看代码生成。

ts 复制代码
export function genOperation(
  oper: OperationNode,
  context: CodegenContext,
): CodeFragment[] {

ts 复制代码
    case IRNodeTypes.SET_HTML:
      return genSetHtml(oper, context)

ts 复制代码
export function genSetHtml(
  oper: SetHtmlIRNode,
  context: CodegenContext,
): CodeFragment[] {
  const { vaporHelper } = context
  return [
    NEWLINE,
    ...genCall(
      vaporHelper('setHtml'),
      `n${oper.element}`,
      genExpression(oper.value, context),
    ),
  ]
}

这很熟悉。

阅读运行时

让我们只读 setHtml

ts 复制代码
export function setHtml(el: Element, value: any): void {
  const oldVal = recordPropMetadata(el, 'innerHTML', value)
  if (value !== oldVal) {
    el.innerHTML = value
  }
}

它非常简单,只是设置 innerHTML

目前,它似乎从元信息中提取旧值以防止不必要的设置。

最后

这本书还有部分内容没有完成,后续若作者将内容完善后,我也会把本文译文给补全,翻译不易,喜欢还请给个赞支持下

相关推荐
Apifox3 小时前
理解和掌握 Apifox 中的变量(临时、环境、模块、全局变量等)
前端·后端·测试
小白_ysf4 小时前
阿里云日志服务之WebTracking 小程序端 JavaScript SDK (阿里SDK埋点和原生uni.request请求冲突问题)
前端·微信小程序·uni-app·埋点·阿里云日志服务
你的电影很有趣4 小时前
lesson52:CSS进阶指南:雪碧图与边框技术的创新应用
前端·css
Jerry4 小时前
Compose 延迟布局
前端
前端fighter4 小时前
Vue 3 路由切换:页面未刷新问题
前端·vue.js·面试
lskblog4 小时前
使用 PHP Imagick 扩展实现高质量 PDF 转图片功能
android·开发语言·前端·pdf·word·php·laravel
whysqwhw4 小时前
Node-API 学习二
前端
whysqwhw4 小时前
Node-API 学习一
前端
Jenna的海糖4 小时前
Vue 中 v-model 的 “双向绑定”:从原理到自定义组件适配
前端·javascript·vue.js