Vue3 中的 v-bind 指令:你不知道的那些工作原理
前言
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。代码如下:
上面的代码很简单,使用三种写法将 title 变量绑定到 div 标签的 title 属性上。
我们从浏览器中来看看编译后的代码,如下:
从上面的 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
函数代码如下:
我们先来看看第一个参数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
函数,在我们这个场景中简化后的代码如下:
由于我们在调用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-bind
、v-cloak
、v-html
、v-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 节点,代码如下:
其中createObjectExpression
函数的代码也很简单,代码如下:
上面的代码很简单,properties
数组就是 node 节点上的 props 数组,根据properties
数组生成 props 属性对应的 node 节点。
我们在 debug 终端来看看最终生成的 props 对象propsExpression
是什么样的,如下图:
从上图中可以看到此时properties
属性数组中已经没有了 v-bind 指令了,取而代之的是key
和value
属性。key.content
的值为title
,说明属性名为title
。value.content
的值为$setup.title
,说明属性值为变量$setup.title
。
到这里 v-bind 指令已经被完全解析了,生成的 props 对象中有key
和value
字段,分别代表的是属性名和属性值。后续生成 render 函数时只需要遍历所有的 props,根据key
和value
字段进行字符串拼接就可以给 div 标签生成 title 属性了。
接下来我们继续来看看处理v-bind
指令的 transform 转换函数具体是如何处理的。
transformBind
函数
将断点走进transformBind
函数,在我们这个场景中简化后的代码如下:
我们先来看看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)
说明将值也一起省略了,是第三种写法。就会执行如下代码:
这里的arg.content
就是属性名title
,执行camelize
函数将其从 kebab-case 命名法转换为驼峰命名法。比如我们给 div 上面绑一个自定义属性data-type
,采用第三种缩写模式就是这样的:<div :data-type>
。大家都知道变量名称是不能带短横线的,所以这里的要执行camelize
函数将其转换为驼峰命名法:改为绑定dataType
变量。
从前面的那几张 dir 变量的图我们知道 dir.exp
变量的值是一个对象,所以这里需要执行createSimpleExpression
函数将省略的变量值也补全。createSimpleExpression
的函数代码如下:
经过这一步处理后 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 的代码:
这里的arg
就是 v-bind 绑定的属性名,exp
就是 v-bind 绑定的属性值。createObjectProperty
函数代码如下:
经过createObjectProperty
函数的处理就会生成包含key
、value
属性的对象。key
中存的是绑定的属性名,value
中存的是绑定的属性值。
其实transformBind
函数中做的事情很简单,解析出 v-bind 指令绑定的属性名称和属性值。如果发现 v-bind 指令没有绑定值,那么就说明当前 v-bind 将值也给省略掉了,绑定的属性和属性值同名才能这样写。然后根据属性名和属性值生成一个包含key
、value
键的 props 对象。后续生成 render 函数时只需要遍历所有的 props,根据key
和value
字段进行字符串拼接就可以给 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
转换函数的最后会根据属性名和属性值生成一个包含key
、value
键的 props 对象。key
对应的就是属性名,value
对应的就是属性值。后续生成 render 函数时只需要遍历所有的 props,根据key
和value
字段进行字符串拼接就可以给 div 标签生成 title 属性了。
文章转载自:前端欧阳
评论