掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染
qiyuwang 2024-10-10 11:34 20 浏览 0 评论
前言
这篇文章我们来讲使用了scoped后,vue是如何给html增加自定义属性data-v-x。注:本文中使用的vue版本为3.4.19,@vitejs/plugin-vue的版本为5.0.4。
看个demo
我们先来看个demo,代码如下:
<template>
<div class="block">hello world</div>
</template>
<style scoped>
.block {
color: red;
}
</style>
经过编译后,上面的demo代码就会变成下面这样:
<template>
<div data-v-c1c19b25 class="block">hello world</div>
</template>
<style>
.block[data-v-c1c19b25] {
color: red;
}
</style>
从上面的代码可以看到在div上多了一个data-v-c1c19b25自定义属性,并且css的属性选择器上面也多了一个[data-v-c1c19b25]。
接下来我将通过debug的方式带你了解,vue使用了scoped后是如何给html增加自定义属性data-v-x。
transformMain函数
transformMain 函数的作用是将vue文件转换成js文件。
首先我们需要启动一个debug终端。这里以vscode举例,打开终端然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。
接着我们需要给transformMain 函数打个断点,transformMain 函数的位置在node_modules/@vitejs/plugin-vue/dist/index.mjs。
在debug终端执行yarn dev,在浏览器中打开对应的页面,比如:http://localhost:5173/ 。此时断点将会停留在transformMain 函数中,在我们这个场景中简化后的transformMain 函数代码如下:
async function transformMain(code, filename, options) {
const { descriptor } = createDescriptor(filename, code, options);
const { code: templateCode } = await genTemplateCode(
descriptor
// ...省略
);
const { code: scriptCode } = await genScriptCode(
descriptor
// ...省略
);
const stylesCode = await genStyleCode(
descriptor
// ...省略
);
const output = [scriptCode, templateCode, stylesCode];
const attachedProps = [];
attachedProps.push([`__scopeId`, JSON.stringify(`data-v-${descriptor.id}`)]);
output.push(
`import _export_sfc from '${EXPORT_HELPER_ID}'`,
`export default /*#__PURE__*/_export_sfc(_sfc_main, [${attachedProps
.map(([key, val]) => `['${key}',${val}]`)
.join(",")}])`
);
let resolvedCode = output.join("\n");
return {
code: resolvedCode,
};
}
在debug终端来看看transformMain函数的入参code,如下图:
从上图中可以看到入参code为vue文件的code代码字符串。
reateDescriptor函数会生成一个descriptor对象。而descriptor对象的id属性descriptor.id,就是根据vue文件的路径调用node的createHash加密函数生成的,也就是html标签上的自定义属性data-v-x中的x。
genTemplateCode函数会生成编译后的render函数,如下图:
从上图中可以看到在生成的render函数中,div标签对应的是createElementBlock方法,而在执行createElementBlock方法时并没有将descriptor.id传入进去。
将genTemplateCode函数、genScriptCode函数、genStyleCode函数执行完了后,得到templateCode、scriptCode、stylesCode,分别对应的是编译后的render函数、编译后的js代码、编译后的style样式。
然后将这三个变量const output = [scriptCode, templateCode, stylesCode];收集到output数组中。
接着会执行attachedProps.push方法将一组键值对push到attachedProps数组中,key为__scopeId,值为data-v-${descriptor.id}。看到这里我想你应该已经猜到了,这里的data-v-${descriptor.id}就是给html标签上添加的自定义属性data-v-x。
接着就是遍历attachedProps数组将里面存的键值对拼接到output数组中,代码如下:
output.push(
`import _export_sfc from '${EXPORT_HELPER_ID}'`,
`export default /*#__PURE__*/_export_sfc(_sfc_main, [${attachedProps
.map(([key, val]) => `['${key}',${val}]`)
.join(",")}])`
);
最后就是执行output.join("\n"),使用换行符将output数组中的内容拼接起来就能得到vue文件编译后的js文件,如下图:
从上图中可以看到编译后的js文件export default导出的是_export_sfc函数的执行结果,该函数接收两个参数。第一个参数为当前vue组件对象_sfc_main,第二个参数是由很多组键值对组成的数组。
第一组键值对的key为render,值是名为_sfc_render的render函数。
第二组键值对的key为__scopeId,值为data-v-c1c19b2。
第三组键值对的key为__file,值为当前vue文件的路径。
编译后的js文件
从前面我们知道编译后的js文件export default导出的是_export_sfc函数的执行结果,我们在浏览器中给_export_sfc函数打个断点。刷新页面,代码会走到断点中,_export_sfc函数代码如下:
function export_sfc(sfc, props) {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
}
export_sfc函数的第一个参数为当前vue组件对象sfc,第二个参数为多组键值对组成的数组props。
由于我们这里的vue组件对象上没有__vccOpts属性,所以target的值还是sfc。
接着就是遍历传入的多组键值对,使用target[key] = val给vue组件对象上面额外添加三个属性,分别是render、__scopeId和__file。
在控制台中来看看经过export_sfc函数处理后的vue组件对象是什么样的,如下图:
从上图中可以看到此时的vue组件对象中增加了很多属性,其中我们需要关注的是__scopeId属性,他的值就是给html增加自定义属性data-v-x。
给render函数打断点
前面我们讲过了在render函数中渲染div标签时是使用_createElementBlock("div", _hoisted_1, "hello world"),并且传入的参数中也并没有data-v-x。
所以我们需要搞清楚到底是在哪里使用到__scopeId的呢?我们给render函数打一个断点,如下图:
刷新页面代码会走到render函数的断点中,将断点走进_createElementBlock函数中,在我们这个场景中简化后的_createElementBlock函数代码如下:
function createElementBlock(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag
) {
return setupBlock(
createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
true
)
);
}
从上面的代码可以看到createElementBlock并不是干活的地方,而是在里层先调用createBaseVNode函数,然后使用其结果再去调用setupBlock函数。
将断点走进createBaseVNode函数,在我们这个场景中简化后的代码如下:
function createBaseVNode(type, props, children) {
const vnode = {
type,
props,
scopeId: currentScopeId,
children,
// ...省略
};
return vnode;
}
此时传入的type值为div,props值为对象{class: 'block'},children值为字符串hello world。
createBaseVNode函数的作用就是创建div标签对应的vnode虚拟DOM,在虚拟DOM中有个scopeId属性。后续将虚拟DOM转换成真实DOM时就会读取这个scopeId属性给html标签增加自定义属性data-v-x。
scopeId属性的值是由一个全局变量currentScopeId赋值的,接下来我们需要搞清楚全局变量currentScopeId是如何被赋值的。
renderComponentRoot函数
从Call Stack中可以看到render函数是由一个名为renderComponentRoot的函数调用的,如下图:
将断点走进renderComponentRoot函数,在我们这个场景中简化后的代码如下:
function renderComponentRoot(instance) {
const { props, render, renderCache, data, setupState, ctx } = instance;
let result;
const prev = setCurrentRenderingInstance(instance);
result = normalizeVNode(
render.call(
thisProxy,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
);
setCurrentRenderingInstance(prev);
return result;
}
从上面的代码可以看到renderComponentRoot函数的入参是一个vue实例instance,我们在控制台来看看instance是什么样的,如下图:
从上图可以看到vue实例instance对象上有很多我们熟悉的属性,比如props、refs等。
instance对象上的type属性对象有没有觉得看着很熟悉?
这个type属性对象就是由vue文件编译成js文件后export default导出的vue组件对象。前面我们讲过了里面的__scopeId属性就是根据vue文件的路径调用node的createHash加密函数生成的。
在生成vue实例的时候会将“vue文件编译成js文件后export default导出的vue组件对象”塞到vue实例对象instance的type属性中,生成vue实例是在createComponentInstance函数中完成的,感兴趣的小伙伴可以打断点调试一下。
我们接着来看renderComponentRoot函数,首先会从instance实例中解构出render函数。
然后就是执行setCurrentRenderingInstance将全局维护的vue实例对象变量设置为当前的vue实例对象。
接着就是执行render函数,拿到生成的虚拟DOM赋值给result变量。
最后就是再次执行setCurrentRenderingInstance函数将全局维护的vue实例对象变量重置为上一次的vue实例对象。
setCurrentRenderingInstance函数
接着将断点走进setCurrentRenderingInstance函数,代码如下:
let currentScopeId = null;
let currentRenderingInstance = null;
function setCurrentRenderingInstance(instance) {
const prev = currentRenderingInstance;
currentRenderingInstance = instance;
currentScopeId = (instance && instance.type.__scopeId) || null;
return prev;
}
在setCurrentRenderingInstance函数中会将当前的vue实例赋值给全局变量currentRenderingInstance,并且会将instance.type.__scopeId赋值给全局变量currentScopeId。
在整个render函数执行期间全局变量currentScopeId的值都是instance.type.__scopeId。而instance.type.__scopeId我们前面已经讲过了,他的值是根据vue文件的路径调用node的createHash加密函数生成的,也是给html标签增加自定义属性data-v-x。
componentUpdateFn函数
前面讲过了在renderComponentRoot函数中会执行render函数,render函数会返回对应的虚拟DOM,然后将虚拟DOM赋值给变量result,最后renderComponentRoot函数会将变量result进行return返回。
将断点走出renderComponentRoot函数,此时断点走到了执行renderComponentRoot函数的地方,也就是componentUpdateFn函数。在我们这个场景中简化后的componentUpdateFn函数代码如下:
const componentUpdateFn = () => {
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor, instance, parentSuspense, namespace);
};
从上面的代码可以看到会将renderComponentRoot函数的返回结果(也就是组件的render函数生成的虚拟DOM)赋值给subTree变量,然后去执行大名鼎鼎的patch函数。
这个patch函数相比你多多少少听过,他接收的前两个参数分别是:旧的虚拟DOM、新的虚拟DOM。由于我们这里是初次加载没有旧的虚拟DOM,所以调用patch函数传入的第一个参数是null。第二个参数是render函数生成的新的虚拟DOM。
patch函数
将断点走进patch函数,在我们这个场景中简化后的patch函数代码如下:
const patch = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = !!n2.dynamicChildren
) => {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
};
从上面的代码可以看到在patch函数中主要是执行了processElement函数,参数也是透传给了processElement函数。
接着将断点走进processElement函数,在我们这个场景中简化后的processElement函数代码如下:
const processElement = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
) => {
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
}
};
从上面的代码可以看到如果n1 == null也就是当前没有旧的虚拟DOM,就会去执行mountElement函数将新的虚拟DOM挂载到真实DOM上。很明显我们这里n1的值确实是null,所以代码会走到mountElement函数中。
mountElement函数
接着将断点走进mountElement函数,在我们这个场景中简化后的mountElement函数代码如下:
const mountElement = (
vnode,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
) => {
let el;
el = vnode.el = hostCreateElement(vnode.type);
hostSetElementText(el, vnode.children);
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
};
从上面的代码可以看到在mountElement函数中首先会执行hostCreateElement函数生成真实DOM,并且将真实DOM赋值给变量el和vnode.el,所以虚拟DOM的el属性是指向对应的真实DOM。这里的vnode.type的值为div,所以这里就是生成一个div标签。
然后执行hostSetElementText函数给当前真实DOM的文本节点赋值,当前vnode.children的值为文本hello world。所以这里就是给div标签设置文本节点hello world。
最后就是调用setScopeId函数传入el和vnode.scopeId,给div标签增加自定义属性data-v-x。
接下来我们来看看上面这三个函数。
先将断点走进hostCreateElement函数,在我们这个场景中简化后的代码如下:
function hostCreateElement(tag) {
const el = document.createElement(tag, undefined);
return el;
}
由于传入的tag变量的值是div,所以此时hostCreateElement函数就是调用了document.createElement方法生成一个div标签,并且将其return返回。
经过hostCreateElement函数的处理后,已经生成了一个div标签,并且将其赋值给变量el。接着将断点走进hostSetElementText函数,代码如下:
function hostSetElementText(el, text) {
el.textContent = text;
}
hostSetElementText函数接收的第一个参数为el,也就是生成的div标签。第二个参数为text,也就是要向div标签填充的文本节点,在我们这里是字符串hello world。
这里的textContent属性你可能用的比较少,他的作用和innerText差不多。给textContent属性赋值就是设置元素的文字内容,在这里就是将div标签的文本设置为hello world。
经过hostSetElementText函数的处理后生成的div标签已经有了文本节点hello world。接着将断点走进setScopeId函数,在我们这个场景中简化后的代码如下:
const setScopeId = (el, vnode, scopeId) => {
if (scopeId) {
hostSetScopeId(el, scopeId);
}
};
function hostSetScopeId(el, id) {
el.setAttribute(id, "");
}
在setScopeId函数中如果传入了scopeId,就会执行hostSetScopeId函数。而这个scopeId就是我们前面讲过的data-v-x。
在hostSetScopeId函数中会调用DOM的setAttribute方法,给div标签增加data-v-x属性,由于调用setAttribute方法的时候传入的第二个参数为空字符串,所以div上面的data-v-x属性是没有属性值的。所以最终生成的div标签就是这样的:<div data-v-c1c19b25 class="block">hello world</div>
总结
这篇文章讲了当使用了scoped后,vue是如何给html增加自定义属性data-v-x。
首先在编译时会根据当前vue文件的路径进行加密算法生成一个id,这个id就是自定义属性data-v-x中的x。
然后给编译后的vue组件对象增加一个属性__scopeId,属性值就是data-v-x。
在运行时的renderComponentRoot函数中,这个函数接收的参数是vue实例instance对象,instance.type的值就是编译后的vue组件对象。
在renderComponentRoot函数中会执行setCurrentRenderingInstance函数,将全局变量currentScopeId的值赋值为instance.type.__scopeId,也就是data-v-x。
在renderComponentRoot函数中接着会执行render函数,在生成虚拟DOM的过程中会去读取全局变量currentScopeId,并且将其赋值给虚拟DOM的scopeId属性。
接着就是拿到render函数生成的虚拟DOM去执行patch函数生成真实DOM,在我们这个场景中最终生成真实DOM的是mountElement函数。
在mountElement函数中首先会调用document.createElement函数去生成一个div标签,然后使用textContent属性将div标签的文本节点设置为hello world。
最后就是调用setAttribute方法给div标签设置自定义属性data-v-x。
- 上一篇:CSS变量 css变量作用域
- 下一篇:Vue3 的改变 vue3变化
相关推荐
- # 安装打开 ubuntu-22.04.3-LTS 报错 解决方案
-
#安装打开ubuntu-22.04.3-LTS报错解决方案WslRegisterDistributionfailedwitherror:0x800701bcError:0x80070...
- 利用阿里云镜像在ubuntu上安装Docker
-
简介:...
- 如何将Ubuntu Kylin(优麒麟)19.10系统升级到20.04版本
-
UbuntuKylin系统使用一段时间后,有新的版本发布,如何将现有的UbuntuKylin系统升级到最新版本?可以通过下面的方法进行升级。1.先查看相关的UbuntuKylin系统版本情况。使...
- Ubuntu 16.10内部代号确认为Yakkety Yak
-
在正式宣布Ubuntu16.04LTS(XenialXerus)的当天,Canonical创始人MarkShuttleworth还非常开心的在个人微博上宣布Ubuntu下个版本16.10的内...
- 如何在win11的wsl上装ubuntu(怎么在windows上安装ubuntu)
-
在Windows11的WSL(WindowsSubsystemforLinux)上安装Ubuntu非常简单。以下是详细的步骤:---...
- Win11学院:如何在Windows 11上使用WSL安装Ubuntu
-
IT之家2月18日消息,科技媒体pureinfotech昨日(2月17日)发布博文,介绍了3中简便的方法,让你轻松在Windows11系统中,使用WindowsSubs...
- 如何查看Linux的IP地址(如何查看Linux的ip地址)
-
本头条号每天坚持更新原创干货技术文章,欢迎关注本头条号"Linux学习教程",公众号名称“Linux入门学习教程"。...
- 怎么看电脑系统?(怎么看电脑系统配置)
-
要查看电脑的操作系统信息,可以按照以下步骤操作,根据不同的操作系统选择对应的方法:一、Windows系统通过系统属性查看右键点击桌面上的“此电脑”(或“我的电脑”)图标,选择“属性”。在打开的...
- 如何查询 Linux 内核版本?这些命令一定要会!
-
Linux内核是操作系统的核心,负责管理硬件资源、调度进程、处理系统调用等关键任务。不同的内核版本可能支持不同的硬件特性、提供新的功能,或者修复了已知的安全漏洞。以下是查询内核版本的几个常见场景:...
- 深度剖析:Linux下查看系统版本与CPU架构
-
在Linux系统管理、维护以及软件部署的过程中,精准掌握系统版本和CPU架构是极为关键的基础操作。这些信息不仅有助于我们深入了解系统特性、判断软件兼容性,还能为后续的软件安装、性能优化提供重要依据。接...
- 504 错误代码解析与应对策略(504错误咋解决)
-
在互联网的使用过程中,用户偶尔会遭遇各种错误提示,其中504错误代码是较为常见的一种。504错误并非意味着网站被屏蔽,它实际上是指服务器在规定时间内未能从上游服务器获取响应,专业术语称为“Ga...
- 猎聘APP和官网崩了?回应:正对部分职位整改,临时域名可登录
-
10月12日,有网友反映猎聘网无法打开,猎聘APP无法登录。截至10月14日,仍有网友不断向猎聘官方微博下反映该情况,而猎聘官方微博未发布相关情况说明,只是在微博内对反映该情况的用户进行回复,“抱歉,...
- 域名解析的原理是什么?域名解析的流程是怎样的?
-
域名解析是网站正常运行的关键因素,因此网站管理者了解域名解析的原理和流程对于做好域名管理、解决常见解析问题,保障网站的正常运转十分必要。那么域名解析的原理是什么?域名解析的流程是怎样的?接下来,中科三...
- Linux无法解析域名的解决办法(linux 不能解析域名)
-
如果由于误操作,删除了系统原有的dhcp相关设置就无法正常解析域名。 此时,需要手动修改配置文件: /etc/resolv.conf 将域名解析服务器手动添加到配置文件中 该文件是DNS域名解...
- 域名劫持是什么?(域名劫持是什么)
-
域名劫持是互联网攻击的一种方式,通过攻击域名解析服务器(DNS),或伪造域名解析服务器(DNS)的方法,把目标网站域名解析到错误的地址从而实现用户无法访问目标网站的目的。说的直白些,域名劫持,就是把互...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- # 安装打开 ubuntu-22.04.3-LTS 报错 解决方案
- 利用阿里云镜像在ubuntu上安装Docker
- 如何将Ubuntu Kylin(优麒麟)19.10系统升级到20.04版本
- Ubuntu 16.10内部代号确认为Yakkety Yak
- 如何在win11的wsl上装ubuntu(怎么在windows上安装ubuntu)
- Win11学院:如何在Windows 11上使用WSL安装Ubuntu
- 如何查看Linux的IP地址(如何查看Linux的ip地址)
- 怎么看电脑系统?(怎么看电脑系统配置)
- 如何查询 Linux 内核版本?这些命令一定要会!
- 深度剖析:Linux下查看系统版本与CPU架构
- 标签列表
-
- navicat无法连接mysql服务器 (65)
- 下横线怎么打 (71)
- flash插件怎么安装 (60)
- lol体验服怎么进 (66)
- ae插件怎么安装 (62)
- yum卸载 (75)
- .key文件 (63)
- cad一打开就致命错误是怎么回事 (61)
- rpm文件怎么安装 (66)
- linux取消挂载 (81)
- ie代理配置错误 (61)
- ajax error (67)
- centos7 重启网络 (67)
- centos6下载 (58)
- mysql 外网访问权限 (69)
- centos查看内核版本 (61)
- ps错误16 (66)
- nodejs读取json文件 (64)
- centos7 1810 (59)
- 加载com加载项时运行错误 (67)
- php打乱数组顺序 (68)
- cad安装失败怎么解决 (58)
- 因文件头错误而不能打开怎么解决 (68)
- js判断字符串为空 (62)
- centos查看端口 (64)