Vue内置指令指令本质 在 vue 中我们经常使用到 v-if、v-show、v-for、v-bind、v-on 等内置
指令本质
在 vue 中我们经常使用到 v-if
、v-show
、v-for
、v-bind
、v-on
等内置指令,有的时候就很疑惑它们内部是如何运行的?
vue 中以
v-
开头的都是语法糖,会被编译为 js 可以理解的代码。
v-bind
:会被编译到_sfc_render
函数的$setup
属性上,由于使用了响应式数据自动触发依赖收集。
示例:
<template>
<div id="app">
<div v-bind="countRef">绑定响应式数据</div>
</div>
</template>
被编译为
v-if
:经过编译后是一个三目运算符,在条件成立时采用createElementBlock
函数创建对应的vnode
,在条件不成立时采用createCommonVnode
函数创建注释节点。
示例:
<template>
<div id="app">
<div v-if="countRef">条件为真时显示</div>
<div v-else-if="countRef === 1">数量为1时显示</div>
<div v-else>条件为假时显示</div>
</div>
</template>
被编译为:
v-on
:事件会直接加在元素上面,如果使用了修饰符则使用withModifiers
函数处理修饰符后再根据情况运行事件处理函数。
示例:
<div id="app">
<div v-on:click.stop="handleClick">点击我</div>
</div>
被编译为:
注意:事件会被编译后会当作属性的一员。
withModifiers
函数内部代码:
const systemModifiers = ["ctrl", "shift", "alt", "meta"];
const modifierGuards = {
stop: (e) => e.stopPropagation(),
prevent: (e) => e.preventDefault(),
self: (e) => e.target !== e.currentTarget,
ctrl: (e) => !e.ctrlKey,
shift: (e) => !e.shiftKey,
alt: (e) => !e.altKey,
meta: (e) => !e.metaKey,
left: (e) => "button" in e && e.button !== 0,
middle: (e) => "button" in e && e.button !== 1,
right: (e) => "button" in e && e.button !== 2,
exact: (e, modifiers) => systemModifiers.some((m) => e[`${m}Key`] && !modifiers.includes(m)) // 它使得事件监听器只响应完全匹配的键盘事件
};
const withModifiers = (fn, modifiers) => {
const cache = fn._withMods || (fn._withMods = {});
const cacheKey = modifiers.join("."); // 将数组每一项通过.连接起来转为字符串。
return cache[cacheKey] || (cache[cacheKey] = (event, ...args) => {
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]]; // 找到修饰符对应的处理函数。
if (guard && guard(event, modifiers)) return; // 执行修饰符处理函数根据返回值决定是否执行事件处理函数。
}
return fn(event, ...args);
});
};
v-for
:经过编译后是一个renderList
函数内部采用createElementBlock
函数创建对应的vnode
。
示例:
<div id="app">
<div v-for="item in dataList" :key="item.key">
<p>{{item.value}}</p>
</div>
</div>
被编译为:
v-show
:实际上就是内置的自定义指令,在beforMount
时获取元素的display
属性并保存起来,在update
时根据指令值判断是否将display
设为none
。
示例:
<div id="app">
<div v-show="isShow">控制css样式来进行显隐</div>
</div>
被编译为:
VShow
内部代码:
const vShow = {
beforeMount(el, { value }, { transition }) {
el[vShowOriginalDisplay] = el.style.display === "none" ? "" : el.style.display;
if (transition && value) { // 这是和 transition 组件相关的代码。
transition.beforeEnter(el);
} else {
setDisplay(el, value); // 这个函数根据 value 是否是虚值去设置 display 属性是否为 none。
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el);
}
},
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) return;
if (transition) {
if (value) {
transition.beforeEnter(el);
setDisplay(el, true);
transition.enter(el);
} else {
transition.leave(el, () => {
setDisplay(el, false);
});
}
} else {
setDisplay(el, value);
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value);
}
};
{
vShow.name = "show";
}
function setDisplay(el, value) {
el.style.display = value ? el[vShowOriginalDisplay] : "none";
el[vShowHidden] = !value;
}
自定义指令是如何运行的
通常我们写自定义指令都全局指令,例:
const app = createApp(App);
app.directive('test',{
created(el,binding){}, // 在指令第一次绑定到元素时调用,此时可以访问到指令绑定的值,但元素尚未渲染到 DOM 中。
beforeMount(el,binding){}, // 在绑定元素插入 DOM 前被调用。
mounted(el,binding){}, // 在绑定元素被插入 DOM 后调用。
beforeUpdate(el,binding){}, // 在绑定元素的组件更新之前被调用。
updated(el,binding){}, // 在绑定元素的组件更新之后被调用。
beforeUnmount(el,binding){}, // 在指令与元素解绑之前被调用。
unmounted(el,binding){} // 在指令与元素解绑后被调用。
});
app.mount('#app');
在模板中使用 v-test
时,会使用 withDirectives
函数将配置项添加到 vnode.dirs
数组中,在 vnode
渲染的各个阶段取出对应的配置项去执行。
function withDirectives(vnode, directives) {
// ...
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i];
// 下面以 v-on:click.once.stop="say('hello')" 为例子。
vnode.dirs.push({
dir, // dir表示指令的配置项,例:{created,beforeMount ...}
value, // 指令值
oldValue: void 0,
arg, // 表示参数,只能是字符串,例子中 click 就是字符串类型的参数。
modifiers // 表示修饰符,例子中.once.stop被编译为修饰符,{once: true,stop: true}。
});
}
}
插槽原理
插槽传递
在向组件传递插槽的时候实际上传递的是一个对象,这个对象的 key
是插槽名,value
是一个函数会返回在插槽中定义的虚拟节点。
<Comp>
<p>我是默认插槽</p>
<template #name>
<p>我是具名插槽</p>
</template>
<template #dynamic="{msg}">
<p>我是作用域插槽 {{msg}}</p>
</template>
</Comp>
<!-- 这里传递的插槽会被转为对象 -->
<script>
{
default: function(...args){},
name: function(...args){},
dynamic: function(...args){},
}
</script>
插槽使用
在使用插槽的时候本质上就是在调用函数,将传递过来的对象里面的函数逐个调用得到的 vnode
放入 createElemntVnode
函数即可渲染出页面。
示例:
<template>
<div id="app">
<Test>
<p>我是默认插槽</p>
<template #name>
<p>我是具名插槽</p>
</template>
<template #dynamic="{msg}">
<p>我是作用域插槽 {{msg}}</p>
</template>
</Test>
</div>
</template>
被编译为:
简化:
export default {
setup(_,{slots}){
const default = slots.default();
const name = slots.name();
const dynamic = slots.dynamic({msg: '这是消息'});
return () => {
return createEleemntVnode('div','',[...default,...name,...dynamic]);
}
}
}
转载自:https://juejin.cn/post/7425465315972317184