前言
指令除了可以在普通元素上使用,也可以在组件上使用,作用在组件上,其实本质是subTree vnode 会继承 components vnode的指令dirs, 这样普通subTree vnode有了dirs属性,在普通subTree vnode安装,更新,卸载的时候 便会执行相应的钩子,这在Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?已经解释过了。
在组件上指令的使用
要学会看懂单侧,vue3和element-plus都有单侧,单侧我觉得是最好的学习文档。
下面看下本次用来解释原理的单侧(该单侧代码原地址 )
it('should work on component vnode', async () => {
const count = ref(0)
function assertBindings(binding: DirectiveBinding) {
expect(binding.value).toBe(count.value)
expect(binding.arg).toBe('foo')
expect(binding.instance).toBe(_instance && _instance.proxy)
expect(binding.modifiers && binding.modifiers.ok).toBe(true)
}
const beforeMount = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should not be inserted yet
expect(el.parentNode).toBe(null)
expect(root.children.length).toBe(0)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const mounted = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should be inserted now
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const beforeUpdate = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
// node should not have been updated yet
// expect(el.children[0].text).toBe(`${count.value - 1}`)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode!.type).toBe(_prevVnode!.type)
}) as DirectiveHook)
const updated = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
// node should have been updated
expect(el.children[0].text).toBe(`${count.value}`)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode!.type).toBe(_prevVnode!.type)
}) as DirectiveHook)
const beforeUnmount = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should be removed now
expect(el.parentNode).toBe(root)
expect(root.children[0]).toBe(el)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const unmounted = jest.fn(((el, binding, vnode, prevVNode) => {
expect(el.tag).toBe('div')
// should have been removed
expect(el.parentNode).toBe(null)
expect(root.children.length).toBe(0)
assertBindings(binding)
expect(vnode.type).toBe(_vnode!.type)
expect(prevVNode).toBe(null)
}) as DirectiveHook)
const dir = {
beforeMount,
mounted,
beforeUpdate,
updated,
beforeUnmount,
unmounted
}
let _instance: ComponentInternalInstance | null = null
let _vnode: VNode | null = null
let _prevVnode: VNode | null = null
const Child = (props: { count: number }) => {
_prevVnode = _vnode
_vnode = h('div', props.count)
return _vnode
}
const Comp = {
setup() {
_instance = currentInstance
},
render() {
return withDirectives(h(Child, { count: count.value }), [
[
dir,
// value
count.value,
// argument
'foo',
// modifiers
{ ok: true }
]
])
}
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(beforeMount).toHaveBeenCalledTimes(1)
expect(mounted).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(beforeUpdate).toHaveBeenCalledTimes(1)
expect(updated).toHaveBeenCalledTimes(1)
render(null, root)
expect(beforeUnmount).toHaveBeenCalledTimes(1)
expect(unmounted).toHaveBeenCalledTimes(1)
})
该单侧和 '在普通vnode上绑定指令,指令是如何工作的?'一文中的单侧差不多。
单侧意图告诉我们:
- render(h(Comp), root) 安装组件时,会分别执行 beforeMount mounted钩子函数
- count.value++ 组件更新时,会分别执行 beforeUpdate updated钩子函数
- render(null, root) 组件卸载时 会分别执行 beforeUnmount unmounted钩子函数
本篇文章的目的就是要探索在源码内部何时执行这些钩子的,知道了内部实现,以后使用指令时会更加得心应手.
考虑到部分同学,对render函数比较陌生,我也对该单侧进行了 template 的改写,这样就能直观地看懂该单侧了.
<script>
import { ref, render, h } from 'vue'
const count = ref(0)
const root = document.getElementById('app')
const Child = {
props: {
type: Number,
default: 0
},
template: `
<div>{{ count }}</div>
`
}
const Comp = {
directives: {
xxx: {
beforeMount,
mounted,
beforeUpdate,
updated,
beforeUnmount,
unmounted
}
},
template: `
<Child v-xxx:foo.ok="count" />
`
}
render(h(Comp), root)
</script>
这么改写完,就没有理由看不懂了吧.
嗯,那就进入正题,看看源码内部究竟何时调用这些钩子呢?
内部实现
初始化
render(h(Comp), root)
- render函数 -> patch函数 -> processComponent函数[处理Comp组件] -> mountComponent函数[安装Comp组件] -> instance.update函数【拿到subTree Child vnode节点 -> patch函数 -> processComponent函数[处理Child组件] -> mountComponent函数[安装Child组件] -> instance.update函数[拿到subTree div vnode节点,此时div vnode继承了 Child vnode的dirs] -> 安装完subTree div vnode节点[subTree div vnode el 赋值给 subTree Child vnode el上]] -> 安装完subTree Child vnode节点[subTree Child vnode el赋值给comp vnode el] ] 】-> Comp vnode安装完[comp vnode el挂载到root下]
- 组件vnode节点都会安装他的subTree vnode
- 普通元素vnode children属性可能有值,有值都会递归地安装他的孩子vnode
- 所以上面调用栈就很长了
- 关于组件如何patch到浏览器,后面会单独写一篇文章介绍[从模板 -> render -> vnode -> diff -> 真实dom[el创建、属性、事件如何添加到el上]]
- 这里重点说下获取组件下subTree的方法,该方法内部会使subTree vnode 继承 组件vnode 的dirs
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx
} = instance
let result
currentRenderingInstance = instance
if (__DEV__) {
accessedAttrs = false
}
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {
// functional
const render = Component as FunctionalComponent
// in dev, mark attrs accessed if optional props (attrs === props)
if (__DEV__ && attrs === props) {
markAttrsAccessed()
}
result = normalizeVNode(
render.length > 1
? render(
props,
__DEV__
? {
get attrs() {
markAttrsAccessed()
return attrs
},
slots,
emit
}
: { attrs, slots, emit }
)
: render(props, null as any /* we know it doesn't need it */)
)
fallthroughAttrs = Component.props
? attrs
: getFunctionalFallthrough(attrs)
}
// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
let setRoot: ((root: VNode) => void) | undefined = undefined
if (
__DEV__ &&
result.patchFlag > 0 &&
result.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
) {
;[root, setRoot] = getChildRoot(result)
}
if (Component.inheritAttrs !== false && fallthroughAttrs) {
const keys = Object.keys(fallthroughAttrs)
const { shapeFlag } = root
if (keys.length) {
if (
shapeFlag & ShapeFlags.ELEMENT ||
shapeFlag & ShapeFlags.COMPONENT
) {
if (propsOptions && keys.some(isModelListener)) {
// If a v-model listener (onUpdate:xxx) has a corresponding declared
// prop, it indicates this component expects to handle v-model and
// it should not fallthrough.
// related: #1543, #1643, #1989
fallthroughAttrs = filterModelListeners(
fallthroughAttrs,
propsOptions
)
}
root = cloneVNode(root, fallthroughAttrs)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
const allAttrs = Object.keys(attrs)
const eventAttrs: string[] = []
const extraAttrs: string[] = []
for (let i = 0, l = allAttrs.length; i < l; i++) {
const key = allAttrs[i]
if (isOn(key)) {
// ignore v-model handlers when they fail to fallthrough
if (!isModelListener(key)) {
// remove `on`, lowercase first letter to reflect event casing
// accurately
eventAttrs.push(key[2].toLowerCase() + key.slice(3))
}
} else {
extraAttrs.push(key)
}
}
if (extraAttrs.length) {
warn(
`Extraneous non-props attributes (` +
`${extraAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes.`
)
}
if (eventAttrs.length) {
warn(
`Extraneous non-emits event listeners (` +
`${eventAttrs.join(', ')}) ` +
`were passed to component but could not be automatically inherited ` +
`because component renders fragment or text root nodes. ` +
`If the listener is intended to be a component custom event listener only, ` +
`declare it using the "emits" option.`
)
}
}
}
}
// inherit directives
if (vnode.dirs) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Runtime directive used on component with non-element root node. ` +
`The directives will not function as intended.`
)
}
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
}
// inherit transition data
if (vnode.transition) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Component inside <Transition> renders non-element root node ` +
`that cannot be animated.`
)
}
root.transition = vnode.transition
}
if (__DEV__ && setRoot) {
setRoot(root)
} else {
result = root
}
} catch (err) {
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
result = createVNode(Comment)
}
currentRenderingInstance = null
return result
}
const subTree = (instance.subTree = renderComponentRoot(instance))
该函数很长,但是我们也没有必要全部看完,只要知道该函数的传参是当前组件的实例,返回值是组件render函数返回的vnode即可
// inherit directives
if (vnode.dirs) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Runtime directive used on component with non-element root node. ` +
`The directives will not function as intended.`
)
}
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
}
对于本单侧,child组件的vnode是有dirs,但是他模板的div vnode是没有dirs值的
上面这段代码就是把child vnode 的dirs给了 div vnode dirs
3.普通 subTree vnode继承了组件vnode的dirs值,那普通 subTree vnode安装、更新、卸载时,何时调用那些钩子我就不重复讲解了。
不了解的可以查看Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?
更新
- 更新时执行 instance.update内部会调用 const nextTree = renderComponentRoot(instance)
- nextTree vnode也继承了 component vnode dirs值
- 新老 nextTree vnode 节点 和 prevTree vnode 进行 patch
- 最终还是回到 普通vnode 的diff,可以参考Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?中的组件更新
卸载
可以参考Vue3 疑问系列(1) — 在普通vnode上绑定指令,指令是如何工作的?中的组件卸载
总结
指令用在普通vnode和组件vnode上,是如何工作的,都讲完了.
组件vnode上指令工作的原理就是subTree vnode 继承了component vnode dirs的值,其他就和普通vnodes上指令工作的原理一样了.
下篇: Vue3疑问系列(3) — v-show指令是如何工作的?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!