前言

v-bind指令想必大家都不陌生,并且都知道他支持各种写法,比如<div v-bind:title="title"><div :title="title"><div :title>(vue3.4中引入的新的写法)。这三种写法的作用都是一样的,将title变量绑定到div标签的title属性上。本文将通过debug源码的方式带你搞清楚,v-bind指令是如何实现这么多种方式将title变量绑定到div标签的title属性上的。注:本文中使用的vue版本为3.4.19

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

看个demo

还是老套路,我们来写个demo。代码如下:

<template>
<div v-bind:title="title">Hello Word</div>
<div :title="title">Hello Word</div>
<div :title>Hello Word</div>
</template> <script setup lang="ts">
import { ref } from "vue";
const title = ref("Hello Word");
</script>

上面的代码很简单,使用三种写法将title变量绑定到div标签的title属性上。

我们从浏览器中来看看编译后的代码,如下:

const _sfc_main = _defineComponent({
__name: "index",
setup(__props, { expose: __expose }) {
// ...省略
}
}); function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
_Fragment,
null,
[
_createElementVNode("div", { title: $setup.title }, "Hello Word", 8, _hoisted_1),
_createElementVNode("div", { title: $setup.title }, "Hello Word", 8, _hoisted_2),
_createElementVNode("div", { title: $setup.title }, "Hello Word", 8, _hoisted_3)
],
64
/* STABLE_FRAGMENT */
);
}
_sfc_main.render = _sfc_render;
export default _sfc_main;

从上面的render函数中可以看到三种写法生成的props对象都是一样的: { title: $setup.title }。props属性的key为title,值为$setup.title变量。

再来看看浏览器渲染后的样子,如下图:

从上图中可以看到三个div标签上面都有title属性,并且属性值都是一样的。

transformElement函数

在之前的 面试官:来说说vue3是怎么处理内置的v-for、v-model等指令?文章中我们讲过了在编译阶段会执行一堆transform转换函数,用于处理vue内置的v-for等指令。而v-bind指令就是在这一堆transform转换函数中的transformElement函数中处理的。

还是一样的套路启动一个debug终端。这里以vscode举例,打开终端然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。

transformElement函数打个断点,transformElement函数的代码位置在:node_modules/@vue/compiler-core/dist/compiler-core.cjs.js

debug终端上面执行yarn dev后在浏览器中打开对应的页面,比如:http://localhost:5173/ 。此时断点就会走到transformElement函数中,在我们这个场景中简化后的transformElement函数代码如下:

const transformElement = (node, context) => {
return function postTransformElement() {
let vnodeProps;
const propsBuildResult = buildProps(
node,
context,
undefined,
isComponent,
isDynamicComponent
);
vnodeProps = propsBuildResult.props; node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren
// ...省略
);
};
};

我们先来看看第一个参数node,如下图:

从上图中可以看到此时的node节点对应的就是<div v-bind:title="title">Hello Word</div>节点,其中的props数组中只有一项,对应的就是div标签中的v-bind:title="title"部分。

我们接着来看transformElement函数中的代码,可以分为两部分。

第一部分为调用buildProps函数拿到当前node节点的props属性赋值给vnodeProps变量。

第二部分为根据当前node节点vnodeTag也就是节点的标签比如div、vnodeProps也就是节点的props属性对象、vnodeChildren也就是节点的children子节点、还有一些其他信息生成codegenNode属性。在之前的 终于搞懂了!原来 Vue 3 的 generate 是这样生成 render 函数的文章中我们已经讲过了编译阶段最终生成render函数就是读取每个node节点的codegenNode属性然后进行字符串拼接。

buildProps函数的名字我们不难猜出他的作用就是生成node节点的props属性对象,所以我们接下来需要将目光聚焦到buildProps函数中,看看是如何生成props对象的。

buildProps函数

将断点走进buildProps函数,在我们这个场景中简化后的代码如下:

function buildProps(node, context, props = node.props) {
let propsExpression;
let properties = []; for (let i = 0; i < props.length; i++) {
const prop = props[i];
const { name } = prop;
const directiveTransform = context.directiveTransforms[name];
if (directiveTransform) {
const { props } = directiveTransform(prop, node, context);
properties.push(...props);
}
} propsExpression = createObjectExpression(
dedupeProperties(properties),
elementLoc
);
return {
props: propsExpression,
// ...省略
};
}

由于我们在调用buildProps函数时传的第三个参数为undefined,所以这里的props就是默认值node.props。如下图:

从上图中可以看到props数组中只有一项,props中的name字段为bind,说明v-bind指令还未被处理掉。

并且由于我们当前node节点是第一个div标签:<div v-bind:title="title">,所以props中的rawName的值是v-bind:title

我们接着来看上面for循环遍历props的代码:const directiveTransform = context.directiveTransforms[name],现在我们已经知道了这里的name为bind。那么这里的context.directiveTransforms对象又是什么东西呢?我们在debug终端来看看context.directiveTransforms,如下图:

从上图中可以看到context.directiveTransforms对象中包含许多指令的转换函数,比如v-bindv-cloakv-htmlv-model等。

我们这里name的值为bind,并且context.directiveTransforms对象中有name为bind的转换函数。所以const directiveTransform = context.directiveTransforms[name]就是拿到处理v-bind指令的转换函数,然后赋值给本地的directiveTransform函数。

接着就是执行directiveTransform转换函数,拿到v-bind指令生成的props数组。然后执行properties.push(...props)方法将所有的props数组都收集到properties数组中。

由于node节点中有多个props,在for循环遍历props数组时,会将经过transform转换函数处理后拿到的props数组全部push到properties数组中。properties数组中可能会有重复的prop,所以需要执行dedupeProperties(properties)函数对props属性进行去重。

node节点上的props属性本身也是一种node节点,所以最后就是执行createObjectExpression函数生成props属性的node节点,代码如下:

propsExpression = createObjectExpression(
dedupeProperties(properties),
elementLoc
)

其中createObjectExpression函数的代码也很简单,代码如下:

function createObjectExpression(properties, loc) {
return {
type: NodeTypes.JS_OBJECT_EXPRESSION,
loc,
properties,
};
}

上面的代码很简单,properties数组就是node节点上的props数组,根据properties数组生成props属性对应的node节点。

我们在debug终端来看看最终生成的props对象propsExpression是什么样的,如下图:

从上图中可以看到此时properties属性数组中已经没有了v-bind指令了,取而代之的是keyvalue属性。key.content的值为title,说明属性名为titlevalue.content的值为$setup.title,说明属性值为变量$setup.title

到这里v-bind指令已经被完全解析了,生成的props对象中有keyvalue字段,分别代表的是属性名和属性值。后续生成render函数时只需要遍历所有的props,根据keyvalue字段进行字符串拼接就可以给div标签生成title属性了。

接下来我们继续来看看处理v-bind指令的transform转换函数具体是如何处理的。

transformBind函数

将断点走进transformBind函数,在我们这个场景中简化后的代码如下:

const transformBind = (dir, _node) => {
const arg = dir.arg;
let { exp } = dir; if (!exp) {
const propName = camelize(arg.content);
exp = dir.exp = createSimpleExpression(propName, false, arg.loc);
exp = dir.exp = processExpression(exp, context);
} return {
props: [createObjectProperty(arg, exp)],
};
};

我们先来看看transformBind函数接收的第一个参数dir,从这个名字我想你应该已经猜到了他里面存储的是指令相关的信息。

在debug终端来看看三种写法的dir参数有什么不同。

第一种写法:<div v-bind:title="title">dir如下图:

从上图中可以看到dir.name的值为bind,说明这个是v-bind指令。dir.rawName的值为v-bind:title说明没有使用缩写模式。dir.arg表示bind绑定的属性名称,这里绑定的是title属性。dir.exp表示bind绑定的属性值,这里绑定的是$setup.title变量。

第二种写法:<div :title="title">dir如下图:

从上图中可以看到第二种写法的dir和第一种写法的dir只有一项不一样,那就是dir.rawName。在第二种写法中dir.rawName的值为:title,说明我们这里是采用了缩写模式。

可能有的小伙伴有疑问了,这里的dir是怎么来的?vue是怎么区分第一种全写模式和第二种缩写模式呢?

答案是在parse阶段将html编译成AST抽象语法树阶段时遇到v-bind:title:title时都会将其当做v-bind指令处理,并且将解析处理的指令绑定的属性名塞到dir.arg中,将属性值塞到dir.exp中。

第三种写法:<div :title>dir如下图:

第三种写法也是缩写模式,并且将属性值也一起给省略了。所以这里的dir.exp存储的属性值为undefined。其他的和第二种缩写模式基本一样。

我们再来看transformBind中的代码,if (!exp)说明将值也一起省略了,是第三种写法。就会执行如下代码:

if (!exp) {
const propName = camelize(arg.content);
exp = dir.exp = createSimpleExpression(propName, false, arg.loc);
exp = dir.exp = processExpression(exp, context);
}

这里的arg.content就是属性名title,执行camelize函数将其从kebab-case命名法转换为驼峰命名法。比如我们给div上面绑一个自定义属性data-type,采用第三种缩写模式就是这样的:<div :data-type>。大家都知道变量名称是不能带短横线的,所以这里的要执行camelize函数将其转换为驼峰命名法:改为绑定dataType变量。

从前面的那几张dir变量的图我们知道 dir.exp变量的值是一个对象,所以这里需要执行createSimpleExpression函数将省略的变量值也补全。createSimpleExpression的函数代码如下:

function createSimpleExpression(
content,
isStatic,
loc,
constType
): SimpleExpressionNode {
return {
type: NodeTypes.SIMPLE_EXPRESSION,
loc,
content,
isStatic,
constType: isStatic ? ConstantTypes.CAN_STRINGIFY : constType,
};
}

经过这一步处理后 dir.exp变量的值如下图:

还记得前面两种模式的 dir.exp.content的值吗?他的值是$setup.title,表示属性值为setup中定义的title变量。而我们这里的dir.exp.content的值为title变量,很明显是不对的。

所以需要执行exp = dir.exp = processExpression(exp, context)dir.exp.content中的值替换为$setup.title,执行processExpression函数后的dir.exp变量的值如下图:

我们来看transformBind函数中的最后一块return的代码:

return {
props: [createObjectProperty(arg, exp)],
}

这里的arg就是v-bind绑定的属性名,exp就是v-bind绑定的属性值。createObjectProperty函数代码如下:

function createObjectProperty(key, value) {
return {
type: NodeTypes.JS_PROPERTY,
loc: locStub,
key: isString(key) ? createSimpleExpression(key, true) : key,
value,
};
}

经过createObjectProperty函数的处理就会生成包含keyvalue属性的对象。key中存的是绑定的属性名,value中存的是绑定的属性值。

其实transformBind函数中做的事情很简单,解析出v-bind指令绑定的属性名称和属性值。如果发现v-bind指令没有绑定值,那么就说明当前v-bind将值也给省略掉了,绑定的属性和属性值同名才能这样写。然后根据属性名和属性值生成一个包含keyvalue键的props对象。后续生成render函数时只需要遍历所有的props,根据keyvalue字段进行字符串拼接就可以给div标签生成title属性了。

总结

在transform阶段处理vue内置的v-for、v-model等指令时会去执行一堆transform转换函数,其中有个transformElement转换函数中会去执行buildProps函数。

buildProps函数会去遍历当前node节点的所有props数组,此时的props中还是存的是v-bind指令,每个prop中存的是v-bind指令绑定的属性名和属性值。

在for循环遍历node节点的所有props时,每次都会执行transformBind转换函数。如果我们在写v-bind时将值也给省略了,此时v-bind指令绑定的属性值就是undefined。这时就需要将省略的属性值补回来,补回来的属性值的变量名称和属性名是一样的。

transformBind转换函数的最后会根据属性名和属性值生成一个包含keyvalue键的props对象。key对应的就是属性名,value对应的就是属性值。后续生成render函数时只需要遍历所有的props,根据keyvalue字段进行字符串拼接就可以给div标签生成title属性了。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

Vue3 中的 v-bind 指令:你不知道的那些工作原理的更多相关文章

  1. [中英对照]How PCI Works | PCI工作原理

    How PCI Works | PCI工作原理 Your computer's components work together through a bus. Learn about the PCI ...

  2. v:bind指令对于传boolean值的注意之处

    1,

  3. vue3中的通过proxy实现双向数据绑定的原理

    1.什么是Proxy?它的作用是? 据阮一峰文章介绍:Proxy可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种 ...

  4. 你不知道的https工作原理

      HTTPS其实是有两部分组成:HTTP + SSL / TLS,也就是在HTTP上又加了一层处理加密信息的模块.服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据 1. ...

  5. Vue3中setup语法糖学习

    目录 1,前言 2,基本语法 2,响应式 3,组件使用 3.1,动态组件 3.2,递归组件 4,自定义指令 5,props 5.1,TypeScript支持 6,emit 6.1,TypeScript ...

  6. 计算机系统6-> 计组与体系结构3 | MIPS指令集(中)| MIPS汇编指令与机器表示

    上一篇计算机系统5-> 计组与体系结构2 | MIPS指令集(上)| 指令系统从顶层讲解了一个指令集 / 指令系统应当具备哪些特征和工作原理.这一篇就聚焦MIPS指令集(MIPS32),看看其汇 ...

  7. Vue3中插槽(slot)用法汇总

    Vue中的插槽相信使用过Vue的小伙伴或多或少的都用过,但是你是否了解它全部用法呢?本篇文章就为大家带来Vue3中插槽的全部用法来帮助大家查漏补缺. 什么是插槽 简单来说就是子组件中的提供给父组件使用 ...

  8. iOS中的预编译指令的初步探究

    目录 文件包含 #include #include_next #import 宏定义 #define #undef 条件编译 #if #else #endif #if define #ifdef #i ...

  9. 实践中的Git常用指令分析

    从工作开始,一直都在使用为知笔记(作为程序员需要知道的内容很多---不需要很深入理解,一段时不使用的东西可能就会忘记).但本周一同步不同PC端时,了解到为知会在2017/1/1开始收费! 既然收费了, ...

  10. C/C++中的预编译指令

    工作中遇到的: 一个头文件中的: #pragma warning(disable:4996)#pragma warning(disable:4244)#pragma warning(disable:4 ...

随机推荐

  1. SQL SERVER数据库存储过程加密

    CREATE PROCEDURE [dbo].[kytj_Base_Worker] WITH ENCRYPTION AS SELECT u.worker_number, u.worker_name, ...

  2. 最近常用的几个【行操作】的Pandas函数

    最近在做交易数据的统计分析时,多次用到数据行之间的一些操作,对于其中的细节,简单做了个笔记. 1. shfit函数 shift函数在策略回测代码中经常出现,计算交易信号,持仓信号以及资金曲线时都有涉及 ...

  3. Go-Zero技能提升:深度探究goctl的妙用,轻松应对微服务开发挑战!(三)

    前言 有位同学在群里说:"Go-Zero官方文档太简洁了,对小白有点不友好.好奇你们是怎么学习的?项目是怎么封装的?有什么提高开发效率的技巧吗?". 来来来,这期内容给你安排上,先 ...

  4. centos7实现多网卡多线路

    移动线路IP:179.15.5.253 网卡配置内容: TYPE=Ethernet PROXY_METHOD=none BROWSER_ONLY=no BOOTPROTO=static DEFROUT ...

  5. spring-boot集成Quartz-job存储方式二RAM

    简单区分: RAM:程序启动从数据库中读取原始job配置(也可以从配置文件中读取),job中间运行过程在RAM内存中,程序停止或重启后,RAM中数据丢失,再次启动的时候会重新读取job配置.适合于单机 ...

  6. 4G EPS 中的 Control Plane

    目录 文章目录 目录 前文列表 控制平面 归属环境部分 无线接入网络部分 核心网络 EPS CP 中的 GTP-C UP 中的 GTP-U Tunnel 两端的 F-TEID 需要通过 CP 的信令流 ...

  7. EasyUI组件新增方法与事件

    以window组件为例 事件 扩展事件直接定义在options中,可以再初始化组件时定义事件,也可以使用时临时定义事件.这里是组件初始化后在添加的. 使用情景:添加,插入功能.主界面表格分别点击添加和 ...

  8. gitlab docker 自动部署报错 /bin/bash: line 118: docker: command not found

    原因找不到docker,我们需要绑一下docker 列出所有gitlab-runner配置文件 find / | grep config.toml [root@izwz99pke7zxkpm7l51t ...

  9. TIM_Cmd()函数引发的思考

    在使用定时器的输入捕获进行频率测量时发现用TIM_Cmd()函数关闭定时器后,输入捕获中断还是会被触发,这就很奇怪了,输入捕获是定时器的一种模式,关闭定时器不就意味着输入捕获捕获也被关闭了吗?可是实际 ...

  10. itest(爱测试)开源接口测试&敏捷测试管理平台8.1.0发布

    (一)itest 简介 itest 开源敏捷测试管理,testOps 践行者,极简的任务管理,测试管理,缺陷管理,测试环境管理,接口测试,接口Mock 6合1,又有丰富的统计分析.可按测试包分配测试用 ...