自定义指令
我们首先看指令在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调用时机交给用户控制