Loading...
墨滴

剑大瑞

2021/12/16  阅读:41  主题:全栈蓝

第七篇`Vue3 RunTimeCore`——高阶 `API`

第七篇Vue3 RunTimeCore——高阶 API

本片文章,会从h函数引入,逐步了解到hcreateVNodecloneVNodemergePropsisVNode 等高阶API的使用方式及原理。

渲染函数h()

Vue2中,有个全局API:render函数。Vue内部回给这个函数传递一个h函数,用于创建Vnode的描述对象。

这次,在Vue3中。将h函数独立出来,作为一个单独的API,它的作用仍保持原样:用于创建一个描述所渲染节点的Vnode描述对象。

可以接受三个参数: typepropschildren

  • type用于表示Vnode节点类型,可以是HTML标签名、组件、异步组件或函数式组件。使用返回null的函数将渲染一个注释,此参数必传。
  • props是一个对象,与我们将在模板中使用的 attributeprop 和事件相对应。可选。
  • children是子节点 VNode,使用 h() 生成,或者使用字符串来获取“文本 VNode”,或带有插槽的对象。可选。

在刚开始学习Vue的时候,我一直搞不懂render函数中h的使用方式。如果你也是一直通过HTML模板语法来搭建页面结构,可能也会对h函数不特别熟悉,下面可以一起学习下。

当我们创建一个组件时,一般都是通过HTML模板来描述UI部分,比如:

  • 使用HTML标签:
<template>
    <input 
      type="radio"
      :id="branch"
      :value="branch"
      name="branch"
      v-model="currentBranch">

    <label :for="branch">{{ branch }}</label>
</template>
  • 使用自定义组件标签:
<template>
   <tree-item class="item" :model="treeData" @chang="changeHandler"></tree-item>
</template>

其实这些都可以将通过JS抽象为三部分,并用对象描述:

  • 用于表示模板标签类型的type
  • 传给模板的attributeprop 和事件
  • 标签包裹的子节点children

且子节点同样可以抽象为同样的结构。

官方图片
官方图片

h函数就是做了这么一件事。给他传入typepropschildren。它返回对应的Vnode描述对象。

可不可以直接创建一个Vnode描述对象

当然可以,只不过如果涉及Vnode的描述全部自己写的话,有点太累,而且容易出错。

我们先看下Vue内部定义的Vnode对象所包含的属性:

  • __v_isVNode: *true*,内部属性,有该属性表示为Vnode
  • __v_skip: true,内部属性,表示跳过响应式转换,reactive转换时会根据此属性进行判断
  • isCompatRoot?: *true*,用于是否做了兼容处理的判断
  • type: VNodeTypes,虚拟节点的类型
  • props: (VNodeProps & ExtraProps) | *null*,虚拟节点的props
  • key: *string* | *number* | *null*,虚拟阶段的key,可用于diff
  • ref: VNodeNormalizedRef | *null*,虚拟阶段的引用
  • scopeId: *string* | *null*,仅限于SFC(单文件组件),在设置currentRenderingInstance当前渲染实例时,一期设置
  • slotScopeIds: *string*[] | *null*,仅限于单文件组件,与单文件组件的插槽有关
  • children: VNodeNormalizedChildren,子节点
  • component: ComponentInternalInstance | null,组件实例
  • dirs: DirectiveBinding[] | null,当前Vnode绑定的指令
  • transition: TransitionHooks<HostElement> | nullTransitionHooks
  • DOM相关属性
    • el: HostNode | *null*,宿主阶段
    • anchor: HostNode | *null* // fragment anchor
    • target: HostElement | *null*teleport target 传送的目标
    • targetAnchor: HostNode | *null* // teleport target anchor
    • staticCount: *number*,包含的静态节点的数量
  • suspense 悬挂有关的属性
    • suspense: SuspenseBoundary | *null*

    • ssContent: VNode | *null*

    • ssFallback: VNode | *null*

  • optimization only 用于优化的属性
    • shapeFlag: *number*
    • patchFlag: *number*
    • dynamicProps: *string*[] | *null*
    • dynamicChildren: VNode[] | *null*
  • 根节点会有的属性
    • appContext: AppContext | *null*,实例上下文

可以看到在Vue内部,对于一个Vnode描述对象的属性大概有二十多个,有些属性还必须经过规范梳理。

Vue为了给用于减轻一定的负担,但又不至于太封闭,就创建了渲染h。可以在用户需要的时候,通过h函数创建对应的Vnode即可。

这样就给为一些高阶玩家保留了自由发挥的空间。

那为什么要使用h函数呢?

其实官方文档已经给出了一个非常贴切又简单的实例,👉传送门:渲染函数

通过官方示例,可以知道。

javascript相较于模板语法,有更高的自由度。当使用模板太过臃肿的时候,比如多个if/else,就可以使用渲染函数h

如何用

v-if

<span v-if="user">
   {{user.name}}
</span>
<p v-else>Plase login.</p>

使h函数表述如下:

render() {
  return this.user ? h('span'null, user.name) : h('p''Plase login.')
}

从上面代码可以知道:

  • 可以通过三元运算符代替v-if/v-else指令
  • 或者通过if/else代替v-if/v-else指令

v-show

<div v-show="isActive">Content</div>

使h函数表述如下:

render() {
    return h("div", {
      "directives": [{
        name"show",
        value: isActive
      }], 
    }, "Content");
}

v-for

<ul>
  <li v-for="item in items">{{ item.name }}</li>
</ul>

使h函数表述如下:

render() {
    return h('ul'this.items.map((item) => {
      return h('li', item.name)
    }))
}
  • 可以通过map函数代替v-for指令
  • 通过map返回的Vnode,每一个都是不同的对象

v-on

<button @click="onClick">Button</button>

使h函数表述如下:

render() {
    return h('button',  {
  onClick: onClick
 })
}

对于input标签可以通过

  • onBlur监听失去焦点事件

  • onFocus监听焦点事件

  • onInput监听输入事件

  • onClick监听点击事件

  • onKeypress监听键盘事件

v-model

Vue中,我们可以通过v-bind由上向下传值。

也可以通过v-model由上向下传值。

当使用v-model时,其本质时v-bindv-on的语法糖;

在h函数中,如何表示v-model?我们看下代码:

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return h(Component, {
    modelValuethis.modelValue,
    'onUpdate:modelValue'value => this.$emit('update:modelValue', value)
  })
}

上面的代码是一个官方示例。这里表示的是:

  • 但使用v-model绑定value时。必须给子组件props中绑定一个value,及一个监听更新的函数,来代替v-bindv-on

attrs

在英文中propsattrs都代表属性的含义,但在Vue中这两个属性含义却不相同:

  • props表示元素对象的属性
  • attrs表示元素标签的属性

比如当我们调用h函数创建Vnode时,传递的第二个参数,就是Vnode对象的属性。

而当我们需要给元素标签设置attrs时该如何做呢?

<input type="button" disabled="true"/>

使h函数表述如下:

render() {
    return h(input, {
     "attrs": {
         type: button,
         disabledtrue
     }
 })
}

由此在h函数中可见props包含attrs

v-slot

Vueslot为模板提供了内容分发能力。

在使用时,只需要使用slot标签进行占位就可以。

下面看下如何使用h函数创建插槽。

<div><slot></slot></div>

使h函数表述如下:

普通插槽

可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:

render() {
  return h('div', {}, this.$slots.default())
}

作用域插槽:

<!--定义插槽组件child-->
<div><slot :text="message"></slot></div>
<!--使用组件child-->
<div><child v-slot:default="slotProps">{{ slotProps.text }}</child></div>
render() {
  return h('div', {}, this.$slots.default({
    textthis.message
  }))
}
  • 可以通过this.$slot访问静态插槽的内容
  • 如果需要传递状态,可以给this.$slots.default()函数传递一个对象参数

自定义组件

<div><child v-slot:default="slotProps"><span>{{ slotProps.text }}</span></child></div>
const { h, resolveComponent } = Vue

render() {
  return h('div', [
    h(
      resolveComponent('child'),
      {},
      // 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
      {
        default(props) => Vue.h('span', props.text)
      }
    )
  ])
}

resolveComponent API会返回child组件的Vnode

动态组件

<component :is="name"></component>

使h函数表述如下:

const { h, resolveDynamicComponent } = Vue
render() {
  const Component = resolveDynamicComponent(this.name)
  return h(Component)
}

使用动态组件时,可以使用resolveDynamicComponent代替is attribute,resolveDynamicComponent支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。

能给is传什么就能给resolveDynamicComponent传什么。

内置组件

Vue中, KeepAlive, Teleport, Transition, TransitionGroup等通被称为Vue内置组件,内置组件与用户自定义组件不同的是,内置组件没有进行局部或者全局注册,所以无法通过resolveComponent去访问他们。在使用h函数时,需要自信导入:

const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
  return h(Transition, { mode'out-in' }, /* ... */)
}

ref

<someComponent ref="someComponent"></someComponent>

使h函数表述如下:

render() {
  return h(someComponent, {"ref""someComponent"}))
}

自定义指令

可以使用 withDirectives 将自定义指令应用于VNode

const { h, resolveDirective, withDirectives } = Vue

// <div v-pin:top.animate="200"></div>
render () {
  const pin = resolveDirective('pin')
  return withDirectives(h('div'), [
    [pin, 200'top', { animatetrue }]
  ])
}

resolveDirective API 是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。

后面我们会对withDirectives API进行分析。

resolveComponent APIresolveDirective API原理是一样的。当我们创建一个组件的时候,给这个组件配置compontes属性、directives属性。这些属性最终都会绑定在实例上,resolve组件/指令的过程,就是通过访问当前实例的compontes/directives属性的过程。

渲染函数h()源码分析

这部分内容可能比较枯燥,主要就是分析h函数是如何创建Vnode,创建过程中会做哪些处理。

Vnode就是一个虚拟节点的普通JS对象,Vue会根据对象信息,渲染对应的节点。

Vnode类型

可以给h函数传递的Vnode节点类型:

  • string
  • VNode
  • Component
  • Text
  • Static
  • Comment
  • Fragment
  • TeleportImpl
  • SuspenseImpl

直接看下源码:

function h(type, propsOrChildren, children{
  // 根据参数长度判断是否有子节点
  const l = arguments.length
  
  if (l === 2) {
    // 👉传两个参数
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 👉propsOrChildren 是对象且不是数组时
      if (isVNode(propsOrChildren)) {
        //👉props是Vnode类型,则propsOrChildren为子节点
        return createVNode(type, null, [propsOrChildren])
      }
      // 👉props不包含子节点
      return createVNode(type, propsOrChildren)
    } else {
      // 👉省略props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    // 👉当存在2个已上的参数时
    // 👉将子节点放入children数组中
    if (l > 3) {
      children = Array.prototype.slice.call(arguments2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

通过上面代码知道,渲染函数h只是createVnode函数的语法糖。

渲染h()函数的主要职责就是通过判断参数的长度和类型,去调用createVnode函数创建Vnode

下面看下createVnode函数。

createVNode

createVnode函数位于Vue源码的runtime-corevnode.ts文件夹。

createVNode 其实还是调用的_createVNode

这里暂时不用关注vnodeArgsTransformer

export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode)

const createVNodeWithArgsTransform = (...args) => {
  return _createVNode(
    ...(vnodeArgsTransformer
      ? vnodeArgsTransformer(args, currentRenderingInstance)
      : args)
  )
}


_createVNode

  • 首先进行类型校验,如果不符合预期,在dev环境会警告,prod环境会作为注释节点类型。
  • 在判断是否已经是Vnode,是的话直接克隆节点,并对自己点进行规范梳理。
  • 如果是类组件,会获取__vccOpts
  • 做Vue2的异步或者函数组件的兼容
  • 如果props存在,会对props进行判断,并规范我们传给节点的classstyle,会将class处理为字符串,将style处理为对象
  • 创建Vnode
  • 规范梳理子节点
  • 如果构建时需要做兼容处理,则做Vue2的兼容处理,最后返回虚拟节点
function _createVNode(
  type,
  props,
  children,
  patchFlag,
  dynamicProps,
  isBlockNode = false
)
{
   
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }

  // 👉如果type是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
  }

  // 👉兼容Vue2的处理
  if (__COMPAT__) {
    type = convertLegacyComponent(type, currentRenderingInstance)
  }

  //👉 if块中主要处理 class & style 属性
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    // 如果props是响应式对象,需要通过Object.assign进行拷贝
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, props)
    }
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      // class不是字符串,需要规范为字符串
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // 将vnode类型信息转为 bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    // 省略...
    )
  }

  //👉 创建VNode的描述对象
  const vnode: VNode = {
    __v_isVNodetrue// 👉标识 该对象为虚拟节点
    __v_skiptrue// 👉标识 该对象跳过proxy
    type, // 类型
    props,
    key: props && normalizeKey(props), // 👉梳理props
    ref: props && normalizeRef(props),// 👉梳理ref
    scopeId: currentScopeId,
    slotScopeIdsnull,
    childrennull,
    componentnull,
    suspensenull,
    ssContentnull,
    ssFallbacknull,
    dirsnull,
    transitionnull,
    elnull,
    anchornull,
    targetnull,
    targetAnchornull,
    staticCount0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildrennull// 动态子节点
    appContextnull // 实例上下文
  }

  // validate key
  if (__DEV__ && vnode.key !== vnode.key) {
    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }
      
  // 👉规范子节点
  normalizeChildren(vnode, children)

  // 👉如果时suspense类型虚拟DOM,规范 suspense 子节点
  if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
    ;(type).normalize(vnode)
  }

  // 这里暂时不关注
  if (
    isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }
  // 👉兼容处理
  if (__COMPAT__) {
    convertLegacyVModelProps(vnode)
    convertLegacyRefInFor(vnode)
    defineLegacyVNodeProperties(vnode)
  }
  // 👉返回虚拟节点
  return vnode
}

通过上面的代码可以看出,_createVNode函数的主要职责:

  • 梳理规范props中的classstylechild
  • 创建Vnode的描述对象,并返回
  • Vue2做兼容处理

Object.assignProxy:https://stackoverflow.com/questions/43185453/object-assign-and-proxies

上面代码中,如果typeVnode类型,会调用cloneVNode创建克隆的节点,接下来我们看下cloneVNode函数。

cloneVNode

其实我们可以先思考一下,克隆一个Vnode,其实可以简化为克隆一棵tree,是一个递归克隆的过程。

export function cloneVNode(
  vnode,
  extraProps,
  mergeRef = false
)
{
  // This is intentionally NOT using spread or extend to avoid the runtime
  // key enumeration cost.
  const { props, ref, patchFlag, children } = vnode
  // 👉合并props
  const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
  // 👉创建Vnode克隆对象
  const cloned = {
    __v_isVNodetrue,
    __v_skiptrue,
    type: vnode.type,
    props: mergedProps,
    key: mergedProps && normalizeKey(mergedProps),
    ref:
      extraProps && extraProps.ref
          mergeRef && ref
          ? isArray(ref)
            ? ref.concat(normalizeRef(extraProps)!)
            : [ref, normalizeRef(extraProps)!]
          : normalizeRef(extraProps)
        : ref,
    scopeId: vnode.scopeId,
    slotScopeIds: vnode.slotScopeIds,
    children:
      __DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children)
        ? children.map(deepCloneVNode) // 对子节点进行深克隆
        : children,
    target: vnode.target,
    targetAnchor: vnode.targetAnchor,
    staticCount: vnode.staticCount,
    shapeFlag: vnode.shapeFlag,
    patchFlag:
      extraProps && vnode.type !== Fragment
        ? patchFlag === -1 // hoisted node
          ? PatchFlags.FULL_PROPS
          : patchFlag | PatchFlags.FULL_PROPS
        : patchFlag,
    dynamicProps: vnode.dynamicProps,
    dynamicChildren: vnode.dynamicChildren,
    appContext: vnode.appContext,
    dirs: vnode.dirs,
    transition: vnode.transition,

    component: vnode.component,
    suspense: vnode.suspense,
    ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
    ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
    el: vnode.el,
    anchor: vnode.anchor
  }
  // 兼容处理
  if (__COMPAT__) {
    defineLegacyVNodeProperties(cloned)
  }
  return cloned
}

cloneVNode主要做了这么几件事:

  • 合并props
  • 创建克隆对象
  • 对Vnode子节点进行深度克隆

deepClone

深度克隆, 如果子节点是数组类型会进行递归克隆。

function deepCloneVNode(vnode{
  const cloned = cloneVNode(vnode)
  if (isArray(vnode.children)) {
    cloned.children = vnode.children.map(deepCloneVNode)
  }
  return cloned
}

isVNode

很简单,根据创建Vnode描述对象时的私有属性判断。

export function isVNode(value{
  return value ? value.__v_isVNode === true : false
}

normalizeChildren

_createVNode中,我们知道,如果Vnode纯在子节点的时候,会执行normalizeChildren规范梳理子节点。下面看下normalizeChildren是如何做的:

export function normalizeChildren(vnode, children{
  let type = 0
  const { shapeFlag } = vnode
  // 👉下面会对children类型做判断,不同类型,不同操作
  if (children == null) {
    // 👉children 是null
    children = null
  } else if (isArray(children)) {
    // 👉children 是数组,标记type为ARRAY_CHILDREN
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'object') {
     // 👉children 是对象
    if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
      // Normalize slot to plain children for plain element and Teleport
      const slot = (children).default
      if (slot) {
        // _c marker is added by withCtx() indicating this is a compiled slot
        slot._c && (slot._d = false)
        normalizeChildren(vnode, slot())
        slot._c && (slot._d = true)
      }
      return
    } else {
      type = ShapeFlags.SLOTS_CHILDREN
      const slotFlag = (children)._
      if (!slotFlag && !(InternalObjectKey in children!)) {
         
        ;(children)._ctx = currentRenderingInstance
      } else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
        
        if (
          (currentRenderingInstance.slots)._ === SlotFlags.STABLE
        ) {
          ;(children)._ = SlotFlags.STABLE
        } else {
          ;(children)._ = SlotFlags.DYNAMIC
          vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
        }
      }
    }
  } else if (isFunction(children)) {
    // 👉children是函数
    children = { default: children, _ctx: currentRenderingInstance }
    type = ShapeFlags.SLOTS_CHILDREN
  } else {
    children = String(children)
    // force teleport children to array so it can be moved around
    if (shapeFlag & ShapeFlags.TELEPORT) {
      type = ShapeFlags.ARRAY_CHILDREN
      children = [createTextVNode(children)]
    } else {
      type = ShapeFlags.TEXT_CHILDREN
    }
  }
  vnode.children = children
  vnode.shapeFlag |= type
}

从上面代码可以看出,normalizeChildren主要对Vnode.childrentype做了规范梳理。

isClassComponent

export function isClassComponent(value{
  return isFunction(value) && '__vccOpts' in value
}

normalizeStyle

当我们给组件绑定style的时候,可能回这么写:

<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() {
  return {
    activeColor'red',
    fontSize30
  }
}

通过对象语法动态绑定style

也可能这么写:

<div :style="[baseStyles, overridingStyles]"></div>
data() {
  return {
      baseStyles: {
          activeColor'red',
       fontSize30
      },
      overridingStyles: {
       display: flex
      },
  }
}

通过数组给元素绑定多个style对象。

但是这两种写法。最终都会通过normalizeStyle函数进行规范梳理。

下面看下normalizeStyle函数:

export function normalizeStyle(value{
  if (isArray(value)) {
    const res = {}
    for (let i = 0; i < value.length; i++) {
      const item = value[i]
      const normalized = normalizeStyle(
        isString(item) ? parseStringStyle(item) : item
      )
      if (normalized) {
        for (const key in normalized) {
          res[key] = normalized[key]
        }
      }
    }
    return res
  } else if (isObject(value)) {
    return value
  }
}

normalizeStyle函数很简单,通过遍历递归将数组类型的value,规范为对象类型并返回。

normalizeClass

在我们给节点绑定类的时候,基本有三种形式:

  • 以字符串形式绑定
  • 以对象形式绑定
  • 以数组形式绑定

但最终绑定到节点上的class,都会以string处理,normalizeClass做的就是这件事。

将所有非string的形式链接为string

export function normalizeClass(value{
  let res = ''
  if (isString(value)) {
    res = value
  } else if (isArray(value)) {
    // 👉遍历递归处理
    for (let i = 0; i < value.length; i++) {
      const normalized = normalizeClass(value[i])
      if (normalized) {
        res += normalized + ' '
      }
    }
  } else if (isObject(value)) {
    // 👉将对象转化为string
    for (const name in value) {
      if (value[name]) {
        res += name + ' '
      }
    }
  }
  return res.trim()
}

normalizeClass函数思路其实与normalizeStyle相同。

Tip:这种遍历递归经常会在面试题中出现。

mergeProps

在前面的分析中,我们知道,克隆Vnode的过程中,回调用mergePropsvnode.props进行合并。并将合并后的mergedProps传给cloned Vnode

下面看下mergedProps是如何进行合并的?

export function mergeProps(...args{
  const ret = extend({}, args[0])
  for (let i = 1; i < args.length; i++) {
    const toMerge = args[i]
    for (const key in toMerge) {
      if (key === 'class') {
        // 👉merge Class
        if (ret.class !== toMerge.class) {
          ret.class = normalizeClass([ret.class, toMerge.class])
        }
      } else if (key === 'style') {
        // 👉merge Style
        ret.style = normalizeStyle([ret.style, toMerge.style])
      } else if (isOn(key)) {
        // 👉merge 监听的事件
        const existing = ret[key]
        const incoming = toMerge[key]
        if (existing !== incoming) {
          ret[key] = existing
            ? [].concat(existing, incoming)
            : incoming
        }
      } else if (key !== '') {
        ret[key] = toMerge[key]
      }
    }
  }
  return ret
}

  • 会对节点的classstyle、绑定的事件及非空属性进行合并
  • 合并的过程会对classstylenormalize处理
  • 如果绑定多个事件,会将所有事件存储在数组中

withDirectives

function withDirectives(vnode, directives{
  const internalInstance = currentRenderingInstance
  if (internalInstance === null) {
    __DEV__ && warn(`withDirectives can only be used inside render functions.`)
    return vnode
  }
  const instance = internalInstance.proxy
  const bindings = vnode.dirs || (vnode.dirs = [])
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers] = directives[i]
    if (isFunction(dir)) {
      dir = {
        mounted: dir,
        updated: dir
      }
    }
    bindings.push({
      dir,
      instance,
      value,
      oldValue,
      arg,
      modifiers
    })
  }
  return vnode
}

通过上面代码可知,withDirectives API的思路其实很简单,就是通过遍历指令配置对象,将配置的指令pushbinding集合中。

总结

h函数其实是createVNode的语法糖,返回的就是一个Js普通对象。在createVNode API 在创建Vnode的时候,会对Vnode的props、childrenrefclassstyle等属性进行规范梳理或者合并。如果Type直接就是Vnode类型,则会返回深度克隆的Vnode对象。相较于HTML模板语法,使用h函数创建组件Vnode,更加灵活,也更抽象。

参考:

[官方文档](

剑大瑞

2021/12/16  阅读:41  主题:全栈蓝

作者介绍

剑大瑞