内置组件
问题:内置组件为什么不需要引入?
答:内置组件默认是全局引入的。
<Teleport>
定义
/*** Teleport 组件定义*/
const Teleport = {__isTeleport: true,// 组件创建和更新process(nl, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals) {if (n1 == null) {// 创建逻辑} else {// 更新逻辑}},// 组件删除remove(vnode, { r: remove, o: { remove: hostRemove } }) {// 删除逻辑},move: moveTeleport,hydrate: hydrateTeleport
}
创建、更新
/*** 组件创建、更新*/
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {// 如果存在新旧节点,且新旧节点类型不同,则销毁旧节点if (n1 && !isSameVNodeType(nl, n2)) { }// 不同节点采用对应的处理方式const { type, shapeFlag } = n2switch (type) {// 处理文本节点case Text:break// 处理注释节点case Comment:break// 处理静态节点case Static:break// 处理 Fragment 元素case Fragment:breakdefault:// 处理普通 DOM 元素if (shapeFlag & 1/* ELEMENT */) { }// 处理组件else if (shapeFlag & 6/* COMPONENT*/) { }// 处理 TELEPORTelse if (shapeFlag & 64/* TELEPORT */) {type.process(nl, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);}// 处理 SUSPENSEelse if (shapeFlag & 128/* SUSPENSE */) { }}
}/*** Teleport 组件创建、更新*/
function process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals) {const { mc: mountChildren, pc: patchChildren, pbc: patchBlockChildren, o: { insert, querySelector, createText, createComment } } = internalsconst disabled = isTeleportDisabled(n2.props)const { shapeFlag, children } = n2// 创建 Teleportif (n1 == null) {// 1、向主视图里插入节点(在非生产环境下插入注释节点,在生产环境下插入空白文本节点)const placeholder = (n2.el = (process.env.NODE_ENV !== 'production') ? createComment('teleport start') : createText(''))const mainAnchor = (n2.anchor = (process.env.NODE_ENV !== 'production') ? createComment('teleport end') : createText(''))insert(placeholder, container, anchor)insert(mainAnchor, container, anchor)// 2、获取目标移动的 DOM 节点const target = (n2.target = resolveTarget(n2.props, querySelector))const targetAnchor = (n2.targetAnchor = createText(''))// 存在则添加目标节点if (target) {insert(targetAnchor, target)}// 不存在则报警告else if ((process.env.NODE_ENV !== 'production')) {warn(/* ... */)}// 3、往目标节点插入 Teleport 组件的子节点const mount = (container, anchor) => {// 子节点为数组,遍历挂载子节点if (shapeFlag & 16/* ARRAY_CHILDREN */) {mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized)}}// disabled 情况就在原先的位置挂载if (disabled) {mount(container, mainAnchor)}// 挂载到 target 的位置else if (target) {mount(target, targetAnchor)}}// 更新 Teleportelse {// 1、更新子节点n2.el = nl.elconst mainAnchor = (n2.anchor = nl.anchor)const target = (n2.target = n1.target)const targetAnchor = (n2.targetAnchor = nl.targetAnchor)const wasDisabled = isTeleportDisabled(n1.props) // 之前是不是 disabled 状态const currentContainer = wasDisabled ? container : targetconst currentAnchor = wasDisabled ? mainAnchor : targetAnchor// 优化更新if (n2.dynamicChildren) {patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren, currentContainer, parentComponent, parentSuspense, isSVG)if (n2.shapeFlag & 16/* ARRAY_CHILDREN */) {const oldChildren = n1.childrenconst children = n2.childrenfor (let i = 0; i < children.length; i++) {if (!children[i].el) {children[i].el = oldChildren[i].el}}}}// 组件全量比对else if (!optimized) {patchChildren(n1, n2, currentContainer, currentAnchor, parentComponent, parentSuspense, isSVG)}// 2、处理 disabled 属性以及 to 属性的变化情况// 新节点不显示,旧节点显示(把子节点移动回主容器)if (disabled) {if (!wasDisabled) {moveTeleport(n2, container, mainAnchor, internals, 1/* TOGGLE */)}}// 新节点展示else {// 目标元素改变if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {const nextTarget = (n2.target = resolveTarget(n2.props, querySelector))// 存在新的目标节点,则移动到新的目标元素if (nextTarget) {moveTeleport(n2, nextTarget, null, internals, 0/* TARGET_CHANGE */)}// 不存在新的目标节点,且在非生产环境下则报警告else if ((process.env.NODE_ENV !== 'production')) {warn(/* ... */)}}// 新节点展示,旧节点不展示(把子节点移动到目标元素位置)else if (wasDisabled) {moveTeleport(n2, target, targetAnchor, internals, 1/* ROGGLE */)}}}
}
移除
/*** Teleport 组件移除*/
function remove(vnode, { r: remove, o: { remove: hostRemove } }) {const { shapeFlag, children, anchor } = vnode// 移除主视图渲染的锚点(Teleport start 的注释节点)hostRemove(anchor)// 如果子节点为数组,则遍历 Teleport 节点进行移除if (shapeFlag & 16/* ARRAY_CHILDREN */) {for (let i = 0; i < children.length; i++) {remove(children[i])}}
}
<KeepAlive>
<KeepAlive>
实际是一个抽象节点,渲染的是它的第一个子节点。
定义
/*** KeepAlive 组件定义*/
const KeepAliveImpl = {name: `KeepAlive`,__isKeepAlive: true,inheritRef: true,// 用户自定义的配置props: {include: [String, RegExp, Array], // 包含exclude: [String, RegExp, Array], // 不包含max: [String, Number], // 最大缓存个数(默认不限制个数)},setup(props, { slots }) {const cache = new Map()const keys = new Set()let current = nullconst instance = getCurrentlnstance()const parentSuspense = instance.suspenseconst sharedContext = instance.ctxconst { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContextconst storageContainer = createElement('div')// 执行 deactivate 钩子函数sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {const instance = vnode.component// 将该节点添加到 DOM 中move(vnode, container, anchor, 0/* ENTER */, parentSuspense)patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)queuePostRenderEffect(() => {instance.isDeactivated = falseif (instance.a) {invokeArrayFns(instance.a)}// 执行组件的 onBeforeMount 钩子函数const vnodeHook = vnode.props && vnode.props.onVnodeMountedif (vnodeHook) {invokeVNodeHook(vnodeHook, instance.parent, vnode)}}, parentSuspense)}// 执行 deactivate 钩子函数sharedContext.deactivate = (vnode) => {constinstance = vnode.component// 从 DOM 中移除该节点move(vnode, storageContainer, null, 1/* LEAVE */, parentSuspense)queuePostRenderEffect(() => {if (instance.da) {invokeArrayFns(instance.da)}// 执行组件的 onBeforeUnmount 钩子函数const vnodeHook = vnode.props && vnode.props.onVnodeUnmountedif (vnodeHook) {invokeVNodeHook(vnodeHook, instance.parent, vnode)}instance.isDeactivated = true}, parentSuspense)}function unmount(vnode) {resetShapeFlag(vnode)_unmount(vnode, instance, parentSuspense)}// 删除缓存(LRU 思想)function pruneCache(filter) {cache.forEach((vnode, key) => {const name = getName(vnode.type)if (name && (!filter || !filter(name))) {pruneCacheEntry(key)}})}function pruneCacheEntry(key) {const cached = cacheget(key)if (!current || cached.type !== current.type) {unmount(cached)} else if (current) {resetShapeFlag(current)}cache.delete(key)keys.delete(key)}// 监听用户提供的配置的变化watch(() => [props.include, props.exclude], ([include, exclude]) => {include && pruneCache(name => matches(include, name))exclude && !pruneCache(name => matches(exclude, name))})let pendingCacheKey = nullconst cacheSubtree = () => {if (pendingCacheKey != null) {cache.set(pendingCacheKey, instance.subTree)}}// 在执行 onBeforeMount、onBeforeUpdate 钩子函数时执行 cacheSubtree 来缓存组件onBeforeMount(cacheSubtree)onBeforeUpdate(cacheSubtree)// 在组件卸载时遍历缓存的组件进行卸载onBeforeUnmount(() => {cache.forEach(cached => {const { subTree, suspense } = instanceif (cached.type === subTree.type) {resetShapeFlag(subTree)const da = subTree.component.dada && queuePostRenderEffect(da, suspense)return}unmount(cached)})})// 返回组件渲染函数return () => {pendingCacheKey = nullif (!slots.default) return null// 1、组件渲染// 获取子节点(<KeepAlive>包裹的组件)const children = slots.default()let vnode = children[0]// 因为 <KeepAlive> 只能有一个子组件,因此如果子组件的数量大于 1,则报警告if (children.length > 1) {if ((process.env.NODE_ENV !== 'production')) {warn(/* ... */)}current = nullreturn children} else if (!isVNode(vnode) || !(vnode.shapeFlag & 4/* STATEFUL_COMPONENT */)) {current = nullreturn vnode}const comp = vnode.typeconst name = getName(comp)const { include, exclude, max } = propsif ((include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name))) {return (current = vnode)}// 2、缓存 vnodeconst key = vnode.key == null ? comp : vnode.keyconst cachedVNode = cache.get(key) // 缓存的 vnodeif (vnode.el) {vnode = cloneVNode(vnode)}pendingCacheKey = key// 存在缓存的 vnodeif (cachedVNode) {vnode.el = cachedVNode.el // vnode 对应的 DOMvnode.component = cachedVNode.component // vnode 对应的组件实例// 更新 vnode 的 shapeFlag,避免 vnode 节点作为新节点被挂载vnode.shapeFlag |= 512/* COMPONENT_KEPT_ALIVE */// 让这个 key 始终新鲜keys.delete(key)keys.add(key)}// 不存在缓存的 vnodeelse {keys.add(key)// 删除最久不用的 key,符合 LRU 思想if (max && keys.size > parselnt(max, 10)) {pruneCacheEntry(keys.values().next().value)}}// 避免 vnode 被卸载vnode.shapeFlag |= 256/* COMPONENT_SHOULD_KEEP_ALIVE */current = vnodereturn vnode}}
}/*** 缓存子树*/
const cacheSubtree = () => {// pendingCacheKey 是在 <KeppAlive> 的 render 函数中才会被赋值,所以 <KeppAlive> 在首次进入 onBeforeMount 钩子函数时不会缓存if (pendingCacheKey != null) {cache.set(pendingCacheKey, instance.subTree)}
}
问题:
<KeepAlive>
组件是如何注册activated
、deactivated
钩子函数?答:
activated
钩子函数在组件beforeMount
钩子函数时执行;deactivated
钩子函数在组件beforeUnmount
钩子函数时执行。
创建
/*** KeepAlive 组件的创建*/
const processComponent = (nl, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {if (n1 == null) {// 处理 KeepAlive 组件if (n2.shapeFlag & 512/* COMPONENT_KEPT ALIVE */) {parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)}// 挂载组件else {mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)}}// 更新组件else { }
}/*** KeepAlive 缓存组件的创建* 实现:<KeepAlive> 包裹的子组件在其渲染后、下一次更新前会被缓存,缓存后的子组件在下一次渲染的时候直接从缓存中拿到子树 vnode 以及其对应的 DOM 元素直接渲染*/
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {const instance = vnode.componentmove(vnode, container, anchor, 0/* ENTER */, parentSuspense)patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)queuePostRenderEffect(() => {instance.isDeactivated = falseif (instance.a) {invokeArrayFns(instance.a)}const vnodeHook = vnode.props && vnode.props.onVnodeMountedif (vnodeHook) {invokeVNodeHook(vnodeHook, instance.parent, vnode)}}, parentSuspense)
}
卸载
/*** KeepAlive 组件的卸载*/
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {const { shapeFlag } = vnodeif (shapeFlag & 256/* COMPONENT_SHOULD_KEEP ALIVE */) {parentComponent.ctx.deactivate(vnode)return}// 卸载组件
}/*** KeepAlive 在卸载时执行 onBeforeUnmount 钩子函数*/
onBeforeUnmount(() => {cache.forEach(cached => {const [subTree, suspense] = instanceif (cached.type === subTree.type) {resetShapeFlag(subTree)const da = subTree.component.dada && queuePostRenderEffect(da, suspense)return}unmount(cached)})
})
<Transition>
<Transition>
渲染的是第一个子元素节点。核心思想:当
<Transition>
包裹的元素插入、删除时,在适当的时机插入这些 CSS 样式。渲染流程:
- 组件的渲染
- 钩子函数的执行
- 模式的应用
定义
/*** Transition 组件的定义*/
const Transition = (props, { slots }) => h(BaseTransition, (props), slots)/*** Transition 组件的基础配置*/
const BaseTransition = {name: `BaseTransition`,props: {mode: String,appear: Boolean,persisted: Boolean,// enteronBeforeEnter: TransitionHookValidator,onEnter: TransitionHookValidator,onAfterEnter: TransitionHookValidator,onEnterCancelled: TransitionHookValidator,// leaveonBeforeLeave: TransitionHookValidator,onLeave: TransitionHookValidator,onAfterLeave: TransitionHookValidator,onLeaveCancelled: TransitionHookValidator,// appearonBeforeAppear: TransitionHookValidator,onAppear: TransitionHookValidator,onAfterAppear: TransitionHookValidator,onAppearCancelled: TransitionHookValidator,},setup(props, { slots }) {const instance = getCurrentInstance()const state = useTransitionState()let prevTransitionKeyreturn () => {const children = slots.default && getTransitionRawChildren(slots.default(), true)if (!children || !children.length) return// Transition 组件只允许一个子元素节点,多个报警告,提示使用 TransitionGroup 组件if ((process.env.NODE_ENV !== 'production') && children.length > 1) {warn(/* ... */)}// 不需要追踪响应式,所以改成原始值,提升性能const rawProps = toRaw(props)const { mode } = rawProps// 检查 mode 是否合法if ((process.env.NODE_ENV !== 'production') && mode && !['in-out', 'out-in', 'default'].includes(mode)) {warn(/* ... */)}// 获取第一个子元素节点const child = children[0]if (state.isLeaving) return emptyPlaceholder(child)// 处理<transition><keep-alive/></transition>的情况const innerChild = getKeepAliveChild(child)if (!innerChild) {return emptyPlaceholder(child)}// 通过 resolveTransitionHooks 定义组件创建和删除阶段的钩子函数对象const enterHooks = resolveTransitionHooks(innerChild, rawProps, state, instance)// 通过 setTransitionHooks 把钩子函数对象设置到 vnode.transition 上setTransitionHooks(innerChild, enterHooks)const oldChild = instance.subTreeconst oldInnerChild = oldChild && getKeepAliveChild(oldChild)let transitionKeyChanged = falseconst { getTransitionKey } = innerChild.typeif (getTransitionKey) {const key = getTransitionKey()if (prevTransitionKey === undefined) {prevTransitionKey = key} else if (key !== prevTransitionKey) {prevTransitionKey = keytransitionKeyChanged = true}}if (oldInnerChild && oldInnerChild.type !== Comment && (!isSameVNodeType(innerChild, oldlnnerChild) || transitionKeyChanged)) {const leavingHooks = resolveTransitionHooks(oldInnerChild, rawProps, state, instance)// 更新旧树的钩子函数setTransitionHooks(oldInnerChild, leavingHooks)// 在两个视图之间切换if (mode === 'out-in') {state.isLeaving = true// 当离开过渡结束后,再重新渲染组件leavingHooks.afterLeave = () => {state.isLeaving = falseinstance.update()}// 返回空的占位符节点return emptyPlaceholder(children)} else if (mode === 'in-out') {leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => {const leavingVNodesCache = getLeavingNodesForType(state, oldInnerChild)leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild// early removal callbackel._leaveCb = () => {earlyRemove()el._leaveCb = undefineddelete enterHooks.delayedLeave}enterHooks.delayedLeave = delayedLeave}}}return child}}
}
渲染
/*** <Transition> 组件的渲染*/
function resolveTransitionHooks(vnode, props, state, instance) {const { appear, mode, persisted = false, onBeforeEnter, onEnter, onAfterEnter, onEnterCancelled, onBeforeLeave, onLeave, onAfterLeave, onLeaveCancelled, onBeforeAppear, onAppear, onAfterAppear, onAppearCancelled } = propsconst key = String(vnode.key)const leavingVNodesCache = getLeavingNodesForType(state, vnode)const callHook = (hook, args) => {hook && callWithAsyncErrorHandling(hook, instance, 9/* TRANSITION_HOOK */, args)}const hooks = {mode,persisted,// 在 patch 阶段的 mountElement 函数中,在插入元素节点前且存在过渡条件下执行beforeEnter(el) {// 根据 appear 的值和 DOM 是否挂载判断执行 onBefore 还是 onBeforeEnterlet hook = onBeforeEnterif (!state.isMounted) {if (appear) {hook = onBeforeAppear || onBeforeEnter} else return}// el._leaveCb 存在则执行 leave 钩子函数if (el._leaveCb) {el.leaveCb(true/* cancelled */)}const leavingVNode = leavingVNodesCache[key]if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el.leaveCb) {leavingVNode.el.leaveCb()}callHook(hook, [el])},enter(el) {let hook = onEnterlet afterHook = onAfterEnterlet cancelHook = onEnterCancelledif (!state.isMounted) {if (appear) {hook = onAppear || onEnterafterHook = onAfterAppear || onAfterEntercancelHook = onAppearCancelled || onEnterCancelled} else return}let called = falseconst done = (el._enterCb = (cancelled) => {if (called) returncalled = trueif (cancelled) {callHook(cancelHook, [el])} else {callHook(afterHook, [el])}if (hooks.delayedLeave) {hooks.delayedLeave()}el.enterCb = undefined})if (hook) {hook(el, done)if (hook.length <= 1) {done()}} else {done()}},leave(el, remove) {const key = String(vnode.key)if (el._enterCb) {el._enterCb(true/* cancelled */)}if (stateisUnmounting) {return remove()}callHook(onBeforeLeave, [el])let called = falseconst done = (el._leaveCb = (cancelled) => {if (called) returncalled = trueremove()if (cancelled) {callHook(onLeaveCancelled, [el])} else {callHook(onAfterLeave, [el])}el._leaveCb = undefinedif (leavingVNodesCache[key] === vnode) {delete leavingVNodesCache[key]}})leavingVNodesCache[key] = vnodeif (onLeave) {onLeave(el.done)if (onLeave.length <= 1) done()} else {done()}},clone(vnode) {return resolveTransitionHooks(vnode, props, state, instance)}}return hooks
}
钩子函数的执行
/*** 钩子函数的执行 - 对传递的 props 进行封装*/
function resolveTransitionProps(rawProps) {let {name = 'v',type, css = true,duration,enterFromClass = `${name}-enter-from`,enterActiveClass = `${name}-enter-active`,enterToClass = `${name}-enter-to`,appearFromClass = enterFromClass,appearActiveClass = enterActiveClass,appearToClass = enterToClass,leaveFromClass = `${name}-leave-from`,leaveActiveClass = `${name}-leave-active`,leaveToClass = `${name}-leave-to`} = rawPropsconst baseProps = {}for (const key in rawProps) {if (!(key in DOMTransitionPropsValidators)) {baseProps[key] = rawProps[key]}}if (!css) return basePropsconst durations = normalizeDuration(duration)const enterDuration = durations && durations[0]const leaveDuration = durations && durations[1]const {onBeforeEnter,onEnter,onEnterCancelled,onLeave,onLeaveCancelled,onBeforeAppear = onBeforeEnter,onAppear = onEnter,onAppearCancelled = onEnterCancelled} = baseProps// 进入动画结束后执行,移除 DOM 元素的 enterToClass 和 enterActiveClass,然后执行 done 函数进入 onAfterEnter 钩子函数const finishEnter = (el, isAppear, done) => {removeTransitionClass(el, isAppear ? appearToClass : enterToClass)removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)done && done()}// 离开动画结束后执行,移除 DOM 元素的 leaveToClass 和 leaveActiveClassconst finishLeave = (el, done) => {removeTransitionClass(el, leaveToClass)removeTransitionClass(el, leaveActiveClass)done && done()}// 制作 enter 钩子const makeEnterHook = (isAppear) => {return (el, done) => {const hook = isAppear ? onAppear : onEnterconst resolve = () => finishEnter(el, isAppear, done)hook && hook(el, resolve)nextFrame(() => {removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)// 添加了 enterToClass 后,浏览器就进入过渡动画了addTransitionClass(el, isAppear ? appearToClass : enterToClass)if (!(hook && hook.length > 1)) {if (enterDuration) {setTimeout(resolve, enterDuration)} else {whenTransitionEnds(el, type, resolve)}}})}}return extend(baseProps, {onBeforeEnter(el) {// 执行 onBeforeEnter 钩子函数onBeforeEnter && onBeforeEnter(el)// 给 DOM 元素添加 enterActiveClass 和 enterFromClassaddTransitionClass(el, enterActiveClass)addTransitionClass(el, enterFromClass)},onBeforeAppear(el) {onBeforeAppear && onBeforeAppear(el)addTransitionClass(el, appearActiveClass)addTransitionClass(el, appearFromClass)},onEnter: makeEnterHook(false),onAppear: makeEnterHook(true),onLeave(el, done) {const resolve = () => finishLeave(el, done)addTransitionClass(el, leaveActiveClass)addTransitionClass(el, leaveFromClass)nextFrame(() => {removeTransitionClass(el, leaveFromClass)// 当添加 leaveToClass 时,浏览器就开始执行离开过渡动画了addTransitionClass(el, leaveToClass)if (!(onLeave && onLeave.length > 1)) {if (leaveDuration) {setTimeout(resolve, leaveDuration)} else {whenTransitionEnds(el, type, resolve)}}})onLeave && onLeave(el, resolve)},onEnterCancelled(el) {finishEnter(el, false)onEnterCancelled && onEnterCancelled(el)},onAppearCancelled(el) {finishEnter(el, true)onAppearCancelled && onAppearCancelled(el)},onLeaveCancelled(el) {finishLeave(el)onLeaveCancelled && onLeaveCancelled(el)}})
}