首页 > 其他分享 >掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(下)

掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(下)

时间:2024-07-03 08:58:20浏览次数:19  
标签:vue 函数 DOM 我悟 scopeId scoped vue3 data 属性

前言

在上一篇 掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(上) 文章中我们讲了使用scoped后,vue是如何给CSS选择器添加对应的属性选择器[data-v-x]。这篇文章我们来接着讲使用了scoped后,vue是如何给html增加自定义属性data-v-x。注:本文中使用的vue版本为3.4.19@vitejs/plugin-vue的版本为5.0.4

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

看个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 函数

在 通过debug搞清楚.vue文件怎么变成.js文件文章中我们讲过了transformMain 函数的作用是将vue文件转换成js文件。

首先我们需要启动一个debug终端。这里以vscode举例,打开终端然后点击终端中的+号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal就可以启动一个debug终端。
debug-terminal

接着我们需要给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

从上图中可以看到入参code为vue文件的code代码字符串。

在上一篇 掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(上) 文章中我们讲过了createDescriptor函数会生成一个descriptor对象。而descriptor对象的id属性descriptor.id,就是根据vue文件的路径调用node的createHash加密函数生成的,也就是html标签上的自定义属性data-v-x中的x

genTemplateCode函数会生成编译后的render函数,如下图:
templateCode

从上图中可以看到在生成的render函数中,div标签对应的是createElementBlock方法,而在执行createElementBlock方法时并没有将descriptor.id传入进去。

genTemplateCode函数、genScriptCode函数、genStyleCode函数执行完了后,得到templateCodescriptCodestylesCode,分别对应的是编译后的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文件,如下图:
resolvedCode

从上图中可以看到编译后的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组件对象是什么样的,如下图:
sfc

从上图中可以看到此时的vue组件对象中增加了很多属性,其中我们需要关注的是__scopeId属性,他的值就是给html增加自定义属性data-v-x

给render函数打断点

前面我们讲过了在render函数中渲染div标签时是使用_createElementBlock("div", _hoisted_1, "hello world"),并且传入的参数中也并没有data-v-x

所以我们需要搞清楚到底是在哪里使用到__scopeId的呢?我们给render函数打一个断点,如下图:
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值为divprops值为对象{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的函数调用的,如下图:
call-stack

将断点走进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是什么样的,如下图:
instance

从上图可以看到vue实例instance对象上有很多我们熟悉的属性,比如propsrefs等。

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赋值给变量elvnode.el,所以虚拟DOM的el属性是指向对应的真实DOM。这里的vnode.type的值为div,所以这里就是生成一个div标签。

然后执行hostSetElementText函数给当前真实DOM的文本节点赋值,当前vnode.children的值为文本hello world。所以这里就是给div标签设置文本节点hello world

最后就是调用setScopeId函数传入elvnode.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

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

标签:vue,函数,DOM,我悟,scopeId,scoped,vue3,data,属性
From: https://www.cnblogs.com/heavenYJJ/p/18280587

相关文章

  • 前端vue3项目dagre-d3基础配置项及流程图组件示例(包括安装依赖)
    目录引言d3是什么?dagre是什么?dagre-d3是什么?dagre-d3配置项流程图示例依赖安装组件示例总结引言因为很多文档都是英文,刚开始调研的时候比较费劲,文档里的配置像示例又比较分散,就自己整理了一下,附上测试时写的示例d3是什么?d3.js 是一个强大的JavaScript库,用于在......
  • 【electron-vite+live2d+vue3+element-plus】实现桌面模型宠物+桌面管理系统应用(踩坑)
    脚手架项目使用electron-vite脚手架搭建ps:还有一个框架是electron-vite,这个框架我发现与pixi库有冲突,无法使用,如果不用pixi也可以用这个脚手架。node版本建议18+----------------------------------------------------------------------------------------运行live2D......
  • vue3.4+最新属性变化
    Attribute绑定新增简写方法<!--与:id="id"相同--><div:id></div><!--这也同样有效--><divv-bind:id></div>动态参数<av-bind:[demoName]="url"></a>//简写<a:[demoName]="url"></a>......
  • Vue3 从零到全掌握:最详尽的入门指南(近万字超全内容)
    一、Vue脚手架Vue3官方文档地址:https://v3.cn.vuejs.org/以前的官方脚手架@vue-cli也可以用,但这里推荐一个更轻快的脚手架Vite脚手架网址:Vite中文网方式一:vue-cli脚手架初始化Vue3项目官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create// ......
  • vue2、vue3 纯SCSS 实现环形进度条
    vue3纯SCSS实现环形进度条<template><viewclass="flexalign-centerdiygw-col-24justify-center"><viewclass="progress-circle":class="`progress-${innerPercent}`"><viewclass=&quo......
  • ant-ui+vue3使用踩坑记录
    1、table组件使用Summary合计时,明明设置summary的fixed属性,设置固定还是没有生效!滚动的时候合计栏还是会滚动代码    通过查看官方文档,发现还要配合设置SummaryCell的index序号进行指定   解决方法如下,那个栏目需要固定就设置相应的index 效果 ......
  • Vue3手写一个全局命令式loading组件
    实现效果:vue文件中,打开全局loading...2s后关闭全局命令式loading,效果展示完,直接咱就是上代码 注册:  <!--src/components/myLoading/index.vue--><template><!--添加name属性,以添加样式Transition主要做一个淡入淡出的--><Transitionname="zhLoadi......
  • ts vue3 getCurrentInstance 使用,$refs 调用方式
    代码示例可以通过ref变量实现绑定$ref,或者getCurrentInstance来获取$refs/***$ref使用方式,变量名和组件的属性ref值一致*/consthChildRef=ref()console.log(hChildRef,"hChildRef")constinstance=getCurrentInstance()//constself=(instanceasComponen......
  • vue3一些高阶用法
    1.ref和reactive首先,让我们从Vue3最基础也是最常用的两个方法开始:ref 和 reactive。它们是响应式基础。refref 用于定义一个响应式的数据对象。它适用于单个基本类型或对象的场景。<scriptsetup>import{ref}from'vue'constcount=ref(0)functionincr......
  • 自定义vue3 hooks
    文章目录hooks目录结构demohooks当页面内有很多的功能,js代码太多,不好维护,可以每个功能都有写一个js或者ts,这样的话,代码易读,并且容易维护,组合式setup写法与此结合......