父组件明明使用了v-model,子组件竟然可以不用定义props和emit抛出事件,快来看看吧

news2024/11/28 10:52:36

前言

vue3.4增加了defineModel宏函数,在子组件内修改了defineModel的返回值,父组件上v-model绑定的变量就会被更新。大家都知道v-model:modelValue@update:modelValue的语法糖,但是你知道为什么我们在子组件内没有写任何关于props的定义和emit事件触发的代码吗?还有在template渲染中defineModel的返回值等于父组件v-model绑定的变量值,那么这个返回值是否就是名为modelValue的props呢?直接修改defineModel的返回值就会修改父组件上面绑定的变量,那么这个行为是否相当于子组件直接修改了父组件的变量值,破坏了vue的单向数据流呢?

先说答案

defineModel宏函数经过编译后会给vue组件对象上面增加modelValue的props选项和update:modelValue的emits选项,执行defineModel宏函数的代码会变成执行useModel函数,如下图:
convert

经过编译后defineModel宏函数已经变成了useModel函数,而useModel函数的返回值是一个ref对象。注意这个是ref对象不是props,所以我们才可以在组件内直接修改defineModel的返回值。当我们对这个ref对象进行“读操作”时,会像Proxy一样被拦截到ref对象的get方法。在get方法中会返回本地维护localValue变量,localValue变量依靠watchSyncEffectlocalValue变量始终和父组件传递的modelValueprops值一致。

对返回值进行“写操作”会被拦截到ref对象的set方法中,在set方法中会将最新值同步到本地维护localValue变量,调用vue实例上的emit方法抛出update:modelValue事件给父组件,由父组件去更新父组件中v-model绑定的变量。如下图:
useModel

所以在子组件内无需写任何关于props的定义和emit事件触发的代码,因为在编译defineModel宏函数的时候已经帮我们生成了modelValue的props选项。在对返回的ref变量进行写操作时会触发set方法,在set方法中会调用vue实例上的emit方法抛出update:modelValue事件给父组件。

defineModel宏函数的返回值是一个ref变量,而不是一个props。所以我们可以直接修改defineModel宏函数的返回值,父组件绑定的变量之所以会改变是因为在底层会抛出update:modelValue事件给父组件,由父组件去更新绑定的变量,这一行为当然满足vue的单向数据流。

关注公众号:前端欧阳,解锁我更多vue干货文章。还可以加我微信,私信我想看哪些vue原理文章,我会根据大家的反馈进行创作。

什么是vue的单向数据流

vue的单向数据流是指,通过props将父组件的变量传递给子组件,在子组件中是没有权限去修改父组件传递过来的变量。只能通过emit抛出事件给父组件,让父组件在事件回调中去修改props传递的变量,然后通过props将更新后的变量传递给子组件。在这一过程中数据的流动是单向的,由父组件传递给子组件,只有父组件有数据的更改权,子组件不可直接更改数据。
single-progress

一个defineModel的例子

我在前面的 一文搞懂 Vue3 defineModel 双向绑定:告别繁琐代码!文章中已经讲过了defineModel的各种用法,在这篇文章中我们就不多余赘述了。我们直接来看一个简单的defineModel的例子。

下面这个是父组件的代码:

<template>
  <CommonChild v-model="inputValue" />
  <p>input value is: {{ inputValue }}</p>
</template>

<script setup lang="ts">
import { ref } from "vue";
import CommonChild from "./child.vue";

const inputValue = ref();
</script>

父组件的代码很简单,使用v-model指令将inputValue变量传递给子组件。然后在父组件上使用p标签渲染出inputValue变量的值。

我们接下来看子组件的代码:

<template>
  <input v-model="model" />
  <button @click="handelReset">reset</button>
</template>

<script setup lang="ts">
const model = defineModel();

function handelReset() {
  model.value = "init";
}
</script>

子组件内的代码也很简单,将defineModel的返回值赋值给model变量。然后使用v-model指令将model变量绑定到子组件的input输入框上面。并且还在按钮的click事件时使用model.value = "init"将绑定的值重置为init字符串。请注意在子组件中我们没有任何定义props的代码,也没有抛出emit事件的代码。而是通过defineModel宏函数的返回值来接收父组件传过来的名为modelValue的prop,并且在子组件中是直接通过给defineModel宏函数的返回值进行赋值来修改父组件绑定的inputValue变量的值。

defineModel编译后的样子

要回答前面提的几个问题,我们还是得从编译后的子组件代码说起。下面这个是经过简化编译后的子组件代码:

import {
  defineComponent as _defineComponent,
  useModel as _useModel
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";

const _sfc_main = _defineComponent({
  __name: "child",
  props: {
    modelValue: {},
    modelModifiers: {},
  },
  emits: ["update:modelValue"],
  setup(__props) {
    const model = _useModel(__props, "modelValue");
    function handelReset() {
      model.value = "init";
    }
    const __returned__ = { model, handelReset };
    return __returned__;
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    // ... 省略
  );
}
_sfc_main.render = _sfc_render;
export default _sfc_main;

从上面我们可以看到编译后主要有_sfc_main_sfc_render这两块,其中_sfc_renderrender函数,不是我们这篇文章关注的重点。我们来主要看_sfc_main对象,看这个对象的样子有name、props、emits、setup属性,我想你也能够猜出来他就是vue的组件对象。从组件对象中我们可以看到已经有了一个modelValueprops属性,还有使用emits选项声明了update:modelValue事件。我们在源代码中没有任何地方有定义propsemits选项,很明显这两个是通过编译defineModel宏函数而来的。

我们接着来看里面的setup函数,可以看到经过编译后的setup函数中代码和我们的源代码很相似。只有defineModel不在了,取而代之的是一个useModel函数。

// 编译前的代码
const model = defineModel();

// 编译后的代码
const model = _useModel(__props, "modelValue");

还是同样的套路,在浏览器的sources面板上面找到编译后的js文件,然后给这个useModel打个断点。至于如何找到编译后的js文件我们在前面的文章中已经讲了很多遍了,这里就不赘述了。刷新浏览器我们看到断点已经走到了使用useModel函数的地方,我们这里给useModel函数传了两个参数。第一个参数为子组件接收的props对象,第二个参数是写死的字符串modelValue。进入到useModel函数内部,简化后的useModel函数是这样的:

function useModel(props, name) {
  const i = getCurrentInstance();
  const res = customRef((track2, trigger2) => {
    watchSyncEffect(() => {
      // 省略
    });
  });
  return res;
}

从上面的代码中我们可以看到useModel中使用到的函数没有一个是vue内部源码专用的函数,全都是调用的vue暴露出来的API。这意味着我们可以参考defineModel的实现源码,也就是useModel函数,然后根据自己实际情况改良一个适合自己项目的defineModel函数。

我们先来简单介绍一下useModel函数中使用到的API,分别是getCurrentInstancecustomRefwatchSyncEffect,这三个API都是从vue中import导入的。

getCurrentInstance函数

首先来看看getCurrentInstance函数,他的作用是返回当前的vue实例。为什么要调用这个函数呢?因为在setup中this是拿不到vue实例的,后面对值进行写操作时会调用vue实例上面的emit方法抛出update事件。

watchSyncEffect函数

接着我们来看watchSyncEffect函数,这个API大家平时应该比较熟悉了。他的作用是立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时立即重新执行这个函数。

比如下面这段代码,会立即执行console,当count变量的值改变后,也会立即执行console。

const count = ref(0)

watchSyncEffect(() => console.log(count.value))
// -> 输出 0

customRef函数

最后我们来看customRef函数,他是useModel函数的核心。这个函数小伙伴们应该用的比较少,我们这篇文章只简单讲讲他的用法即可。如果小伙伴们对customRef函数感兴趣可以留言或者给我发消息,关注的小伙伴们多了我后面会安排一篇文章来专门讲customRef函数。官方的解释为:

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

这句话的意思是customRef函数的返回值是一个ref对象。当我们对返回值ref对象进行“读操作”时,会被拦截到ref对象的get方法中。当我们对返回值ref对象进行“写操作”时,会被拦截到ref对象的set方法中。和Promise相似同样接收一个工厂函数作为参数,Promise的工厂函数是接收的resolvereject两个函数作为参数,customRef的工厂函数是接收的tracktrigger两个函数作为参数。track用于手动进行依赖收集,trigger函数用于手动进行依赖触发。

我们知道vue的响应式原理是由依赖收集和依赖触发的方式实现的,比如我们在template中使用一个ref变量。当template被编译为render函数后,在浏览器中执行render函数时,就会对ref变量进行读操作。读操作会被拦截到Proxy的get方法中,由于此时在执行render函数,所以当前的依赖就是render函数。在get方法中会进行依赖收集,将当前的render函数作为依赖收集起来。注意这里的依赖收集是vue内部自动完成的,在我们的代码中无需手动去进行依赖收集。

当我们对ref变量进行写操作时,此时会被拦截到Proxy的set方法,在set方法中会将收集到的依赖依次取出来执行,我们前面收集的依赖是render函数。所以render函数就会重新执行,执行render函数生成虚拟DOM,再生成真实DOM,这样浏览器中渲染的就是最新的ref变量的值。同样这里依赖触发也是在vue内部自动完成的,在我们的代码中无需手动去触发依赖。

搞清楚了依赖收集和依赖触发现在来讲tracktrigger两个函数你应该就能很容易理解了,tracktrigger两个函数可以让我们手动控制什么时候进行依赖收集和依赖触发。执行track函数就会手动收集依赖,执行trigger函数就会手动触发依赖,进行页面刷新。在defineModel这个场景中track手动收集的依赖就是render函数,trigger手动触发会导致render函数重新执行,进而完成页面刷新。

useModel函数

现在我们可以来看useModel函数了,简化后的代码如下:

function useModel(props, name) {
  const i = getCurrentInstance();

  const res = customRef((track2, trigger2) => {
    let localValue;
    watchSyncEffect(() => {
      const propValue = props[name];
      if (hasChanged(localValue, propValue)) {
        localValue = propValue;
        trigger2();
      }
    });
    return {
      get() {
        track2();
        return localValue;
      },
      set(value) {
        if (hasChanged(value, localValue)) {
          localValue = value;
          trigger2();
        }
        i.emit(`update:${name}`, value);
      },
    };
  });
  return res;
}

从上面我们可以看到useModel函数的代码其实很简单,useModel的返回值就是customRef函数的返回值,也就是一个ref变量对象。我们看到返回值对象中有getset方法,还有在customRef函数中使用了watchSyncEffect函数。

get方法

在前面的demo中,我们在子组件的template中使用v-modeldefineModel的返回值绑定到一个input输入框中。代码如下:

<input v-model="model" />

在第一次执行render函数时会对model变量进行读操作,而model变量是defineModel宏函数的返回值。编译后我们看到defineModel宏函数变成了useModel函数。所以对model变量进行读操作,其实就是对useModel函数的返回值进行读操作。我们看到useModel函数的返回值是一个自定义ref,在自定义ref中有get和set方法,当对自定义ref进行读操作时会被拦截到ref对象中的get方法。这里在get方法中会手动执行track2方法进行依赖收集。因为此时是在执行render函数,所以收集到的依赖就是render函数,然后将本地维护的localValue的值进行拦截返回。

set方法

在我们前面的demo中,子组件reset按钮的click事件中会对defineModel的返回值model变量进行写操作,代码如下:

function handelReset() {
  model.value = "init";
}

和对model变量“读操作”同理,对model变量进行“写操作”也会被拦截到返回值ref对象的set方法中。在set方法中会先判断新的值和本地维护的localValue的值比起来是否有修改。如果有修改那就将更新后的值同步更新到本地维护的localValue变量,这样就保证了本地维护的localValue始终是最新的值。然后执行trigger2函数手动触发收集的依赖,在前面get的时候收集的依赖是render函数,所以这里触发依赖会重新执行render函数,然后将最新的值渲染到浏览器上面。

在set方法中接着会调用vue实例上面的emit方法进行抛出事件,代码如下:

i.emit(`update:${name}`, value)

这里的i就是getCurrentInstance函数的返回值。前面我们讲过了getCurrentInstance函数的返回值是当前vue实例,所以这里就是调用vue实例上面的emit方法向父组件抛出事件。这里的name也就是调用useModel函数时传入的第二个参数,我们来回忆一下前面是怎样调用useModel函数的 ,代码如下:

const model = _useModel(__props, "modelValue")

传入的第一个参数为当前的props对象,第二个参数是写死的字符串"modelValue"。那这里调用emit抛出的事件就是update:modelValue,传递的参数为最新的value的值。这就是为什么不需要在子组件中使用使用emit抛出事件,因为在defineModel宏函数编译成的useModel函数中已经帮我们使用emit抛出事件了。

watchSyncEffect函数

我们接着来看子组件中怎么接收父组件传递过来的props呢,答案就在watchSyncEffect函数中。回忆一下前面讲过的useModel函数中的watchSyncEffect代码如下:

function useModel(props, name) {
  const res = customRef((track2, trigger2) => {
    let localValue;
    watchSyncEffect(() => {
      const propValue = props[name];
      if (hasChanged(localValue, propValue)) {
        localValue = propValue;
        trigger2();
      }
    });
    return {
     // ...省略
    };
  });
  return res;
}

这个name也就是调用useModel函数时传过来的第二个参数,我们前面已经讲过了是一个写死的字符串"modelValue"。那这里的const propValue = props[name]就是取父组件传递过来的名为modelValueprop,我们知道v-model就是:modelValue的语法糖,所以这个propValue就是取的是父组件v-model绑定的变量值。如果本地维护的localValue变量的值不等于父组件传递过来的值,那么就将本地维护的localValue变量更新,让localValue变量始终和父组件传递过来的值一样。并且触发依赖重新执行子组件的render函数,将子组件的最新变量的值更新到浏览器中。为什么要调用trigger2函数呢?原因是可以在子组件的template中渲染defineModel函数的返回值,也就是父组件传递过来的prop变量。如果父组件传递过来的prop变量值改变后不重新调用trigger2函数以重新执行render函数,那么子组件中的渲染的变量值就一直都是旧的值了。因为这个是在watchSyncEffect内执行的,所以每次父组件传过来的props值变化后都会再执行一次,让本地维护的localValue变量的值始终等于父组件传递过来的值,并且子组件页面上也始终渲染的是最新的变量值。

这就是为什么在子组件中没有任何props定义了,因为在defineModel宏函数编译后会给vue组件对象塞一个modelValue的prop,并且在useModel函数中会维护一个名为localValue的本地变量接收父组件传递过来的props.modelValue,并且让localValue变量和props.modelValue的值始终保持一致。

总结

现在我们可以回答前面提的几个问题了:

  • 使用defineModel宏函数后,为什么我们在子组件内没有写任何关于props定义的代码?

    答案是本地会维护一个localValue变量接收父组件传递过来的名为modelValue的props。调用defineModel函数的代码经过编译后会变成一个调用useModel函数的代码,useModel函数的返回值是一个ref对象。当我们对defineModel的返回值进行“读操作”时,类似于Proxyget方法一样会对读操作进行拦截到返回值ref对象的get方法中。而get方法的返回值为本地维护的localValue变量,在watchSyncEffect的回调中将父组件传递过来的名为modelValue的props赋值给本地维护的localValue变量。并且由于是在watchSyncEffect中,所以每次props改变都会执行这个回调,所以本地维护的localValue变量始终是等于父组件传递过来的modelValue。也正是因为defineModel宏函数的返回值是一个ref对象而不是一个prop,所以我们可以在子组件内直接将defineModel的返回值使用v-model绑定到子组件input输入框上面。

  • 使用defineModel宏函数后,为什么我们在子组件内没有写任何关于emit事件触发的代码?

    答案是因为调用defineModel函数的代码经过编译后会变成一个调用useModel函数的代码,useModel函数的返回值是一个ref对象。当我们直接修改defineModel的返回值,也就是修改useModel函数的返回值。类似于Proxyset方法一样会对写行为进行拦截到ref对象中的set方法中。在set方法中会手动触发依赖,render函数就会重新执行,浏览器上就会渲染最新的变量值。然后调用vue实例上的emit方法,向父组件抛出update:modelValue事件。并且将最新的值随着事件一起传递给父组件,由父组件在update:modelValue事件回调中将父组件中v-model绑定的变量更新为最新值。

  • template渲染中defineModel的返回值等于父组件v-model绑定的变量值,那么这个返回值是否就是名为modelValue的props呢?

    从第一个回答中我们知道defineModel的返回值不是props,而是一个ref对象。

  • 直接修改defineModel的返回值就会修改父组件上面绑定的变量,那么这个行为是否相当于子组件直接修改了父组件的变量值,破坏了vue的单向数据流呢?

    修改defineModel的返回值,就会更新父组件中v-model绑定的变量值。看着就像是子组件中直接修改了父组件的变量值,从表面上看着像是打破了vue的单向数据流。实则并不是那样的,虽然我们在代码中没有写过emit抛出事件的代码,但是在defineModel函数编译成的useModel函数中已经帮我们使用emit抛出事件了。所以并没有打破vue的单向数据流

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1576936.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

以动态库链接库 .dll 探索结构体参数

Dev c C语言实现第一个 dll 动态链接库 创建与调用-CSDN博客 在写dll 插件中发现的函数指针用途和 typedef 的定义指针的用法-CSDN博客 两步之后&#xff0c;尝试加入结构体实现整体数据使用。 注意结构体 Ak 是相同的 代码如下 DLL文件有两个&#xff0c;dll.dll是上面提到…

[LeetCode][LCR178]训练计划 VI——使用位运算寻找数组中不同的数字

题目 LCR 178. 训练计划 VI 教学过程中&#xff0c;教练示范一次&#xff0c;学员跟做三次。该过程被混乱剪辑后&#xff0c;记录于数组 actions&#xff0c;其中 actions[i] 表示做出该动作的人员编号。请返回教练的编号。 示例 1&#xff1a; 输入&#xff1a;actions [5, …

帝国CMS模板源码整站安装说明(图文)

安装步骤 第一步&#xff1a;先把得到的文件解压缩&#xff0c;把文件通过FTP传到空间里。&#xff08;请不要把类似www.lengleng.net这个文件夹传到FTP&#xff0c;请传这个大文件夹下面的所有文件夹和文件到空间根目录&#xff0c;请不要上传到2级目录&#xff0c;除非你自己…

kimi开放API使用了,来看如何使用

更多精彩内容在公众号。 kimi现在算是国内火得不行的AI工具。最近使用人太多&#xff0c;都经常出现响应不过来的情况。借助这波热潮&#xff0c;kimi顺势推出了API使用。 来看kimi的官方介绍使用。https://platform.moonshot.cn 文本生成模型 Moonshot的文本生成模型&#…

外包干了6天,技术明显进步

先说一下自己的情况&#xff0c;本科生&#xff0c;2019年我通过校招踏入了南京一家软件公司&#xff0c;开始了我的职业生涯。那时的我&#xff0c;满怀热血和憧憬&#xff0c;期待着在这个行业中闯出一片天地。然而&#xff0c;随着时间的推移&#xff0c;我发现自己逐渐陷入…

PWM 脉宽跟随方案介绍

1. 前言 数字电源产品在使用桥式电路拓扑或是多路交错控制中&#xff0c;有时会需要滞后臂的 PWM 脉宽严格跟随超前臂的 PWM 脉宽&#xff0c;或从路的 PWM 脉宽严格跟随主路的 PWM 脉宽&#xff0c;本文将介绍如何利用高精度定时器实现 PWM 输出脉宽跟随&#xff0c;一种使用…

设计模式浅析(十一) ·状态模式

设计模式浅析(十一) 状态模式 日常叨逼叨 java设计模式浅析&#xff0c;如果觉得对你有帮助&#xff0c;记得一键三连&#xff0c;谢谢各位观众老爷&#x1f601;&#x1f601; 状态模式 概念 状态模式 Java中的状态模式&#xff08;State Pattern&#xff09;是一种行为型…

Web漏洞-文件上传常见验证

后缀名&#xff1a;类型&#xff0c;文件头等 后缀名&#xff1a;黑白名单 文件类型&#xff1a;MIME信息 文件头&#xff1a;内容头信息 常见黑名单&#xff08;明确不允许上传的格式后缀&#xff09;&#xff1a;asp、php、jsp、aspx、cgi、war &#xff08;如果没有完整…

绿联 安装Frpc内网穿透并使用Nginx反向代理

绿联 安装Frpc内网穿透并使用Nginx反向代理 1、前言 服务器官网&#xff1a;雨云 - 新一代云服务提供商 本教程使用Frps与Frpc进行内网穿透&#xff0c;其中Frps需要自购服务器安装&#xff0c;若无法购买服务器则本教程对你无用&#xff1b; 另外还需拥有自己的域名&#xf…

什么是数据库?如何安装SQL Server(超详细版)

文章目录 什么是数据库数据库与数据库管理系统数据库系统之间的区别和联系数据库在生活中的应用 安装SQL Server数据库系统要求 安装步骤(超详细)安装前的准备 安装SSMS 什么是数据库 数据库&#xff0c;顾名思义&#xff0c;是存储数据的“仓库”。它不仅仅是简单的数据存储&…

软件验收流程

验收环节&#xff0c;甲方需要做哪些事情&#xff1f;这些事情的流程是什么&#xff1f;做这些事情能给甲方带来什么好处&#xff1f; 软件验收阶段&#xff0c;甲方要做的事情&#xff1a; 验收环节介绍 1. 开始 目的&#xff1a;启动验收流程&#xff0c;为后续工作做好准…

第九届蓝桥杯大赛个人赛省赛(软件类)真题C 语言 A 组-航班时间

#include<iostream> using namespace std;int getTime(){int h1, h2, m1, m2, s1, s2, d 0;//d一定初始化为0&#xff0c;以正确处理不跨天的情况 scanf("%d:%d:%d %d:%d:%d (%d)", &h1, &m1, &s1, &h2, &m2, &s2, &d);return d …

【随笔】Git 高级篇 -- 相对引用2 HEAD~n(十三)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

matlab使用教程(37)—求解数值积分(2)

1多项式积分的解析解 本示例显示如何使用 polyint 函数对多项式求解析积分。使用此函数来计算多项式的不定积分。 1.1定义问题 考虑实数不定积分&#xff0c; 其中 k 是积分常量。由于没有指定积分限值&#xff0c; integral 函数族不太适合求解这个问题。 1.2用向量表示多…

量子计算领域迎来了一项令人振奋的突破。

近日&#xff0c;量子计算领域迎来了一项令人振奋的突破。Quantinuum公司与科技巨头Microsoft宣布&#xff0c;双方在容错量子计算方面取得了显著成果。这一成就原本预计需要数年时间才能实现&#xff0c;但两家公司的紧密合作使得这一目标提前达成。 Quantinuum的新一代量子计…

js笔记(学习存档)

JS的调用方式与执行顺序 使用方式 HTML页面中的任意位置加上<script type"module"></script>标签即可。 常见使用方式有以下几种&#xff1a; 直接在<script type"module"></script>标签内写JS代码。直接引入文件&#xff1a;…

2024-4-7 QT day1作业

myWidget.cpp #include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {//设置窗口标题this->setWindowTitle("QQ");//设置窗口图标this->setWindowIcon(QIcon("C:\\Users\\张谦\\Desktop\\pictrue\\qq.png"));//设…

V R沉浸式体验的应用|R主题馆加盟|V R游戏体验馆加盟

《探索未知世界&#xff0c;沉浸式VR体验引领未来》 随着科技的飞速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术正逐渐走进人们的生活&#xff0c;并为我们带来前所未有的沉浸式体验。下面让我们一起探索一下&#xff0c;VR沉浸式体验的应用领域和魅力所在。 虚…

安卓开机启动流程

目录 一、整体框架二、流程代码分析2.1 Boot ROM2.2 Boot Loader2.3 Kernel层Kernel代码部分 2.4 Init进程Init进程代码部分 2.5 zygote进程zygote代码部分 2.6 SystemServer进程SystemServer代码部分 2.7 启动Launcher与SystemUI 三、SystemServices3.1 引导服务3.2 核心服务3…

15个最流行的STL模型修复工具

如果你进行 3D 打印&#xff0c;可能遇到过可怕的“无法打印 STL”问题&#xff1a;你的 STL 文件看起来很棒&#xff0c;但它会导致切片机出现问题或导致奇怪的打印错误或完全失败。 无论确切原因是什么&#xff0c;这些问题的根源通常归结为 STL 用于表示 3D 模型的连接三角形…