Vue3读源码系列(十):自定义指令、toRefs和customRef

325 阅读4分钟

自定义指令

我们首先看指令在render函数中的表现形式

<input v-focus />
import { resolveDirective as _resolveDirective, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _directive_focus = _resolveDirective("focus")

  return _withDirectives((_openBlock(), _createElementBlock("input", null, null, 512 /* NEED_PATCH */)), [
    [_directive_focus]
  ])
}

可以看到通过resolveDirective获取到对应的指令,然后使用withDirectives将拿到的指令加到相应元素的vnode上。

resolveDirective

// packages/runtime-core/src/helpers/resolveAssets.ts
export function resolveDirective(name: string): Directive | undefined {
  return resolveAsset(DIRECTIVES, name)
}
// 用来获取components/directives/filters
function resolveAsset(
  type: AssetTypes, // "components" | "directives" | "filters"
  name: string,
  warnMissing = true,
  maybeSelfReference = false
) {
  // 获取当前实例
  const instance = currentRenderingInstance || currentInstance
  if (instance) {
    // 组件对象
    const Component = instance.type

    // explicit self name has highest priority
    // 获取components选项
    if (type === COMPONENTS) {
      const selfName = getComponentName(
        Component,
        false /* do not include inferred name to avoid breaking existing code */
      )
      if (
        selfName &&
        (selfName === name ||
          selfName === camelize(name) ||
          selfName === capitalize(camelize(name)))
      ) {
        return Component
      }
    }

    const res =
      // local registration
      // check instance[type] first which is resolved for options API
      // 优先从组件实例上获取 如果没有则从appContext上获取
      // 组件实例上在处理directives选项时挂载了directives属性
      resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
      // global registration
      resolve(instance.appContext[type], name)

    if (!res && maybeSelfReference) {
      // fallback to implicit self-reference
      return Component
    }
    ...
    return res
  } else if (__DEV__) {
    ...
  }
}
function resolve(registry: Record<string, any> | undefined, name: string) {
  return (
    registry &&
    (registry[name] ||
      registry[camelize(name)] ||
      registry[capitalize(camelize(name))])
  )
}

withDirectives

export function withDirectives<T extends VNode>(
  vnode: T,
  directives: DirectiveArguments
): T {
  const internalInstance = currentRenderingInstance
  if (internalInstance === null) {
    __DEV__ && warn(`withDirectives can only be used inside render functions.`)
    return vnode
  }
  // 获取当前组件实例
  const instance =
    (getExposeProxy(internalInstance) as ComponentPublicInstance) ||
    internalInstance.proxy
  // vnode.dirs赋值
  const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  // 遍历传入directives 填充vnode.dirs
  for (let i = 0; i < directives.length; i++) {
    let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    if (dir) {
      if (isFunction(dir)) {
        dir = {
          mounted: dir,
          updated: dir
        } as ObjectDirective
      }
      if (dir.deep) {
        traverse(value)
      }
      bindings.push({
        dir,
        instance,
        value,
        oldValue: void 0,
        arg,
        modifiers
      })
    }
  }
  return vnode
}

上述函数执行发生在组件render函数执行时,创建完subTree后会去进行patch操作,然后element的处理会到processElement再到初次挂载的mountElement,我们直接去看mountElement做了什么

mountElement

const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let el: RendererElement
  let vnodeHook: VNodeHook | undefined | null
  // dirs 是自定义指令
  const { type, props, shapeFlag, transition, dirs } = vnode
  // 创建el
  el = vnode.el = hostCreateElement(
    vnode.type as string,
    isSVG,
    props && props.is,
    props
  )

  // mount children first, since some props may rely on child content
  // 先挂载children 因为有些props可能依赖child内容
  // being already rendered, e.g. `<select value>`
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 子节点只有文本 node.textContent = 'xxx'
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 处理children
    mountChildren(
      vnode.children as VNodeArrayChildren,
      el,
      null,
      parentComponent,
      parentSuspense,
      isSVG && type !== 'foreignObject',
      slotScopeIds,
      optimized
    )
  }
  // 处理自定义指令 执行created钩子
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'created')
  }
  // scopeId
  setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
  // props
  if (props) {
    for (const key in props) {
      if (key !== 'value' && !isReservedProp(key)) {
        hostPatchProp(
          el,
          key,
          null,
          props[key],
          isSVG,
          vnode.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }
    /**
     * Special case for setting value on DOM elements:
     * - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
     * - it needs to be forced (#1471)
     * #2353 proposes adding another renderer option to configure this, but
     * the properties affects are so finite it is worth special casing it
     * here to reduce the complexity. (Special casing it also should not
     * affect non-DOM renderers)
     */
    if ('value' in props) {
      hostPatchProp(el, 'value', null, props.value)
    }
    if ((vnodeHook = props.onVnodeBeforeMount)) {
      invokeVNodeHook(vnodeHook, parentComponent, vnode)
    }
  }

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    Object.defineProperty(el, '__vnode', {
      value: vnode,
      enumerable: false
    })
    Object.defineProperty(el, '__vueParentComponent', {
      value: parentComponent,
      enumerable: false
    })
  }
  // 处理自定义指令 执行beforeMount钩子
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  }
  // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
  // #1689 For inside suspense + suspense resolved case, just call it
  const needCallTransitionHooks =
    (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
    transition &&
    !transition.persisted
  if (needCallTransitionHooks) {
    transition!.beforeEnter(el)
  }
  // el插入container
  hostInsert(el, container, anchor)
  if (
    (vnodeHook = props && props.onVnodeMounted) ||
    needCallTransitionHooks ||
    dirs
  ) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
      needCallTransitionHooks && transition!.enter(el)
      // 处理自定义指令 执行mounted钩子
      dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
    }, parentSuspense)
  }
}

可以看到在元素生命周期的不同阶段,对应的自定义指令的钩子被拿出来执行,这其实就是自定义指令的运行原理。

toRefs

toRefs是为了防止解构reactive对象导致响应式丢失,它的实现非常简单,就是多套了一层对象,当我们访问toRefsObj[key].value的时候其实还是访问reactiveObj[key],我们看源码是怎么做的

// packages/reactivity/src/ref.ts
export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = isArray(object) ? new Array(object.length) : {}
  // 循环调用 toRef
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  // 返回ret 注意这是个普通对象
  return ret
}

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  return isRef(val) // 由于reactive的get handler会判断isRef并取值.value 所以此处会执行new操作
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true
  constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K]
  ) {}
  // get value取值 直接取this._object[this._key] 还是直接访问reactiveObj[key]
  get value() {
    const val = this._object[this._key]
    return val === undefined ? (this._defaultValue as T[K]) : val
  }
  // set value同理 还是直接操作reactiveObj
  set value(newVal) {
    this._object[this._key] = newVal
  }

  get dep(): Dep | undefined {
    return getDepFromReactive(toRaw(this._object), this._key)
  }
}

customRef

customRef把track和trigger暴露给用户,让用户来决定什么时候收集和执行依赖,我们来看具体实现:

export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
  // 返回CustomRefImpl实例 传入用户传入工厂函数
  return new CustomRefImpl(factory) as any
}

class CustomRefImpl<T> {
  public dep?: Dep = undefined

  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']

  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    // 传入track和trigger
    const { get, set } = factory(
      () => trackRefValue(this),
      () => triggerRefValue(this)
    )
    // 存储用户返回的get set
    this._get = get
    this._set = set
  }

  get value() {
    // 使用用户的get
    return this._get()
  }

  set value(newVal) {
    // 使用用户的set
    this._set(newVal)
  }
}

其实跟ref的区别就是把track和trigger调用时机交给用户控制