GVKun编程网logo

【Vue.js】900- Vue 3.0 进阶之 VNode 探秘(vue的vnode)

10

关于【Vue.js】900-Vue3.0进阶之VNode探秘和vue的vnode的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于【Vue.js】873-Vue3.0进阶之指令探秘、【Vue

关于【Vue.js】900- Vue 3.0 进阶之 VNode 探秘vue的vnode的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于【Vue.js】873- Vue 3.0 进阶之指令探秘、【Vue.js】875- Vue 3.0 进阶之自定义事件探秘、【Vue.js】880- Vue 3.0 进阶之双向绑定探秘、【Vue.js】890- Vue 3.0 进阶之 VNode 探秘等相关知识的信息别忘了在本站进行查找喔。

本文目录一览:

【Vue.js】900- Vue 3.0 进阶之 VNode 探秘(vue的vnode)

【Vue.js】900- Vue 3.0 进阶之 VNode 探秘(vue的vnode)

本文是 Vue 3.0 进阶系列 的第五篇文章,在这篇文章中,阿宝哥将介绍 Vue 3 中的核心对象 —— VNode,该对象用于描述节点的信息,它的全称是虚拟节点(virtual node)。与 “虚拟节点” 相关联的另一个概念是 “虚拟 DOM”,它是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。通常一个 Vue 应用会以一棵嵌套的组件树的形式来组织:

(图片来源:https://v3.cn.vuejs.org/)

所以 “虚拟 DOM” 对 Vue 应用来说,是至关重要的。而 “虚拟 DOM” 又是由 VNode 组成的,它是 Vue 底层的核心基石。接下来,阿宝哥将带大家一起来探索 Vue 3 中与 VNode 相关的一些知识。

一、VNode 长什么样?

// packages/runtime-core/src/vnode.ts
export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
 // 省略内部的属性
}

runtime-core/src/vnode.ts 文件中,我们找到了 VNode 的类型定义。通过 VNode 的类型定义可知,VNode 本质是一个对象,该对象中按照属性的作用,分为 5 大类。这里阿宝哥只详细介绍其中常见的两大类型属性 —— 内部属性DOM 属性

1.1 内部属性

__v_isVNode: true // 标识是否为VNode
[ReactiveFlags.SKIP]: true // 标识VNode不是observable
type: VNodeTypes // VNode 类型
props: (VNodeProps & ExtraProps) | null // 属性信息
key: string | number | null // 特殊 attribute 主要用在 Vue 的虚拟 DOM 算法
ref: VNodeNormalizedRef | null // 被用来给元素或子组件注册引用信息。
scopeId: string | null // SFC only
children: VNodeNormalizedChildren // 保存子节点
component: ComponentInternalInstance | null // 指向VNode对应的组件实例
dirs: DirectiveBinding[] | null // 保存应用在VNode的指令信息
transition: TransitionHooks<HostElement> | null // 存储过渡效果信息

1.2 DOM 属性

el: HostNode | null // element 
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
staticCount: number // number of elements contained in a static vnode

1.3 suspense 属性

suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null

1.4 optimization 属性

shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null

1.5 应用上下文属性

appContext: AppContext | null

二、如何创建 VNode?

要创建 VNode 对象的话,我们可以使用 Vue 提供的 h 函数。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h() 。该函数接受三个参数:

// packages/runtime-core/src/h.ts
export function h(typeany, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) { 
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { 
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(typenull, [propsOrChildren])
      }
      // 只包含属性不含有子元素
      return createVNode(type, propsOrChildren) // h(''div'', { id: ''foo'' })
    } else {
      // 忽略属性
      return createVNode(typenull, propsOrChildren) // h(''div'', [''foo''])
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

观察以上代码可知, h 函数内部的主要处理逻辑就是根据参数个数和参数类型,执行相应处理操作,但最终都是通过调用 createVNode 函数来创建 VNode 对象。在开始介绍 createVNode 函数前,阿宝哥先举一些实际开发中的示例:

const app = createApp({ // 示例一
  render() => h(''div''''我是阿宝哥'')
})

const Comp = () => h("p""我是阿宝哥"); // 示例二

app.component(''component-a'', { // 示例三
  template"<p>我是阿宝哥</p>"
})

示例一和示例二很明显都使用了 h 函数,而示例三并未看到 hcreateVNode 函数的身影。为了一探究竟,我们需要借助 Vue 3 Template Explorer 这个在线工具来编译一下 "<p>我是阿宝哥</p>" 模板,该模板编译后的结果如下(函数模式):

// https://vue-next-template-explorer.netlify.app/
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock,
      createBlock: _createBlock } = _Vue
    return (_openBlock(), _createBlock("p"null"我是阿宝哥"))
  }
}

由以上编译结果可知, "<p>我是阿宝哥</p>" 模板被编译生成了一个 render 函数,调用该函数后会返回 createBlock 函数的调用结果。其中 createBlock 函数的实现如下所示:

// packages/runtime-core/src/vnode.ts
export function createBlock(
  type: VNodeTypes | ClassComponent,
  props?: Record<stringany> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[]
): VNode 
{
  const vnode = createVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    true /* isBlock: prevent a block from tracking itself */
  )
  // 省略部分代码
  return vnode
}

createBlock 函数内部,我们终于看到了 createVNode 函数的身影。顾名思义,该函数的作用就是用于创建 VNode,接下来我们来分析一下它。

三、createVNode 函数内部做了啥?

下面我们将从参数说明和逻辑说明两方面来介绍 createVNode 函数:

3.1 参数说明

createVNode 被定义在 runtime-core/src/vnode.ts 文件中:

// packages/runtime-core/src/vnode.ts
export const createVNode = (__DEV__
  ? createVNodeWithArgsTransform
  : _createVNode) as typeof _createVNode

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode 
{
  // 
  return vnode
}

在分析该函数的具体代码前,我们先来看一下它的参数。该函数可以接收 6 个参数,这里阿宝哥用思维导图来重点介绍前面 2 个参数:

type 参数
// packages/runtime-core/src/vnode.ts
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  // 省略其他参数
): VNode 
{ ... }

由上图可知,type 参数支持很多类型,比如常用的 stringVNodeComponent 等。此外,也有一些陌生的面孔,比如 TextCommentStaticFragment 等类型,它们的定义如下:

// packages/runtime-core/src/vnode.ts
export const Text = Symbol(__DEV__ ? ''Text'' : undefined)
export const Comment = Symbol(__DEV__ ? ''Comment'' : undefined)
export const Static = Symbol(__DEV__ ? ''Static'' : undefined)

export const Fragment = (Symbol(__DEV__ ? ''Fragment'' : undefinedas anyas {
  __isFragment: true
  new (): {
    $props: VNodeProps
  }
}

那么定义那么多的类型有什么意义呢?这是因为在 patch 阶段,会根据不同的 VNode 类型来执行不同的操作:

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any 
{
  const patch: PatchFn = (
    n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null,
    isSVG = false, optimized = false
  ) => {
    // 省略部分代码
    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text: // 处理文本节点
        processText(n1, n2, container, anchor)
        break
      case Comment: // 处理注释节点
        processCommentNode(n1, n2, container, anchor)
        break
      case Static: // 处理静态节点
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment: // 处理Fragment节点
        processFragment(...)
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) { // 元素类型
          processElement(...)
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件类型
          processComponent(...)
        } else if (shapeFlag & ShapeFlags.TELEPORT) { // teleport内置组件
          ;(type as typeof TeleportImpl).process(...)
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(...)
        }
    }
  }
}

介绍完 type 参数后,接下来我们来看 props 参数,具体如下图所示:

props 参数
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
): VNode 
{ ... }

props 参数的类型是联合类型,这里我们来分析 Data & VNodeProps 交叉类型:

其中 Data 类型是通过 TypeScript 内置的工具类型 Record 来定义的:

export type Data = Record<string, unknown>
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

VNodeProps 类型是通过类型别名来定义的,除了含有 keyref 属性之外,其他的属性主要是定义了与生命周期有关的钩子:

// packages/runtime-core/src/vnode.ts
export type VNodeProps = {
  key?: string | number
  ref?: VNodeRef

  // vnode hooks
  onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
  onVnodeMounted?: VNodeMountHook | VNodeMountHook[]
  onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]
  onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]
  onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]
  onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]
}

3.2 逻辑说明

createVNode 函数内部涉及较多的处理逻辑,这里我们只分析主要的逻辑:

// packages/runtime-core/src/vnode.ts
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode 
{
  // 处理VNode类型,比如处理动态组件的场景:<component :is="vnode"/>
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // 类组件规范化处理
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 类和样式规范化处理
  if (props) {
    // 省略相关代码
  }

  // 把vnode的类型信息转换为位图
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT // ELEMENT = 1
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE // SUSPENSE = 1 << 7,
      : isTeleport(type)
        ? ShapeFlags.TELEPORT // TELEPORT = 1 << 6,
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT // STATEFUL_COMPONENT = 1 << 2,
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT // FUNCTIONAL_COMPONENT = 1 << 1,
            : 0

  // 创建VNode对象
  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    // ...
  }

  // 子元素规范化处理
  normalizeChildren(vnode, children)
  return vnode
}

介绍完 createVNode 函数之后,阿宝哥再来介绍另一个比较重要的函数 —— normalizeVNode

四、如何创建规范的 VNode 对象?

normalizeVNode 函数的作用,用于将传入的 child 参数转换为规范的 VNode 对象。

// packages/runtime-core/src/vnode.ts
export function normalizeVNode(child: VNodeChild): VNode {
  if (child == null || typeof child === ''boolean'') { // null/undefined/boolean -> Comment
    return createVNode(Comment)
  } else if (isArray(child)) { // array -> Fragment
    return createVNode(Fragment, null, child)
  } else if (typeof child === ''object'') { // VNode -> VNode or mounted VNode -> cloned VNode
    return child.el === null ? child : cloneVNode(child)
  } else { // primitive types:''foo'' or 1
    return createVNode(Text, nullString(child))
  }
}

由以上代码可知,normalizeVNode 函数内部会根据 child 参数的类型进行不同的处理:

4.1 null / undefined -> Comment

expect(normalizeVNode(null)).toMatchObject({ type: Comment })
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })

4.2 boolean -> Comment

expect(normalizeVNode(true)).toMatchObject({ type: Comment })
expect(normalizeVNode(false)).toMatchObject({ type: Comment })

4.3 array -> Fragment

expect(normalizeVNode([''foo''])).toMatchObject({ type: Fragment })

4.4 VNode -> VNode

const vnode = createVNode(''div'')
expect(normalizeVNode(vnode)).toBe(vnode)

4.5 mounted VNode -> cloned VNode

const mounted = createVNode(''div'')
mounted.el = {}
const normalized = normalizeVNode(mounted)
expect(normalized).not.toBe(mounted)
expect(normalized).toEqual(mounted)

4.6 primitive types

expect(normalizeVNode(''foo'')).toMatchObject({ type: Text, children: `foo` })
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })

五、阿宝哥有话说

5.1 如何判断是否为 VNode 对象?

// packages/runtime-core/src/vnode.ts
export function isVNode(value: any): value is VNode {
  return value ? value.__v_isVNode === true : false
}

VNode 对象中含有一个 __v_isVNode 内部属性,利用该属性可以用来判断当前对象是否为 VNode 对象。

5.2 如何判断两个 VNode 对象的类型是否相同?

// packages/runtime-core/src/vnode.ts
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  // 省略__DEV__环境的处理逻辑
  return n1.type === n2.type && n1.key === n2.key
}

在 Vue 3 中,是通过比较 VNode 对象的 typekey 属性,来判断两个 VNode 对象的类型是否相同。

5.3 如何快速创建某些类型的 VNode 对象?

在 Vue 3 内部提供了 createTextVNodecreateCommentVNodecreateStaticVNode 函数来快速的创建文本节点、注释节点和静态节点:

createTextVNode
export function createTextVNode(text: string = '' '', flag: number = 0): VNode {
  return createVNode(Text, null, text, flag)
}
createCommentVNode
export function createCommentVNode(
  text: string = '''',
  asBlock: boolean = false
): VNode 
{
  return asBlock
    ? (openBlock(), createBlock(Comment, null, text))
    : createVNode(Comment, null, text)
}
createStaticVNode
export function createStaticVNode(
  content: string,
  numberOfNodes: number
): VNode 
{
  const vnode = createVNode(Static, null, content)
  vnode.staticCount = numberOfNodes
  return vnode
}

本文阿宝哥主要介绍了 VNode 对象是什么、如何创建 VNode 对象及如何创建规范的 VNode 对象。为了让大家能够更深入地理解 hcreateVNode 函数的相关知识,阿宝哥还从源码的角度分析了 createVNode 函数 。

在后续的文章中,阿宝哥将会介绍 VNode 在 Vue 3 内部是如何被使用的,感兴趣的小伙伴不要错过哟。

六、参考资源

  • Vue 3 官网 - 渲染函数
聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【Vue.js】873- Vue 3.0 进阶之指令探秘

【Vue.js】873- Vue 3.0 进阶之指令探秘

在 Vue 的项目中,我们经常会遇到 v-ifv-showv-forv-model 这些内置指令,它们为我们提供了不同的功能。除了使用这些内置指令之外,Vue 也允许注册自定义指令。

接下来,阿宝哥将使用 Vue 3 官方文档 自定义指令 章节中使用的示例,来一步步揭开自定义指令背后的秘密。

提示:在阅读本文前,建议您先阅读 Vue 3 官方文档 自定义指令 章节的内容。

一、自定义指令

1、注册全局自定义指令

const app = Vue.createApp({})

// 注册一个全局自定义指令 v-focus
app.directive(''focus'', {
  // 当被绑定的元素挂载到 DOM 中时被调用
  mounted(el) {
    // 聚焦元素
    el.focus()
  }
})

2、使用全局自定义指令

<div id="app">
   <input v-focus />
</div>

3、完整的使用示例

<div id="app">
   <input v-focus />
</div>
<script>
   const { createApp } = Vue
   
   const app = Vue.createApp({}) // ①
   app.directive(''focus'', { // ② 
     // 当被绑定的元素挂载到 DOM 中时被调用
     mounted(el) {
       el.focus() // 聚焦元素
     }
   })
   app.mount(''#app''// ③
</script>

当页面加载完成后,页面中的输入框元素将自动获得焦点。该示例的代码比较简单,主要包含 3 个步骤:创建 App 对象、注册全局自定义指令和应用挂载。其中创建 App 对象的细节,阿宝哥会在后续的文章中单独介绍,下面我们将重点分析其他 2 个步骤。首先我们先来分析注册全局自定义指令的过程。

二、注册全局自定义指令的过程

在以上示例中,我们使用 app 对象的 directive 方法来注册全局自定义指令:

app.directive(''focus'', {
  // 当被绑定的元素挂载到 DOM 中时被调用
  mounted(el) {
    el.focus() // 聚焦元素
  }
})

当然,除了注册全局自定义指令外,我们也可以注册局部指令,因为组件中也接受一个 directives 的选项:

directives: {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}

对于以上示例来说,我们使用的 app.directive 方法被定义在 runtime-core/src/apiCreateApp.ts 文件中:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement
{
  return function createApp(rootComponent, rootProps = null{
    const context = createAppContext()
    let isMounted = false

    const app: App = (context.app = {
      // 省略部分代码
      _context: context,
      
      // 用于注册或检索全局指令。
      directive(name: string, directive?: Directive) {
        if (__DEV__) {
          validateDirectiveName(name)
        }
        if (!directive) {
          return context.directives[name] as any
        }
        if (__DEV__ && context.directives[name]) {
          warn(`Directive "${name}" has already been registered in target app.`)
        }
        context.directives[name] = directive
        return app
      },

    return app
  }
}

通过观察以上代码,我们可以知道 directive 方法支持以下两个参数:

  • name:表示指令的名称;
  • directive(可选):表示指令的定义。

name 参数比较简单,所以我们重点分析 directive 参数,该参数的类型是 Directive 类型:

// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
  | ObjectDirective<T, V>
  | FunctionDirective<T, V>

由上可知 Directive 类型属于联合类型,所以我们需要继续分析 ObjectDirectiveFunctionDirective 类型。这里我们先来看一下 ObjectDirective 类型的定义:

// packages/runtime-core/src/directives.ts
export interface ObjectDirective<T = any, V = any> {
  created?: DirectiveHook<T, null, V>
  beforeMount?: DirectiveHook<T, null, V>
  mounted?: DirectiveHook<T, null, V>
  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
  updated?: DirectiveHook<T, VNode<any, T>, V>
  beforeUnmount?: DirectiveHook<T, null, V>
  unmounted?: DirectiveHook<T, null, V>
  getSSRProps?: SSRDirectiveHook
}

该类型定义了对象类型的指令,对象上的每个属性表示指令生命周期上的钩子。而 FunctionDirective 类型则表示函数类型的指令:

// packages/runtime-core/src/directives.ts
export type FunctionDirective<T = any, V = any> = DirectiveHook<T, any, V>
                              
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
  el: T,
  binding: DirectiveBinding<V>,
  vnode: VNode<any, T>,
  prevVNode: Prev
) => void                              

介绍完 Directive 类型,我们再回顾一下前面的示例,相信你就会清晰很多:

app.directive(''focus'', {
  // 当被绑定的元素挂载到 DOM 中时触发
  mounted(el) {
    el.focus() // 聚焦元素
  }
})

对于以上示例,当我们调用 app.directive 方法注册自定义 focus 指令时,就会执行以下逻辑:

directive(name: string, directive?: Directive) {
  if (__DEV__) { // 避免自定义指令名称,与已有的内置指令名称冲突
    validateDirectiveName(name)
  }
  if (!directive) { // 获取name对应的指令对象
    return context.directives[name] as any
  }
  if (__DEV__ && context.directives[name]) {
    warn(`Directive "${name}" has already been registered in target app.`)
  }
  context.directives[name] = directive // 注册全局指令
  return app
}

focus 指令注册成功之后,该指令会被保存在 context 对象的 directives 属性中,具体如下图所示:

顾名思义 context 是表示应用的上下文对象,那么该对象是如何创建的呢?其实,该对象是通过 createAppContext 函数来创建的:

const context = createAppContext()

createAppContext 函数被定义在 runtime-core/src/apiCreateApp.ts 文件中:

// packages/runtime-core/src/apiCreateApp.ts
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      isCustomElement: NO,
      errorHandler: undefined,
      warnHandler: undefined
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}

看到这里,是不是觉得注册全局自定义指令的内部处理逻辑其实挺简单的。那么对于已注册的 focus 指令,何时会被调用呢?要回答这个问题,我们就需要分析另一个步骤 —— 应用挂载

三、应用挂载的过程

为了更加直观地了解应用挂载的过程,阿宝哥利用 Chrome 开发者工具,记录了应用挂载的主要过程:

通过上图,我们就可以知道应用挂载期间所经历的主要过程。此外,从图中我们也发现了一个与指令相关的函数 resolveDirective。很明显,该函数用于解析指令,且该函数在 render 方法中会被调用。在源码中,我们找到了该函数的定义:

// packages/runtime-core/src/helpers/resolveAssets.ts
export function resolveDirective(name: string): Directive | undefined {
  return resolveAsset(DIRECTIVES, name)
}

resolveDirective 函数内部,会继续调用 resolveAsset 函数来执行具体的解析操作。在分析 resolveAsset 函数的具体实现之前,我们在 resolveDirective 函数内部加个断点,来一睹 render 方法的 “芳容”:

在上图中,我们看到了与 focus 指令相关的 _resolveDirective("focus") 函数调用。前面我们已经知道在 resolveDirective 函数内部会继续调用 resolveAsset 函数,该函数的具体实现如下:

// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
  typetypeof COMPONENTS | typeof DIRECTIVES,
  name: string,
  warnMissing = true
{
  const instance = currentRenderingInstance || currentInstance
  if (instance) {
    const Component = instance.type
    // 省略解析组件的处理逻辑
    const res =
      // 局部注册
      resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
      // 全局注册
      resolve(instance.appContext[type], name)
    return res
  } else if (__DEV__) {
    warn(
      `resolve${capitalize(type.slice(0-1))} ` +
        `can only be used in render() or setup().`
    )
  }
}

因为注册 focus 指令时,使用的是全局注册的方式,所以解析的过程会执行 resolve(instance.appContext[type], name) 该语句,其中 resolve 方法的定义如下:

function resolve(registry: Record<stringany> | undefined, name: string{
  return (
    registry &&
    (registry[name] ||
      registry[camelize(name)] ||
      registry[capitalize(camelize(name))])
  )
}

分析完以上的处理流程,我们可以知道在解析全局注册的指令时,会通过 resolve 函数从应用的上下文对象中获取已注册的指令对象。在获取到 _directive_focus 指令对象后,render 方法内部会继续调用 _withDirectives 函数,用于把指令添加到 VNode 对象上,该函数被定义在 runtime-core/src/directives.ts 文件中:

// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode>(
  vnode: T,
  directives: DirectiveArguments
): T 
{
  const internalInstance = currentRenderingInstance // 获取当前渲染的实例
  const instance = internalInstance.proxy
  const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    // 在 mounted 和 updated 时,触发相同行为,而不关系其他的钩子函数
    if (isFunction(dir)) { // 处理函数类型指令
      dir = {
        mounted: dir,
        updated: dir
      } as ObjectDirective
    }
    bindings.push({
      dir,
      instance,
      value,
      oldValue: void 0,
      arg,
      modifiers
    })
  }
  return vnode
}

因为一个节点上可能会应用多个指令,所以 withDirectives 函数在 VNode 对象上定义了一个 dirs 属性且该属性值为数组。对于前面的示例来说,在调用 withDirectives 函数之后,VNode 对象上就会新增一个 dirs 属性,具体如下图所示:

通过上面的分析,我们已经知道在组件的 render 方法中,我们会通过  withDirectives 函数把指令注册对应的 VNode 对象上。那么 focus 指令上定义的钩子什么时候会被调用呢?在继续分析之前,我们先来介绍一下指令对象所支持的钩子函数。

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • created:在绑定元素的属性或事件监听器被应用之前调用。

  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。

  • mounted:在绑定元素的父组件被挂载后调用。

  • beforeUpdate:在更新包含组件的 VNode 之前调用。

  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。

  • beforeUnmount:在卸载绑定元素的父组件之前调用。

  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。

介绍完这些钩子函数之后,我们再来回顾一下前面介绍的 ObjectDirective 类型:

// packages/runtime-core/src/directives.ts
export interface ObjectDirective<T = any, V = any> {
  created?: DirectiveHook<T, null, V>
  beforeMount?: DirectiveHook<T, null, V>
  mounted?: DirectiveHook<T, null, V>
  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
  updated?: DirectiveHook<T, VNode<any, T>, V>
  beforeUnmount?: DirectiveHook<T, null, V>
  unmounted?: DirectiveHook<T, null, V>
  getSSRProps?: SSRDirectiveHook
}

好的,接下来我们来分析一下 focus 指令上定义的钩子什么时候被调用。同样,阿宝哥在 focus 指令的 mounted 方法中加个断点:

在图中右侧的调用栈中,我们看到了 invokeDirectiveHook 函数,很明显该函数的作用就是调用指令上已注册的钩子。出于篇幅考虑,具体的细节阿宝哥就不继续介绍了,感兴趣的小伙伴可以自行断点调试一下。

四、阿宝哥有话说

4.1 Vue 3 有哪些内置指令?

在介绍注册全局自定义指令的过程中,我们看到了一个 validateDirectiveName 函数,该函数用于验证自定义指令的名称,从而避免自定义指令名称,与已有的内置指令名称冲突。

// packages/runtime-core/src/directives.ts
export function validateDirectiveName(name: string{
  if (isBuiltInDirective(name)) {
    warn(''Do not use built-in directive ids as custom directive id: '' + name)
  }
}

validateDirectiveName 函数内部,会通过 isBuiltInDirective(name) 语句来判断是否为内置指令:

const isBuiltInDirective = /*#__PURE__*/ makeMap(
  ''bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text''
)

以上代码中的 makeMap 函数,用于生成一个 map 对象(Object.create(null))并返回一个函数,用于检测某个 key 是否存在 map 对象中。另外,通过以上代码,我们就可以很清楚地了解 Vue 3 中为我们提供了哪些内置指令。

4.2 指令有几种类型?

在 Vue 3 中指令分为 ObjectDirectiveFunctionDirective 两种类型:

// packages/runtime-core/src/directives.ts
export type Directive<T = any, V = any> =
  | ObjectDirective<T, V>
  | FunctionDirective<T, V>
ObjectDirective
export interface ObjectDirective<T = any, V = any> {
  created?: DirectiveHook<T, null, V>
  beforeMount?: DirectiveHook<T, null, V>
  mounted?: DirectiveHook<T, null, V>
  beforeUpdate?: DirectiveHook<T, VNode<any, T>, V>
  updated?: DirectiveHook<T, VNode<any, T>, V>
  beforeUnmount?: DirectiveHook<T, null, V>
  unmounted?: DirectiveHook<T, null, V>
  getSSRProps?: SSRDirectiveHook
}
FunctionDirective
export type FunctionDirective<T = any, V = any> = DirectiveHook<T, any, V>
                              
export type DirectiveHook<T = any, Prev = VNode<any, T> | null, V = any> = (
  el: T,
  binding: DirectiveBinding<V>,
  vnode: VNode<any, T>,
  prevVNode: Prev
) => void

如果你想在 mountedupdated 时触发相同行为,而不关心其他的钩子函数。那么你可以通过将回调函数传递给指令来实现:

app.directive(''pin'', (el, binding) => {
  el.style.position = ''fixed''
  const s = binding.arg || ''top''
  el.style[s] = binding.value + ''px''
})

4.3 注册全局指令与局部指令有什么区别?

注册全局指令
app.directive(''focus'', {
  // 当被绑定的元素挂载到 DOM 中时被调用
  mounted(el) {
    el.focus() // 聚焦元素
  }
});
注册局部指令
const Component = defineComponent({
  directives: {
    focus: {
      mounted(el) {
        el.focus()
      }
    }
  },
  render() {
    const { directives } = this.$options;
    return [withDirectives(h(''input''), [[directives.focus, ]])]
  }
});
解析全局注册和局部注册的指令
// packages/runtime-core/src/helpers/resolveAssets.ts
function resolveAsset(
  typetypeof COMPONENTS | typeof DIRECTIVES,
  name: string,
  warnMissing = true
{
  const instance = currentRenderingInstance || currentInstance
  if (instance) {
    const Component = instance.type
    // 省略解析组件的处理逻辑
    const res =
      // 局部注册
      resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
      // 全局注册
      resolve(instance.appContext[type], name)
    return res
  }
}

4.4 内置指令和自定义指令生成的渲染函数有什么区别?

要了解内置指令和自定义指令生成的渲染函数的区别,阿宝哥以 v-ifv-show 内置指令和 v-focus 自定义指令为例,然后使用 Vue 3 Template Explorer 这个在线工具来编译生成渲染函数:

v-if 内置指令
<input v-if="isShow" />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock, 
      createBlock: _createBlock, createCommentVNode: _createCommentVNode } = _Vue

    return isShow
      ? (_openBlock(), _createBlock("input", { key0 }))
      : _createCommentVNode("v-if"true)
  }
}

对于 v-if 指令来说,在编译后会通过 ?: 三目运算符来实现动态创建节点的功能。

v-show 内置指令
<input v-show="isShow" />
  
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return _withDirectives((_openBlock(), _createBlock("input"nullnull512 /* NEED_PATCH */)), [
      [_vShow, isShow]
    ])
  }
}

以上示例中的 vShow 指令被定义在 packages/runtime-dom/src/directives/vShow.ts 文件中,该指令属于 ObjectDirective 类型的指令,该指令内部定义了 beforeMountmountedupdatedbeforeUnmount 四个钩子。

v-focus 自定义指令
<input v-focus />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { resolveDirective: _resolveDirective, createVNode: _createVNode, 
      withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _directive_focus = _resolveDirective("focus")
    return _withDirectives((_openBlock(), _createBlock("input"nullnull512 /* NEED_PATCH */)), [
      [_directive_focus]
    ])
  }
}

通过对比 v-focusv-show 指令生成的渲染函数,我们可知 v-focus 自定义指令与 v-show 内置指令都会通过 withDirectives 函数,把指令注册到 VNode 对象上。而自定义指令相比内置指令来说,会多一个指令解析的过程。

此外,如果在 input 元素上,同时应用了 v-showv-focus 指令,则在调用 _withDirectives 函数时,将使用二维数组:

<input v-show="isShow" v-focus />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { vShow: _vShow, resolveDirective: _resolveDirective, createVNode: _createVNode, 
      withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _directive_focus = _resolveDirective("focus")
    return _withDirectives((_openBlock(), _createBlock("input"nullnull512 /* NEED_PATCH */)), [
      [_vShow, isShow],
      [_directive_focus]
    ])
  }
}

4.5 如何在渲染函数中应用指令?

除了在模板中应用指令之外,利用前面介绍的 withDirectives 函数,我们可以很方便地在渲染函数中应用指定的指令:

<div id="app"></div>
<script>
   const { createApp, h, vShow, defineComponent, withDirectives } = Vue
   const Component = defineComponent({
     data() {
       return { valuetrue }
     },
     render() {
       return [withDirectives(h(''div''''我是阿宝哥''), [[vShow, this.value]])]
     }
   });
   const app = Vue.createApp(Component)
   app.mount(''#app'')
</script>

本文阿宝哥主要介绍了在 Vue 3 中如何自定义指令、如何注册全局和局部指令。为了让大家能够更深入地掌握自定义指令的相关知识,阿宝哥从源码的角度分析了指令的注册和应用过程。

在后续的文章中,阿宝哥将会介绍一些特殊的指令,当然也会重点分析一下双向绑定的原理,感兴趣的小伙伴不要错过哟。

五、参考资源

  • Vue 3 官网 - 自定义指令
  • Vue 3 官网 - 应用 API
聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【Vue.js】875- Vue 3.0 进阶之自定义事件探秘

【Vue.js】875- Vue 3.0 进阶之自定义事件探秘

这是 Vue 3.0 进阶系列 的第二篇文章,该系列的第一篇文章是 Vue 3.0 进阶之指令探秘。本文阿宝哥将以一个简单的示例为切入点,带大家一起一步步揭开自定义事件背后的秘密。

<div id="app"></div>
<script>
   const app = Vue.createApp({
     template''<welcome-button v-on:welcome="sayHi"></welcome-button>'',
     methods: {
       sayHi() {
         console.log(''你好,我是阿宝哥!'');
       }
     }
    })

   app.component(''welcome-button'', {
     emits: [''welcome''],
     template`
       <button v-on:click="$emit(''welcome'')">
          欢迎
       </button>
      `

    })
    app.mount("#app")
</script>

在以上示例中,我们先通过 Vue.createApp 方法创建 app 对象,之后利用该对象上的 component 方法注册全局组件 ——  welcome-button 组件。在定义该组件时,我们通过 emits 属性定义了该组件上的自定义事件。当然用户点击 欢迎 按钮时,就会发出 welcome 事件,之后就会调用 sayHi 方法,接着控制台就会输出 你好,我是阿宝哥!

虽然该示例比较简单,但也存在以下 2 个问题:

  • $emit 方法来自哪里?
  • 自定义事件的处理流程是什么?

下面我们将围绕这些问题来进一步分析自定义事件背后的机制,首先我们先来分析第一个问题。

一、$emit 方法来自哪里?

使用 Chrome 开发者工具,我们在 sayHi 方法内部加个断点,然后点击 欢迎 按钮,此时函数调用栈如下图所示:

在上图右侧的调用栈,我们发现了一个存在于 componentEmits.ts 文件中的 emit 方法。但在模板中,我们使用的是 $emit 方法,为了搞清楚这个问题,我们来看一下 onClick 方法:

由上图可知,我们的 $emit 方法来自 _ctx 对象,那么该对象是什么对象呢?同样,利用断点我们可以看到 _ctx 对象的内部结构:

很明显 _ctx 对象是一个 Proxy 对象,如果你对 Proxy 对象还不了解,可以阅读 你不知道的 Proxy 这篇文章。当访问 _ctx 对象的 $emit 属性时,将会进入 get 捕获器,所以接下来我们来分析 get 捕获器:

通过 [[FunctionLocation]] 属性,我们找到了 get 捕获器的定义,具体如下所示:

// packages/runtime-core/src/componentPublicInstance.ts
export const RuntimeCompiledPublicInstanceProxyHandlers = extend(
  {},
  PublicInstanceProxyHandlers,
  {
    get(target: ComponentRenderContext, key: string) {
      // fast path for unscopables when using `with` block
      if ((key as any) === Symbol.unscopables) {
        return
      }
      return PublicInstanceProxyHandlers.get!(target, key, target)
    },
    has(_: ComponentRenderContext, key: string) {
      const has = key[0] !== ''_'' && !isGloballyWhitelisted(key)
      // 省略部分代码
      return has
    }
  }
)

观察以上代码可知,在 get 捕获器内部会继续调用 PublicInstanceProxyHandlers 对象的 get 方法来获取 key 对应的值。由于 PublicInstanceProxyHandlers 内部的代码相对比较复杂,这里我们只分析与示例相关的代码:

// packages/runtime-core/src/componentPublicInstance.ts
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const { ctx, setupState, data, props, accessCache, type, appContext } = instance
   
    // 省略大部分内容
    const publicGetter = publicPropertiesMap[key]
    // public $xxx properties
    if (publicGetter) {
      if (key === ''$attrs'') {
        track(instance, TrackOpTypes.GET, key)
        __DEV__ && markAttrsAccessed()
      }
      return publicGetter(instance)
    },
    // 省略set和has捕获器
}

在上面代码中,我们看到了 publicPropertiesMap 对象,该对象被定义在 componentPublicInstance.ts 文件中:

// packages/runtime-core/src/componentPublicInstance.ts
const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
  $: i => i,
  $el: i => i.vnode.el,
  $data: i => i.data,
  $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
  $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
  $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
  $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
  $parent: i => getPublicInstance(i.parent),
  $root: i => getPublicInstance(i.root),
  $emit: i => i.emit,
  $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
  $forceUpdate: i => () => queueJob(i.update),
  $nextTick: i => nextTick.bind(i.proxy!),
  $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
as PublicPropertiesMap)

publicPropertiesMap 对象中,我们找到了 $emit 属性,该属性的值为 $emit: i => i.emit,即 $emit 指向的是参数 i 对象的 emit 属性。下面我们来看一下,当获取 $emit 属性时,target 对象是什么:

由上图可知 target 对象有一个 _ 属性,该属性的值是一个对象,且该对象含有 vnodetypeparent 等属性。因此我们猜测 _ 属性的值是组件实例。为了证实这个猜测,利用 Chrome 开发者工具,我们就可以轻易地分析出组件挂载过程中调用了哪些函数:

在上图中,我们看到了在组件挂载阶段,调用了 createComponentInstance 函数。顾名思义,该函数用于创建组件实例,其具体实现如下所示:

// packages/runtime-core/src/component.ts
export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
{
  const type = vnode.type as ConcreteComponent
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    appContext,
    // 省略大部分属性
    emit: null as any
    emitted: null,
  }
  if (__DEV__) { // 开发模式
    instance.ctx = createRenderContext(instance)
  } else { // 生产模式
    instance.ctx = { _: instance }
  }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  return instance
}

在以上代码中,我们除了发现 instance 对象之外,还看到了 instance.emit = emit.bind(null, instance) 这个语句。这时我们就找到了 $emit 方法来自哪里的答案。弄清楚第一个问题之后,接下来我们来分析自定义事件的处理流程。

二、自定义事件的处理流程是什么?

要搞清楚,为什么点击 欢迎 按钮派发 welcome 事件之后,就会自动调用 sayHi 方法的原因。我们就必须分析 emit 函数的内部处理逻辑,该函数被定义在 runtime-core/src/componentEmits.t 文件中:

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string,
  ...rawArgs: any[]
{
  const props = instance.vnode.props || EMPTY_OBJ
 // 省略大部分代码
  let args = rawArgs

  // convert handler name to camelCase. See issue #2249
  let handlerName = toHandlerKey(camelize(event))
  let handler = props[handlerName]

  if (handler) {
    callWithAsyncErrorHandling(
      handler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }
}

其实在 emit 函数内部还会涉及 v-model update:xxx 事件的处理,关于 v-model 指令的内部原理,阿宝哥会写单独的文章来介绍。这里我们只分析与当前示例相关的处理逻辑。

emit 函数中,会使用 toHandlerKey 函数把事件名转换为驼峰式的 handlerName

// packages/shared/src/index.ts
export const toHandlerKey = cacheStringFunction(
  (str: string) => (str ? `on${capitalize(str)}` : ``)
)

在获取 handlerName 之后,就会从 props 对象上获取该 handlerName 对应的 handler 对象。如果该 handler 对象存在,则会调用 callWithAsyncErrorHandling 函数,来执行当前自定义事件对应的事件处理函数。callWithAsyncErrorHandling 函数的定义如下:

// packages/runtime-core/src/errorHandling.ts
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
}

通过以上代码可知,如果 fn 参数是函数对象的话,在 callWithAsyncErrorHandling 函数内部还会继续调用 callWithErrorHandling 函数来最终执行事件处理函数:

// packages/runtime-core/src/errorHandling.ts
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
}

callWithErrorHandling 函数内部,使用 try catch 语句来捕获异常并进行异常处理。如果调用 fn 事件处理函数之后,返回的是一个 Promise 对象的话,则会通过 Promise 对象上的 catch 方法来处理异常。了解完上面的内容,再回顾一下前面见过的函数调用栈,相信此时你就不会再陌生了。

现在前面提到的 2 个问题,我们都已经找到答案了。为了能更好地掌握自定义事件的相关内容,阿宝哥将使用 Vue 3 Template Explorer 这个在线工具,来分析一下示例中模板编译的结果:

App 组件模板

<welcome-button v-on:welcome="sayHi"></welcome-button>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue
    const _component_welcome_button = _resolveComponent("welcome-button")

    return (_openBlock(), _createBlock(_component_welcome_button,
     { onWelcome: sayHi }, null, 8 /
* PROPS */, ["onWelcome"]))
  }
}

welcome-button 组件模板

<button v-on:click="$emit(''welcome'')">欢迎</button>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock,
      createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock("button", {
      onClick: $event => ($emit(''welcome''))
    }, "欢迎", 8 /
* PROPS */, ["onClick"]))
  }
}

观察以上结果,我们可知通过 v-on: 绑定的事件,都会转换为以 on 开头的属性,比如 onWelcomeonClick。为什么要转换成这种形式呢?这是因为在 emit 函数内部会通过 toHandlerKeycamelize 这两个函数对事件名进行转换:

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string,
  ...rawArgs: any[]
{
 // 省略大部分代码
  // convert handler name to camelCase. See issue #2249
  let handlerName = toHandlerKey(camelize(event))
  let handler = props[handlerName]
}

为了搞清楚转换规则,我们先来看一下 camelize 函数:

// packages/shared/src/index.ts
const camelizeRE = /-(\w)/g

export const camelize = cacheStringFunction(
  (str: string): string => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''''))
  }
)

观察以上代码,我们可以知道 camelize 函数的作用,用于把 kebab-case (短横线分隔命名) 命名的事件名转换为 camelCase (驼峰命名法) 的事件名,比如 "test-event" 事件名经过 camelize 函数处理后,将被转换为 "testEvent"。该转换后的结果,还会通过 toHandlerKey 函数进行进一步处理,toHandlerKey 函数被定义在 shared/src/index.ts 文件中:

// packages/shared/src/index.ts
export const toHandlerKey = cacheStringFunction(
  (str: string) => (str ? `on${capitalize(str)}` : ``)
)

export const capitalize = cacheStringFunction(
  (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
)

对于前面使用的 "testEvent" 事件名经过 toHandlerKey 函数处理后,将被最终转换为 "onTestEvent" 的形式。为了能够更直观地了解事件监听器的合法形式,我们来看一下 runtime-core 模块中的测试用例:

// packages/runtime-core/__tests__/componentEmits.spec.ts
test(''isEmitListener''() => {
  const options = {
    click: null,
    ''test-event''null,
    fooBar: null,
    FooBaz: null
  }
  expect(isEmitListener(options, ''onClick'')).toBe(true)
  expect(isEmitListener(options, ''onclick'')).toBe(false)
  expect(isEmitListener(options, ''onBlick'')).toBe(false)
  // .once listeners
  expect(isEmitListener(options, ''onClickOnce'')).toBe(true)
  expect(isEmitListener(options, ''onclickOnce'')).toBe(false)
  // kebab-case option
  expect(isEmitListener(options, ''onTestEvent'')).toBe(true)
  // camelCase option
  expect(isEmitListener(options, ''onFooBar'')).toBe(true)
  // PascalCase option
  expect(isEmitListener(options, ''onFooBaz'')).toBe(true)
})

了解完事件监听器的合法形式之后,我们再来看一下 cacheStringFunction 函数:

// packages/shared/src/index.ts
const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
  const cache: Record<stringstring> = Object.create(null)
  return ((str: string) => {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }
as any
}

以上代码也比较简单,cacheStringFunction 函数的作用是为了实现缓存功能。

三、阿宝哥有话说

3.1 如何在渲染函数中绑定事件?

在前面的示例中,我们通过 v-on 指令完成事件绑定,那么在渲染函数中如何绑定事件呢?

<div id="app"></div>
<script>
  const { createApp, defineComponent, h } = Vue
  
  const Foo = defineComponent({
    emits: ["foo"], 
    render() { return h("h3""Vue 3 自定义事件")},
    created() {
      this.$emit(''foo'');
    }
  });
  const onFoo = () => {
    console.log("foo be called")
  };
  const Comp = () => h(Foo, { onFoo })
  const app = createApp(Comp);
  app.mount("#app")
</script>

在以上示例中,我们通过 defineComponent 全局 API 定义了 Foo 组件,然后通过 h 函数创建了函数式组件 Comp,在创建 Comp 组件时,通过设置 onFoo 属性实现了自定义事件的绑定操作。

3.2 如何只执行一次事件处理器?

在模板中设置
<welcome-button v-on:welcome.once="sayHi"></welcome-button>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue
    const _component_welcome_button = _resolveComponent("welcome-button")

    return (_openBlock(), _createBlock(_component_welcome_button, 
      { onWelcomeOnce: sayHi }, null, 8 /
* PROPS */, ["onWelcomeOnce"]))
  }
}

在以上代码中,我们使用了 once 事件修饰符,来实现只执行一次事件处理器的功能。除了 once 修饰符之外,还有其他的修饰符,比如:

<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form @submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
在渲染函数中设置
<div id="app"></div>
<script>
   const { createApp, defineComponent, h } = Vue
   const Foo = defineComponent({
     emits: ["foo"], 
     render() { return h("h3", "Vue 3 自定义事件")},
     created() {
       this.$emit(''foo'');
       this.$emit(''foo'');
     }
   });
   const onFoo = () => {
     console.log("foo be called")
   };
   /
/ 在事件名后添加Once,表示该事件处理器只执行一次
   const Comp = () => h(Foo, { onFooOnce: onFoo })
   const app = createApp(Comp);
   app.mount("#app")
</
script>

以上两种方式都能生效的原因是,模板中的指令 v-on:welcome.once,经过编译后会转换为onWelcomeOnce,并且在 emit 函数中定义了 once 修饰符的处理规则:

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string,
  ...rawArgs: any[]
{
  const props = instance.vnode.props || EMPTY_OBJ

  const onceHandler = props[handlerName + `Once`]
  if (onceHandler) {
    if (!instance.emitted) {
      ;(instance.emitted = {} as Record<stringboolean>)[handlerName] = true
    } else if (instance.emitted[handlerName]) {
      return
    }
    callWithAsyncErrorHandling(
      onceHandler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }
}

3.3 如何添加多个事件处理器

在模板中设置
<div @click="foo(), bar()"/>
  
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock, 
      createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock("div", {
      onClick$event => (foo(), bar())
    }, null8 /* PROPS */, ["onClick"]))
  }
}
在渲染函数中设置
<div id="app"></div>
<script>
   const { createApp, defineComponent, h } = Vue
   const Foo = defineComponent({
     emits: ["foo"], 
     render() { return h("h3""Vue 3 自定义事件")},
     created() {
       this.$emit(''foo'');
     }
   });
   const onFoo = () => {
     console.log("foo be called")
   };
   const onBar = () => {
     console.log("bar be called")
   };
   const Comp = () => h(Foo, { onFoo: [onFoo, onBar] })
   const app = createApp(Comp);
  app.mount("#app")
</script>

以上方式能够生效的原因是,在前面介绍的 callWithAsyncErrorHandling 函数中含有多个事件处理器的处理逻辑:

// packages/runtime-core/src/errorHandling.ts
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any[] 
{
  if (isFunction(fn)) {
   // 省略部分代码
  }

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

3.4 Vue 3 的 $emit 与 Vue 2 的 $emit 有什么区别?

在 Vue 2 中 $emit 方法是 Vue.prototype 对象上的属性,而 Vue 3 上的 $emit 是组件实例上的一个属性,instance.emit = emit.bind(null, instance)

// src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>{
  const hookRE = /^hook:/

  // 省略$on、$once和$off等方法的定义
  // Vue实例是一个EventBus对象
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

本文阿宝哥主要介绍了在 Vue 3 中自定义事件背后的秘密。为了让大家能够更深入地掌握自定义事件的相关知识,阿宝哥从源码的角度分析了 $emit 方法的来源和自定义事件的处理流程。

在 Vue 3.0 进阶系列第一篇文章 Vue 3.0 进阶之指令探秘 中,我们已经介绍了指令相关的知识,有了这些基础,之后阿宝哥将带大家一起探索 Vue 3 双向绑定的原理,感兴趣的小伙伴不要错过哟。

四、参考资源

  • Vue 3 官网 - 事件处理
  • Vue 3 官网 - 自定义事件
  • Vue 3 官网 - 全局 API
聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【Vue.js】880- Vue 3.0 进阶之双向绑定探秘

【Vue.js】880- Vue 3.0 进阶之双向绑定探秘

本文是 Vue 3.0 进阶系列 的第三篇文章,在阅读本文前,建议你先阅读 Vue 3.0 进阶之指令探秘Vue 3.0 进阶之自定义事件探秘 这两篇文章。在看具体示例前,阿宝哥先来简单介绍一下双向绑定,它由两个单向绑定组成:

  • 模型 —> 视图数据绑定;
  • 视图 —> 模型事件绑定。

在 Vue 中 :value 实现了 模型到视图 的数据绑定,@event 实现了 视图到模型 的事件绑定:

<input :value="searchText" @input="searchText = $event.target.value" />

而在表单中,通过使用内置的 v-model 指令,我们可以轻松地实现双向绑定,比如 <input v-model="searchText" />。介绍完上面的内容,接下来阿宝哥将以一个简单的示例为切入点,带大家一起一步步揭开双向绑定背后的秘密。

<div id="app">
   <input v-model="searchText" />
   <p>搜索的内容:{{searchText}}</p>
</div>
<script>
   const { createApp } = Vue
   const app = Vue.createApp({
     data() {
       return {
         searchText"阿宝哥"
       }
     }
   })
   app.mount(''#app'')
</script>

在以上示例中,我们在 input 搜索输入框中应用了 v-model 指令,当输入框的内容发生变化时,p 标签中内容会同步更新。

要揭开 v-model 指令背后的秘密,我们可以利用 Vue 3 Template Explorer 在线工具,来看一下模板编译后的结果:

<input v-model="searchText" />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { vModelText: _vModelText, createVNode: _createVNode, 
      withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return _withDirectives((_openBlock(), _createBlock("input", {
      "onUpdate:modelValue"$event => (searchText = $event)
    }, null8 /* PROPS */, ["onUpdate:modelValue"])), 
    [ 
      [_vModelText, searchText] 
    ])
  }
}

<input v-model="searchText" /> 模板生成的渲染函数中,我们看到了 Vue 3.0 进阶之指令探秘 文章中介绍的 withDirectives 函数,该函数用于把指令信息添加到 VNode 对象上,它被定义在 runtime-core/src/directives.ts 文件中:

// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode>(
  vnode: T,
  directives: DirectiveArguments
): T 
{
  const internalInstance = currentRenderingInstance
  // 省略部分代码
  const instance = internalInstance.proxy
  const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    // 在 mounted 和 updated 时,触发相同行为,而不关系其他的钩子函数
    if (isFunction(dir)) { // 处理函数类型指令
      dir = {
        mounted: dir,
        updated: dir
      } as ObjectDirective
    }
    bindings.push({ // 把指令信息保存到vnode.dirs数组中
      dir, instance, value, 
      oldValue: void 0, arg, modifiers
    })
  }
  return vnode
}

除此之外,在模板生成的渲染函数中,我们看到了 vModelText 指令,通过它的名称,我们猜测该指令与模型相关,所以我们先来分析 vModelText 指令。

一、vModelText 指令

vModelText 指令是 ObjectDirective 类型的指令,该指令中定义了 3 个钩子函数:

  • created:在绑定元素的属性或事件监听器被应用之前调用。
  • mounted:在绑定元素的父组件被挂载后调用。
  • beforeUpdate:在更新包含组件的 VNode 之前调用。
// packages/runtime-dom/src/directives/vModel.ts
type ModelDirective<T> = ObjectDirective<T & { _assign: AssignerFn }>

export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    // ...
  },
  mounted(el, { value }) {
    // ..
  },
  beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
    // ..
  }
}

接下来,阿宝哥将逐一分析每个钩子函数,这里先从 created 钩子函数开始。

1.1 created 钩子

// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    el._assign = getModelAssigner(vnode)
    const castToNumber = number || el.type === ''number'' // 是否转为数值类型
    // 若使用 lazy 修饰符,则在 change 事件触发后将输入框的值与数据进行同步
    addEventListener(el, lazy ? ''change'' : ''input''e => { 
      if ((e.target as any).composing) return // 组合输入进行中
      let domValue: string | number = el.value
      if (trim) { // 自动过滤用户输入的首尾空白字符
        domValue = domValue.trim()
      } else if (castToNumber) { // 自动将用户的输入值转为数值类型
        domValue = toNumber(domValue)
      }
      el._assign(domValue) // 更新模型
    })
    if (trim) {
      addEventListener(el, ''change''() => {
        el.value = el.value.trim()
      })
    }
    if (!lazy) {
      addEventListener(el, ''compositionstart'', onCompositionStart)
      addEventListener(el, ''compositionend'', onCompositionEnd)
      // Safari < 10.2 & UIWebView doesn''t fire compositionend when
      // switching focus before confirming composition choice
      // this also fixes the issue where some browsers e.g. iOS Chrome
      // fires "change" instead of "input" on autocomplete.
      addEventListener(el, ''change'', onCompositionEnd)
    }
  },
}

对于 created 方法来说,它会通过解构的方式获取 v-model 指令上添加的修饰符,在 v-model 上可以添加 .lazy.number.trim 修饰符。这里我们简单介绍一下 3 种修饰符的作用:

  • .lazy 修饰符:在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步。你可以添加 lazy 修饰符,从而转为在 change 事件之后进行同步。

    <!-- 在 change 时而非 input 时更新 -->
    <input v-model.lazy="msg" />
  • .number 修饰符:如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符。这通常很有用,因为即使在 type="number" 时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat() 解析,则会返回原始的值。

    <input v-model.number="age" type="number" />
  • .trim 修饰符:如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符。

    <input v-model.trim="msg" />

而在 created 方法内部,会通过 getModelAssigner 函数获取 ModelAssigner,从而用于更新模型对象。

// packages/runtime-dom/src/directives/vModel.ts
const getModelAssigner = (vnode: VNode): AssignerFn => {
  const fn = vnode.props![''onUpdate:modelValue'']
  return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
}

对于我们的示例来说,通过 getModelAssigner 函数获取的 ModelAssigner 对象是 $event => (searchText = $event) 函数。在获取  ModelAssigner 对象之后,我们就可以更新模型的值了。created 方法中的其他代码相对比较简单,阿宝哥就不详细介绍了。这里我们来介绍一下 compositionstartcompositionend 事件。

中文、日文、韩文等需要借助输入法组合输入,即使是英文,也可以利用组合输入进行选词等操作。在一些实际场景中,我们希望等用户组合输入完的一段文字才进行对应操作,而不是每输入一个字母,就执行相关操作。

比如,在关键字搜索场景中,等用户完整输入 阿宝哥 之后再执行搜索操作,而不是输入字母 a 之后就开始搜索。要实现这个功能,我们就需要借助 compositionstartcompositionend 事件。另外,需要注意的是,compositionstart 事件发生在 input 事件之前,因此利用它可以优化中文输入的体验。

了解完 compositionstart(组合输入开始) 和 compositionend (组合输入结束)事件,我们再来看一下 onCompositionStartonCompositionEnd 这两个事件处理器:

function onCompositionStart(e: Event{
  ;(e.target as any).composing = true
}

function onCompositionEnd(e: Event{
  const target = e.target as any
  if (target.composing) { 
    target.composing = false
    trigger(target, ''input'')
  }
}

// 触发元素上的指定事件
function trigger(el: HTMLElement, typestring{
  const e = document.createEvent(''HTMLEvents'')
  e.initEvent(typetruetrue)
  el.dispatchEvent(e)
}

当组合输入时,在 onCompositionStart 事件处理器中,会 e.target 对象上添加 composing 属性并设置该属性的值为 true。而在 change 事件或 input 事件回调函数中,如果发现  e.target 对象的 composing 属性为 true 则会直接返回。当组合输入完成后,在 onCompositionEnd 事件处理器中,会把 target.composing 的值设置为 false 并手动触发 input 事件:

// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    // 省略部分代码
    addEventListener(el, lazy ? ''change'' : ''input''e => {
      if ((e.target as any).composing) return
     // ...
    })
  },
}

好的,created 钩子函数就分析到这里,接下来我们来分析 mounted 钩子。

1.2 mounted 钩子

// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  // set value on mounted so it''s after min/max for type="range"
  mounted(el, { value }) {
    el.value = value == null ? '''' : value
  },
}

mounted 钩子的逻辑很简单,如果 value 值为 null 时,把元素的值设置为空字符串,否则直接使用 value 的值。

1.3 beforeUpdate 钩子

// packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
    el._assign = getModelAssigner(vnode)
    // avoid clearing unresolved text. #2302
    if ((el as any).composing) return
    if (document.activeElement === el) {
      if (trim && el.value.trim() === value) {
        return
      }
      if ((number || el.type === ''number'') && toNumber(el.value) === value) {
        return
      }
    }
    const newValue = value == null ? '''' : value
    if (el.value !== newValue) { // 新旧值不相等时,执行更新操作
      el.value = newValue
    }
  }
}

相信使用过 Vue 的小伙伴都知道,v-model 指令不仅可以应用在 inputtextarea 元素上,在复选框(Checkbox)、单选框(Radio)和选择框(Select)上也可以使用 v-model 指令。不过需要注意的是,虽然这些元素上都是使用 v-model 指令,但实际上对于复选框、单选框和选择框来说,它们是由不同的指令来完成对应的功能。这里我们以单选框为例,来看一下应用 v-model 指令后,模板编译的结果:

<input type="radio" value="One" v-model="picked" />

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { vModelRadio: _vModelRadio, createVNode: _createVNode, 
      withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return _withDirectives((_openBlock(), _createBlock("input", {
      type"radio",
      value"One",
      "onUpdate:modelValue"$event => (picked = $event)
    }, null8 /* PROPS */, ["onUpdate:modelValue"])), [
      [_vModelRadio, picked]
    ])
  }
}

由以上代码可知,在单选框应用 v-model 指令后,双向绑定的功能会交给 vModelRadio 指令来实现。除了 vModelRadio 之外,还有 vModelSelectvModelCheckbox 指令,它们被定义在 runtime-dom/src/directives/vModel.ts 文件中,感兴趣的小伙伴可以自行研究一下。

其实 v-model 本质上是语法糖。它负责监听用户的输入事件来更新数据,并在某些场景下进行一些特殊处理。需要注意的是 v-model 会忽略所有表单元素的 valuecheckedselected attribute 的初始值而总是将当前活动实例的数据作为数据来源。你应该通过在组件的 data 选项中声明初始值。

此外,v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 元素使用 check property 和 change 事件;
  • select 元素将 value 作为 prop 并将 change 作为事件。

这里你已经知道,可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。但如果你也想在组件上使用 v-model 指令来创建双向数据绑定,那应该如何实现呢?

二、在组件上使用 v-model

假设你想定义一个 custom-input 组件并在该组件上使用 v-model 指令来实现双向绑定,在实现该功能前,我们先利用 Vue 3 Template Explorer 在线工具,看一下模板编译后的结果:

<custom-input v-model="searchText"></custom-input>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _component_custom_input = _resolveComponent("custom-input")
    return (_openBlock(), _createBlock(_component_custom_input, {
      modelValue: searchText,
      "onUpdate:modelValue": $event => (searchText = $event)
    }, null, 8 /
* PROPS */, ["modelValue", "onUpdate:modelValue"]))
  }
}

通过观察以上的渲染函数,我们可知在 custom-input 组件上应用了 v-model 指令,经过编译器编译之后,会生成一个名为 modelValue 的输入属性和一个名为 update:modelValue 的自定义事件名。如果你对自定义事件内部原理还不清楚的话,可以阅读 Vue 3.0 进阶之自定义事件探秘 这篇文章。了解完这些内容之后,我们就可以开始实现 custom-input 组件了:

<div id="app">
   <custom-input v-model="searchText"></custom-input>
   <p>搜索的内容:{{searchText}}</p>
</div>
<script>
   const { createApp } = Vue
   const app = Vue.createApp({
     data() {
       return {
         searchText: "阿宝哥"
       }
     }
    })
   app.component(''custom-input'', {
     props: [''modelValue''],
     emits: [''update:modelValue''],
     template: `
       <input type="text" 
          :value="modelValue"
          @input="$emit(''update:modelValue'', $event.target.value)"
       >`
   })
   app.mount(''#app'')
</
script>

在自定义组件中实现双向绑定的功能,除了使用自定义事件之外,还可以使用计算属性的功能来定义 gettersetter。这里阿宝哥就不展开介绍了,感兴趣的小伙伴可以阅读 Vue 3 官网 - 组件基础 的相关内容。

三、阿宝哥有话说

3.1 如何修改 v-model 默认的 prop 名和事件名?

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 指令传递参数来修改这些名称:

<custom-input v-model:name="searchText"></custom-input>

以上的模板,经过编译器编译后的结果如下:

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _component_custom_input = _resolveComponent("custom-input")
    return (_openBlock(), _createBlock(_component_custom_input, {
      name: searchText,
      "onUpdate:name"$event => (searchText = $event)
    }, null8 /* PROPS */, ["name""onUpdate:name"]))
  }
}

通过观察生成的渲染函数,我们可知自定义 custom-input 组件接收一个 name 输入属性并含有一个名为 update:name 的自定义事件:

app.component(''custom-input'', {
  props: {
    nameString
  },
  emits: [''update:name''],
  template`
    <input type="text"
      :value="name"
      @input="$emit(''update:name'', $event.target.value)">
  `

})

至于自定义的事件名为什么是 "onUpdate:name" 这种形式,你可以从 Vue 3.0 进阶之自定义事件探秘 这篇文章中介绍的 emit 函数中找到对应的答案。

3.2 能否在组件上使用多个 v-model 指令?

在某些场景下,我们是希望在组件上使用多个 v-model 指令,每个指令与不同的数据做绑定。比如一个 user-name 组件,该组件允许用户输入 firstNamelastName。该组件期望的使用方式如下:

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
>
</user-name>

同样,我们使用 Vue 3 Template Explorer 在线工具,先来看一下以上模板编译后的结果:

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _component_user_name = _resolveComponent("user-name")
    return (_openBlock(), _createBlock(_component_user_name, {
      "first-name": firstName,
      "onUpdate:first-name"$event => (firstName = $event),
      "last-name": lastName,
      "onUpdate:last-name"$event => (lastName = $event)
    }, null8 /* PROPS */, ["first-name""onUpdate:first-name""last-name""onUpdate:last-name"]))
  }
}

通过观察以上的输出结果,我们可知 v-model:first-namev-model:last-name 都会生成对应的 prop 属性和自定义事件。HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法)的 prop 名需要使用其等价的 kebab-case(短横线分隔命名)命名。比如:

<!-- kebab-case in HTML -->
<blog-post post-title="hello!"></blog-post>

app.component(''blog-post'', {
  props: [''postTitle''],
  template''<h3>{{ postTitle }}</h3>''
})

反之,对于 first-namelast-name 属性名来说,在定义 user-name 组件时,我们将使用 firstNamelastName 驼峰命名方式。

 <div id="app">
    <user-name
       v-model:first-name="firstName"
       v-model:last-name="lastName">

    </user-name>
    Your name: {{firstName}} {{lastName}}
</div>
<script>
   const { createApp } = Vue
   const app = Vue.createApp({
     data() {
       return {
         firstName"",
         lastName""
       }
     }
   })
   app.component(''user-name'', {
     props: {
       firstNameString,
       lastNameString
     },
     emits: [''update:firstName''''update:lastName''],
     template`
       <input
          type="text"
          :value="firstName"
          @input="$emit(''update:firstName'', $event.target.value)">
       <input
          type="text"
          :value="lastName"
          @input="$emit(''update:lastName'', $event.target.value)">
      `

   })
   app.mount(''#app'')
</script>

在以上的代码中,user-name 组件使用的自定义属性和事件名都是驼峰的形式。很明显与模板编译后生成的命名格式不一致,那么以上的 user-name 组件可以正常工作么?答案是可以的,这是因为对于自定义事件来说,在 emit 函数内部会通过 hyphenate 函数,把事件名从 camelCase(驼峰命名法)的形式转换为 kebab-case(短横线分隔命名)的形式,即 hyphenate(event)

// packages/runtime-core/src/componentEmits.ts
export function emit(
  instance: ComponentInternalInstance,
  event: string,
  ...rawArgs: any[]
{
  // 省略部分代码
  // for v-model update:xxx events, also trigger kebab-case equivalent
  // for props passed via kebab-case
  if (!handler && isModelListener) {
    handlerName = toHandlerKey(hyphenate(event))
    handler = props[handlerName]
  }

  if (handler) {
    callWithAsyncErrorHandling(
      handler,
      instance,
      ErrorCodes.COMPONENT_EVENT_HANDLER,
      args
    )
  }
}

hyphenate 函数的实现也很简单,具体如下所示:

// packages/shared/src/index.ts
const hyphenateRE = /\B([A-Z])/g

// cacheStringFunction 函数提供了缓存功能
export const hyphenate = cacheStringFunction((str: string) =>
  str.replace(hyphenateRE, ''-$1'').toLowerCase()
)

3.3 如何为 v-model 添加自定义修饰符?

在前面阿宝哥已经介绍了 v-model 的内置修饰符:.trim.number.lazy。但在某些场景下,你可能希望自定义修饰符。在介绍如何自定义修饰符前,我们再次利用 Vue 3 Template Explorer 在线工具,看一下 v-model 使用内置修饰符后,模板编译的结果:

<custom-input v-model.lazy.number="searchText"></custom-input>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _component_custom_input = _resolveComponent("custom-input")
    return (_openBlock(), _createBlock(_component_custom_input, {
      modelValue: searchText,
      "onUpdate:modelValue": $event => (searchText = $event),
      modelModifiers: { lazy: true, number: true }
    }, null, 8 /
* PROPS */, ["modelValue", "onUpdate:modelValue"]))
  }
}

通过观察生成的渲染函数,我们可以看到 v-model 上添加的 .lazy.number 修饰符,被编译到 modelModifiers prop 属性中。假设我们要为自定义一个 capitalize 修饰符 ,该修饰符的作用是将 v-model 绑定字符串的第一个字母转成大写:

<custom-input v-model.capitalize="searchText"></custom-input>

const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
  with (_ctx) {
    const { resolveComponent: _resolveComponent, createVNode: _createVNode, 
      openBlock: _openBlock, createBlock: _createBlock } = _Vue

    const _component_custom_input = _resolveComponent("custom-input")
    return (_openBlock(), _createBlock(_component_custom_input, {
      modelValue: searchText,
      "onUpdate:modelValue": $event => (searchText = $event),
      modelModifiers: { capitalize: true }
    }, null, 8 /
* PROPS */, ["modelValue", "onUpdate:modelValue"]))
  }
}

很明显 v-model 上的 .capitalize 修饰符,也被编译到 modelModifiers prop 属性中。了解完这些,我们就可以实现上述的修饰符,具体如下所示:

<div id="app">
   <custom-input v-model.capitalize="searchText"></custom-input>
   <p>搜索的内容:{{searchText}}</p>
</div>
<script>
   const { createApp } = Vue
   const app = Vue.createApp({
     data() {
       return {
         searchText: ""
       }
     }
   })
   app.component(''custom-input'', {
     props: {
       modelValue: String,
       modelModifiers: {
         default: () => ({})
       }
     },
     emits: [''update:modelValue''],
     methods: {
       emitValue(e) {
         let value = e.target.value
         if (this.modelModifiers.capitalize) {
           value = value.charAt(0).toUpperCase() + value.slice(1)
         }
         this.$emit(''update:modelValue'', value)
       }
     },
     template: `<input
       type="text"
       :value="modelValue"
       @input="emitValue">`
   })
  app.mount(''#app'')
</
script>

本文阿宝哥主要介绍了双向绑定的概念和 Vue 3 中双向绑定背后的原理。为了让大家能够更深入地掌握 v-model 的相关知识,阿宝哥从源码的角度分析了 vModelText 指令的内部实现。

此外,阿宝哥还介绍了在组件中如何使用多个 v-model 指令及如何为 v-model 添加自定义修饰符。Vue 3.0 进阶系列的文章还在持续更新,感兴趣的小伙伴请持续关注哟。

四、参考资源

  • Vue 3 官网 - 自定义指令
  • Vue 3 官网 - 自定义事件
聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【Vue.js】890- Vue 3.0 进阶之 VNode 探秘

【Vue.js】890- Vue 3.0 进阶之 VNode 探秘

本文是 Vue 3.0 进阶系列 的第五篇文章,在这篇文章中,阿宝哥将介绍 Vue 3 中的核心对象 —— VNode,该对象用于描述节点的信息,它的全称是虚拟节点(virtual node)。与 “虚拟节点” 相关联的另一个概念是 “虚拟 DOM”,它是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。通常一个 Vue 应用会以一棵嵌套的组件树的形式来组织:

(图片来源:https://v3.cn.vuejs.org/)

所以 “虚拟 DOM” 对 Vue 应用来说,是至关重要的。而 “虚拟 DOM” 又是由 VNode 组成的,它是 Vue 底层的核心基石。接下来,阿宝哥将带大家一起来探索 Vue 3 中与 VNode 相关的一些知识。

一、VNode 长什么样?

// packages/runtime-core/src/vnode.ts
export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
 // 省略内部的属性
}

runtime-core/src/vnode.ts 文件中,我们找到了 VNode 的类型定义。通过 VNode 的类型定义可知,VNode 本质是一个对象,该对象中按照属性的作用,分为 5 大类。这里阿宝哥只详细介绍其中常见的两大类型属性 —— 内部属性DOM 属性

1.1 内部属性

__v_isVNode: true // 标识是否为VNode
[ReactiveFlags.SKIP]: true // 标识VNode不是observable
type: VNodeTypes // VNode 类型
props: (VNodeProps & ExtraProps) | null // 属性信息
key: string | number | null // 特殊 attribute 主要用在 Vue 的虚拟 DOM 算法
ref: VNodeNormalizedRef | null // 被用来给元素或子组件注册引用信息。
scopeId: string | null // SFC only
children: VNodeNormalizedChildren // 保存子节点
component: ComponentInternalInstance | null // 指向VNode对应的组件实例
dirs: DirectiveBinding[] | null // 保存应用在VNode的指令信息
transition: TransitionHooks<HostElement> | null // 存储过渡效果信息

1.2 DOM 属性

el: HostNode | null // element 
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
staticCount: number // number of elements contained in a static vnode

1.3 suspense 属性

suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null

1.4 optimization 属性

shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null

1.5 应用上下文属性

appContext: AppContext | null

二、如何创建 VNode?

要创建 VNode 对象的话,我们可以使用 Vue 提供的 h 函数。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h() 。该函数接受三个参数:

// packages/runtime-core/src/h.ts
export function h(typeany, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length
  if (l === 2) { 
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { 
      // single vnode without props
      if (isVNode(propsOrChildren)) {
        return createVNode(typenull, [propsOrChildren])
      }
      // 只包含属性不含有子元素
      return createVNode(type, propsOrChildren) // h(''div'', { id: ''foo'' })
    } else {
      // 忽略属性
      return createVNode(typenull, propsOrChildren) // h(''div'', [''foo''])
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

观察以上代码可知, h 函数内部的主要处理逻辑就是根据参数个数和参数类型,执行相应处理操作,但最终都是通过调用 createVNode 函数来创建 VNode 对象。在开始介绍 createVNode 函数前,阿宝哥先举一些实际开发中的示例:

const app = createApp({ // 示例一
  render() => h(''div''''我是阿宝哥'')
})

const Comp = () => h("p""我是阿宝哥"); // 示例二

app.component(''component-a'', { // 示例三
  template"<p>我是阿宝哥</p>"
})

示例一和示例二很明显都使用了 h 函数,而示例三并未看到 hcreateVNode 函数的身影。为了一探究竟,我们需要借助 Vue 3 Template Explorer 这个在线工具来编译一下 "<p>我是阿宝哥</p>" 模板,该模板编译后的结果如下(函数模式):

// https://vue-next-template-explorer.netlify.app/
const _Vue = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options{
  with (_ctx) {
    const { createVNode: _createVNode, openBlock: _openBlock,
      createBlock: _createBlock } = _Vue
    return (_openBlock(), _createBlock("p"null"我是阿宝哥"))
  }
}

由以上编译结果可知, "<p>我是阿宝哥</p>" 模板被编译生成了一个 render 函数,调用该函数后会返回 createBlock 函数的调用结果。其中 createBlock 函数的实现如下所示:

// packages/runtime-core/src/vnode.ts
export function createBlock(
  type: VNodeTypes | ClassComponent,
  props?: Record<stringany> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[]
): VNode 
{
  const vnode = createVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    true /* isBlock: prevent a block from tracking itself */
  )
  // 省略部分代码
  return vnode
}

createBlock 函数内部,我们终于看到了 createVNode 函数的身影。顾名思义,该函数的作用就是用于创建 VNode,接下来我们来分析一下它。

三、createVNode 函数内部做了啥?

下面我们将从参数说明和逻辑说明两方面来介绍 createVNode 函数:

3.1 参数说明

createVNode 被定义在 runtime-core/src/vnode.ts 文件中:

// packages/runtime-core/src/vnode.ts
export const createVNode = (__DEV__
  ? createVNodeWithArgsTransform
  : _createVNode) as typeof _createVNode

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode 
{
  // 
  return vnode
}

在分析该函数的具体代码前,我们先来看一下它的参数。该函数可以接收 6 个参数,这里阿宝哥用思维导图来重点介绍前面 2 个参数:

type 参数
// packages/runtime-core/src/vnode.ts
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  // 省略其他参数
): VNode 
{ ... }

由上图可知,type 参数支持很多类型,比如常用的 stringVNodeComponent 等。此外,也有一些陌生的面孔,比如 TextCommentStaticFragment 等类型,它们的定义如下:

// packages/runtime-core/src/vnode.ts
export const Text = Symbol(__DEV__ ? ''Text'' : undefined)
export const Comment = Symbol(__DEV__ ? ''Comment'' : undefined)
export const Static = Symbol(__DEV__ ? ''Static'' : undefined)

export const Fragment = (Symbol(__DEV__ ? ''Fragment'' : undefinedas anyas {
  __isFragment: true
  new (): {
    $props: VNodeProps
  }
}

那么定义那么多的类型有什么意义呢?这是因为在 patch 阶段,会根据不同的 VNode 类型来执行不同的操作:

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any 
{
  const patch: PatchFn = (
    n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null,
    isSVG = false, optimized = false
  ) => {
    // 省略部分代码
    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text: // 处理文本节点
        processText(n1, n2, container, anchor)
        break
      case Comment: // 处理注释节点
        processCommentNode(n1, n2, container, anchor)
        break
      case Static: // 处理静态节点
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment: // 处理Fragment节点
        processFragment(...)
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) { // 元素类型
          processElement(...)
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件类型
          processComponent(...)
        } else if (shapeFlag & ShapeFlags.TELEPORT) { // teleport内置组件
          ;(type as typeof TeleportImpl).process(...)
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(...)
        }
    }
  }
}

介绍完 type 参数后,接下来我们来看 props 参数,具体如下图所示:

props 参数
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
): VNode 
{ ... }

props 参数的类型是联合类型,这里我们来分析 Data & VNodeProps 交叉类型:

其中 Data 类型是通过 TypeScript 内置的工具类型 Record 来定义的:

export type Data = Record<string, unknown>
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

VNodeProps 类型是通过类型别名来定义的,除了含有 keyref 属性之外,其他的属性主要是定义了与生命周期有关的钩子:

// packages/runtime-core/src/vnode.ts
export type VNodeProps = {
  key?: string | number
  ref?: VNodeRef

  // vnode hooks
  onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
  onVnodeMounted?: VNodeMountHook | VNodeMountHook[]
  onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]
  onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]
  onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]
  onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]
}

3.2 逻辑说明

createVNode 函数内部涉及较多的处理逻辑,这里我们只分析主要的逻辑:

// packages/runtime-core/src/vnode.ts
function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode 
{
  // 处理VNode类型,比如处理动态组件的场景:<component :is="vnode"/>
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // 类组件规范化处理
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 类和样式规范化处理
  if (props) {
    // 省略相关代码
  }

  // 把vnode的类型信息转换为位图
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT // ELEMENT = 1
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE // SUSPENSE = 1 << 7,
      : isTeleport(type)
        ? ShapeFlags.TELEPORT // TELEPORT = 1 << 6,
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT // STATEFUL_COMPONENT = 1 << 2,
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT // FUNCTIONAL_COMPONENT = 1 << 1,
            : 0

  // 创建VNode对象
  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    // ...
  }

  // 子元素规范化处理
  normalizeChildren(vnode, children)
  return vnode
}

介绍完 createVNode 函数之后,阿宝哥再来介绍另一个比较重要的函数 —— normalizeVNode

四、如何创建规范的 VNode 对象?

normalizeVNode 函数的作用,用于将传入的 child 参数转换为规范的 VNode 对象。

// packages/runtime-core/src/vnode.ts
export function normalizeVNode(child: VNodeChild): VNode {
  if (child == null || typeof child === ''boolean'') { // null/undefined/boolean -> Comment
    return createVNode(Comment)
  } else if (isArray(child)) { // array -> Fragment
    return createVNode(Fragment, null, child)
  } else if (typeof child === ''object'') { // VNode -> VNode or mounted VNode -> cloned VNode
    return child.el === null ? child : cloneVNode(child)
  } else { // primitive types:''foo'' or 1
    return createVNode(Text, nullString(child))
  }
}

由以上代码可知,normalizeVNode 函数内部会根据 child 参数的类型进行不同的处理:

4.1 null / undefined -> Comment

expect(normalizeVNode(null)).toMatchObject({ type: Comment })
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })

4.2 boolean -> Comment

expect(normalizeVNode(true)).toMatchObject({ type: Comment })
expect(normalizeVNode(false)).toMatchObject({ type: Comment })

4.3 array -> Fragment

expect(normalizeVNode([''foo''])).toMatchObject({ type: Fragment })

4.4 VNode -> VNode

const vnode = createVNode(''div'')
expect(normalizeVNode(vnode)).toBe(vnode)

4.5 mounted VNode -> cloned VNode

const mounted = createVNode(''div'')
mounted.el = {}
const normalized = normalizeVNode(mounted)
expect(normalized).not.toBe(mounted)
expect(normalized).toEqual(mounted)

4.6 primitive types

expect(normalizeVNode(''foo'')).toMatchObject({ type: Text, children: `foo` })
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })

五、阿宝哥有话说

5.1 如何判断是否为 VNode 对象?

// packages/runtime-core/src/vnode.ts
export function isVNode(value: any): value is VNode {
  return value ? value.__v_isVNode === true : false
}

VNode 对象中含有一个 __v_isVNode 内部属性,利用该属性可以用来判断当前对象是否为 VNode 对象。

5.2 如何判断两个 VNode 对象的类型是否相同?

// packages/runtime-core/src/vnode.ts
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  // 省略__DEV__环境的处理逻辑
  return n1.type === n2.type && n1.key === n2.key
}

在 Vue 3 中,是通过比较 VNode 对象的 typekey 属性,来判断两个 VNode 对象的类型是否相同。

5.3 如何快速创建某些类型的 VNode 对象?

在 Vue 3 内部提供了 createTextVNodecreateCommentVNodecreateStaticVNode 函数来快速的创建文本节点、注释节点和静态节点:

createTextVNode
export function createTextVNode(text: string = '' '', flag: number = 0): VNode {
  return createVNode(Text, null, text, flag)
}
createCommentVNode
export function createCommentVNode(
  text: string = '''',
  asBlock: boolean = false
): VNode 
{
  return asBlock
    ? (openBlock(), createBlock(Comment, null, text))
    : createVNode(Comment, null, text)
}
createStaticVNode
export function createStaticVNode(
  content: string,
  numberOfNodes: number
): VNode 
{
  const vnode = createVNode(Static, null, content)
  vnode.staticCount = numberOfNodes
  return vnode
}

本文阿宝哥主要介绍了 VNode 对象是什么、如何创建 VNode 对象及如何创建规范的 VNode 对象。为了让大家能够更深入地理解 hcreateVNode 函数的相关知识,阿宝哥还从源码的角度分析了 createVNode 函数 。

在后续的文章中,阿宝哥将会介绍 VNode 在 Vue 3 内部是如何被使用的,感兴趣的小伙伴不要错过哟。

六、参考资源

聚焦全栈,专注分享 TypeScript、Web API、前端架构等技术干货。

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

我们今天的关于【Vue.js】900- Vue 3.0 进阶之 VNode 探秘vue的vnode的分享就到这里,谢谢您的阅读,如果想了解更多关于【Vue.js】873- Vue 3.0 进阶之指令探秘、【Vue.js】875- Vue 3.0 进阶之自定义事件探秘、【Vue.js】880- Vue 3.0 进阶之双向绑定探秘、【Vue.js】890- Vue 3.0 进阶之 VNode 探秘的相关信息,可以在本站进行搜索。

本文标签:

上一篇nodejs 完成 mqtt 服务端(nodemcu mqtt)

下一篇后台json数据加载到grid的store中(加载json文件)