写点什么

Vue3 中的 v-bind 指令:你不知道的那些工作原理

  • 2024-06-25
    福建
  • 本文字数:5530 字

    阅读完需:约 18 分钟

前言


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


看个 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 属性了。


文章转载自:前端欧阳

原文链接:https://www.cnblogs.com/heavenYJJ/p/18263874

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Vue3 中的 v-bind 指令:你不知道的那些工作原理_JavaScript_快乐非自愿限量之名_InfoQ写作社区