深度剖析 Vue3 在浏览器的运行原理

news2025/1/18 11:02:26

上一讲我们学习了 Vue 响应式的大致原理,响应式就是可以把普通的 JavaScript 对象包裹成响应式对象,这样,我们对对象做的修改,响应式都能够监听到,并且执行 effect 内部注册的函数来执行数据修改之后的效果

那今天我就跟你聊一下 Vue 在浏览器里是如何运行的,照例我们还是对着 Vue 3 的源码来学习,不过源码复杂,为了帮助你理解主要逻辑,我会直接把源码简化再演示

好了废话不多说,我们马上开始;前端框架需要处理的最核心的两个流程,就是首次渲染和数据更新后的渲染

首次渲染

我们知道,想要启动一个 Vue 项目,只需要从 Vue 中引入 createApp,传入 App 组件,并且调用 createApp 返回的 App 实例的 mount 方法,就实现了项目的启动;这个时候 Vue 也完成了首次渲染,代码逻辑如下:

所以 createApp 就是项目的初始化渲染入口

这里就有一个看代码的小技巧,分享给你,我们首次查看源码的时候,可以先把一些无用的信息删除,方便自己梳理主体的逻辑。看 Vue 代码,和今天主题无关的无用信息有哪些,COMPAT 代码是用来兼容 Vue 2 的,DEV 代码是用来调试的,我们可以把这些代码删除之后,得到下面的简化版 createApp 源码

再看思路就比较清晰了

我们使用 ensureRenderer 返回的对象去创建 app,并且重写了app.mount 方法;在 mount 方法内部,我们查找 mount 传递的 DOM 元素,并且调用 ensureRenderer 返回的 mount 方法,进行初始化渲染

如下图所示:

之前我们讲过要会 TypeScript,这时你就能感受到 TypeScript 的好处了,现在即使我们不知道 app.mount 是什么逻辑,也能知道这个函数的参数只能是 Element、ShadowRoot 或者 string 三者之一,也就很好理解内部的 normalizeContainer 就是把你传递的参数统一变为浏览器的 DOM 元素,Typescript 类型带来的好处,我们在读源码的时候会一直感受得到

export const createApp = (...args) => {const app = ensureRenderer().createApp(...args);const { mount } = app;// 重写mountapp.mount = (containerOrSelector: Element | ShadowRoot | string): any => {const container = normalizeContainer(containerOrSelector);if (!container) return;const component = app._component;if (!isFunction(component) && !component.render && !component.template) {component.template = container.innerHTML;}container.innerHTML = "";const proxy = mount(container, false, container instanceof SVGElement);if (container instanceof Element) {container.removeAttribute("v-cloak");container.setAttribute("data-v-app", "");}return proxy;};return app;}; 

我们继续深入了解 ensureRenderer 方法,以及 ensureRenderer 方法返回的 createApp方法

这里 ensureRenderer 函数,内部通过 createRenderer 函数,创建了一个浏览器的渲染器,并且缓存了渲染器 renderer,这种使用闭包做缓存的方式,你在日常开发中也可以借鉴这种思路

createRenderer 函数,我们在自定义渲染器那一讲里学到过,传递的 rendererOptions 就是浏览器里面标签的增删改查 API:

// 浏览器dom操作
import { nodeOps } from "./nodeOps"; // 浏览器dom属性更新
import { patchProp } from "./patchProp";
import { createRenderer } from "@vue/runtime-core";
const rendererOptions = extend({ patchProp }, nodeOps);
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer;
function ensureRenderer() {return (renderer ||((renderer = createRenderer < Node),Element | (ShadowRoot > rendererOptions)));
} 

可以看到,createRenderer 函数传递的参数是 nodeOps 和 patchProp 的合并对象

我们继续进入 nodeOps 和 pathProp 也可以看到下面的代码,写了很多方法;通过 ensureRenderer 存储这些操作方法后,createApp 内部就可以脱离具体的渲染平台了,这也是 Vue 3 实现跨端的核心逻辑:

export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = { insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) },remove: child => { const parent = child.parentNode if (parent) { parent.removeChild(child) } },createElement: (tag, isSVG, is, props): Element => { const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : undefined) if (tag === 'select' && props && props.multiple != null) { ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple) }return el },createText: text => doc.createTextNode(text), createComment: text => doc.createComment(text), setText: (node, text) => { node.nodeValue = text },setElementText: (el, text) => { el.textContent = text },parentNode: node => node.parentNode as Element | null, nextSibling: node => node.nextSibling, querySelector: selector => doc.querySelector(selector), ... 
} 

然后我们就需要进入到 rumtime-core 模块去看下 createRenderer 是如何工作的

createRenderer 是调用 baseCreateRenderer 创建的,baseCreateRenderer 函数内部有十几个函数,代码行数合计 2000 行左右,这也是我们学习 Vue 源码最复杂的一个函数了

按前面简化源码的思路,先把工具函数的实现折叠起来,精简之后代码主要逻辑其实很简单

我们一起来看

首先获取了平台上所有的 insert、remove 函数,这些函数都是 nodeOps 传递进来的,然后定义了一些列 patch、mount、unmount 函数,通过名字我们不难猜出,这就是 Vue 中更新、渲染组件的工具函数,比如mountElement 就是渲染 DOM 元素、mountComponent 就是渲染组件 updateComponent 就是更新组件

export function createRenderer<HostNode = RendererNode,HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {return baseCreateRenderer < HostNode, HostElement > (options)
}
function baseCreateRenderer() {const { insert: hostInsert,remove: hostRemove,patchProp: hostPatchProp,createElement: hostCreateElement,createText: hostCreateText,createComment: hostCreateComment,setText: hostSetText,setElementText: hostSetElementText,parentNode: hostParentNode,nextSibling: hostNextSibling,setScopeId: hostSetScopeId = NOOP,cloneNode: hostCloneNode,insertStaticContent: hostInsertStaticContent} = optionsconst patch = () =>... //一个函数const processText = () =>...const processCommentNode = () =>...const processElement = () =>...const mountElement = () =>...const mountChildren = () =>...const patchElement = () =>...const patchBlockChildren = () =>...const patchProps = () =>...const processComponent = () =>...const mountComponent = () =>...const updateComponent = () =>...const setupRenderEffect = () =>...const patchChildren = () =>...const patchKeyedChildren = () =>...const unmount = () =>...const unmountComponent = () =>...const unmountComponent = () =>...const unmountComponent = () =>...const unmountComponent = () =>...const render: RootRenderFunction = (vnode, container, isSVG) => {if (vnode == null) {if (container._vnode) {unmount(container._vnode, null, null, true)}} else {patch(container._vnode || null, vnode, container, null, null, null, isSV }flushPostFlushCbs() container._vnode = vnode} return {render, hydrate, createApp: createAppAPI(render, hydrate)}
} 

整个 createApp 函数的执行逻辑如下图所示:

最后返回的 createApp 方法,实际上是 createAPI 的返回值,并且给 createAPI 传递了render 方法;render 方法内部很简单,就是判断 container 容器上有没有 _vnode 属性,如果有的话就执行 unmout 方法,没有的话就执行 patch 方法,最后把 vnode 信息存储在 container._vnode 上

那 createAppAPI 又做了什么呢?我们继续进入 createAppAPI 源码,看下面的代码;内部创建了一个 app 对象,app 上注册了我们熟悉的 use、component 和 mount 等方法:

export function createAppAPI<HostElement>(render: RootRenderFunction, hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {return function createApp(rootComponent, rootProps = null) {const context = createAppContext()let isMounted = false const app: App = (context.app = {_context: context, _instance: null,use(plugin: Plugin, ...options: any[]) ,component(name: string, component?: Component): any {if (!component) {return context.components[name]}context.components[name] = component return app},directive(name: string, directive?: Directive) mount(rootContainer: HostElement, isHydrate?: boolean, isSVG?: boolean): any {if (!isMounted) {const vnode = createVNode(rootComponent as ConcreteComponent, rootProps)vnode.appContext = context // 核心的逻辑if (isHydrate && hydrate) {hydrate(vnode as VNode<Node, Element>, rootContainer as any)} else {render(vnode, rootContainer, isSVG)}return getExposeProxy(vnode.component!) || vnode.component!.proxy}},provide(key, value) { context.provides[key as string] = value return app }}) return app}
} 

可以看到 mount 内部执行的是传递进来的 render 方法,也就是上面的 render 方法

container 就是我们 app.mount 中传递的 DOM 元素,对 DOM 元素进行处理之后,执行 patch 函数实现整个应用的加载

所以我们的下一个任务就是需要搞清楚 patch 函数的执行逻辑


patch 函数

patch 传递的是 container._vnode,也就是上一次渲染缓存的 vnode、本次渲染组件的vnode,以及容器 container;下面就是 patch 函数的代码,核心代码我添加了注释;其中 n1 是上次渲染的虚拟 DOM,n2 是下次要渲染的虚拟 DOM

首先可以把 n1 和 n2 做一次判断,如果虚拟 DOM 的节点类型不同,就直接 unmount 之前的节点;因为比如之前是 Button 组件,现在要渲染 Container 组件,就没有计算 diff的必要,直接把 Button 组件销毁再渲染 Container 即可

如果 n1 和 n2 类型相同,比如都是 Button 组件或者都是 div 标签,我们需要判断具体的类型再去执行不同的函数,比如 processText、processFragment、processElement 以及 processComponent 等函数;

看第 55 行,这里的 ShapeFlags 用到了位运算的知识,我们后面会通过刷算法题的方式介绍,暂时我们只需要知道,ShapeFlags 可以帮助我们快速判断需要操作的类型就可以了

const patch: PatchFn = ( n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,isSVG = false,slotScopeIds = null,optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren ) => { // 两次虚拟dom完全一样 啥也不用干if (n1 === n2) { return }// 虚拟dom节点类型不一样, unmount老的虚拟dom,并且n1赋值null if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true)n1 = null }// n2是要渲染的虚拟dom,我们获取type,ref和shapeFlag const { type, ref, shapeFlag } = n2 switch (type) { case Text: // 文本processText(n1, n2, container, anchor) break case Comment: // 注释processCommentNode(n1, n2, container, anchor) break case Static: // 静态节点if (n1 == null) {mountStaticNode(n2, container, anchor, isSVG)} else if (__DEV__) {patchStaticNode(n1, n2, container, isSVG)}break case Fragment: processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
break default: // 运运算判断操作类型
if (shapeFlag & ShapeFlags.ELEMENT) { // html标签processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.TELEPORT) {(type as typeof TeleportImpl).process(n1 as TeleportVNode, n2 as TeleportVNode,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds, optimized, internals)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE
) {;(type as typeof SuspenseImpl).process(n1,n2,container,anchor,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized,internals)
}
else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) }
}// set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } 

代码的整体执行逻辑如下图所示:

我们首次渲染的 App 是一个组件,所以要执行的就是 processComponent 方法


processComponent 方法

那我们继续进入到 processComponent 代码内部,看下面的代码。首次渲染的时候,n1就是 null,所以会执行 mountComponent;如果是更新组件的时候,n1 就是上次渲染的 vdom,需要执行 updateComponent

const processComponent = ( n1: VNode | null,n2: VNode,container: RendererElement,anchor: RendererNode | null,parentComponent: ComponentInternalInstance | null,parentSuspense: SuspenseBoundary | null,isSVG: boolean,slotScopeIds: string[] | null,optimized: boolean) => {n2.slotScopeIds = slotScopeIdsif (n1 == null) {if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {; (parentComponent!.ctx as KeepAliveContext).activate(n2, container, anchor, isSVG, optimized)}else {mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)}} else {updateComponent(n1, n2, optimized)}
} 

updateComponent 是虚拟 DOM 的逻辑,我们会在下一讲详细剖析,这一讲主要讲首次渲染的过程

所以我们进入 mountComponent 函数中,可以看到 mountComponent 函数内部会对组件的类型进行一系列的判断,还有一些对 Vue 2 的兼容代码,核心的渲染逻辑就是 setupComponent 函数和 setupRenderEffect 函数

import { setupComponent } from './component'
const mountComponent: MountComponentFn = () => {// 2.x compat may pre-creaate the component instance before actually // mounting const compatMountInstance = __COMPAT__ && initialVNode.isCompatRoot && initialVNode.componentconst instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)) // resolve props and slots for setup context if (!(__COMPAT__ && compatMountInstance)) {setupComponent(instance)} (instance,initialVNode,container,anchor,parentSuspense,isSVG, optimized )
if (__DEV__) { popWarningContext() endMeasure(instance, `mount`) }
} 

setupComponent 和 setupRenderEffect,它俩又做了点什么呢?可以参考下面的示意图这两个实现组件首次渲染的函数:


setupComponent

首先看 setupComponent,要完成的就是执行我们写的 setup 函数

可以看到,内部先初始化了 props 和 slots,并且执行 setupStatefulComponent 创建组件,而这个函数内部从 component 中获取 setup 属性,也就是 script setup 内部实现的函数,就进入到我们组件内部的reactive、ref 等函数实现的逻辑了

export function setupComponent( instance: ComponentInternalInstance,isSSR = false) { isInSSRComponentSetup = isSSR const { props, children } = instance.vnode const isStateful = isStatefulComponent(instance) initProps(instance, props, isStateful, isSSR) initSlots(instance, children) const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined isInSSRComponentSetup = false return setupResult } function setupStatefulComponent(instance: ComponentInternalInstance, isSSR: boolean) {const Component = instance.type as ComponentOptions // 执行setup const { setup } = Component if (setup) {const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null)setCurrentInstance(instance) pauseTracking()const setupResult = callWithErrorHandling(setup,instance,ErrorCodes.SETUP_FUNCTION,[instance.props, setupContext])if (isPromise(setupResult)) {setupResult.then(unsetCurrentInstance, unsetCurrentInstance)} else {handleSetupResult(instance, setupResult, isSSR)}} else {finishComponentSetup(instance, isSSR)}}
export function callWithErrorHandling( fn: Function,instance: ComponentInternalInstance | null,type: ErrorTypes, args?: unknown[] ) {let restry { res = args ? fn(...args) : fn() } catch (err) { handleError(err, instance, type) } return res
} 

setupRenderEffect

另一个 setupRenderEffect 函数,就是为了后续数据修改注册的函数,我们先梳理一下核心的实现逻辑

组件首次加载会调用 patch 函数去初始化子组件,注意 setupRenderEffect 本身就是在 patch 函数内部执行的,所以这里就会递归整个虚拟 DOM 树,然后触发生命周期 mounted,完成这个组件的初始化

页面首次更新结束后,setupRenderEffect 不仅实现了组件的递归渲染,还注册了组件的更新机制

在下面的核心代码中,我们通过 ReactiveEffect 创建了 effect 函数,这个概念上一讲我们手写过,然后执行 instance.update 赋值为 effect.run 方法,这样结合 setup 内部的 ref 和 reactive 绑定的数据,数据修改之后,就会触发 update 方法的执行,内部就会 componentUpdateFn,内部进行递归的 patch 调用执行每个组件内部的 update 方法实现组件的更新

if (!instance.isMounted) {patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
} else {// updateComponent 
}
// create reactive effect for rendering 
const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(instance.update),instance.scope // track it in component's effect scope 
)
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update.id = instance.uid
update() 

这样我们就实现了整个 Vue 的渲染和更新流程

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

公司建网站多少钱?【网站多少钱】

很多公司成立初期会选择建一个公司网站&#xff0c;那么首先就会考虑公司建网站多少钱的问题。公司建网站多少钱会受到建网站方式的影响&#xff0c;不同方式费用不同&#xff0c;那么下面就说说公司建网站多少钱。 1、公司有团队&#xff0c;自己建 公司有技术团队的情况&am…

【FLASH存储器系列九】ONFI数据接口详述之二

目录 1.1 总线状态 1.2 ZQ校准 1.3 CE_n引脚缩减机制 1.4 差分信号 1.5 Warm up周期 1.6 从SDR到NV-LPDDR4的技术发展总结 1.1 总线状态 SDR 的总线状态&#xff1a; NV-DDR总线状态&#xff1a;值为11b的ALE/CLE 用于数据传输。总线状态在CLK的上升沿开始&#xff0c;持续…

最高效“双11”背后:圆通更不一样了

每年的“双11”都是一场看不见硝烟的战争&#xff0c;有人守着直播间抢货&#xff0c;也有人守着电脑追发货。 河南商丘大学城附近的某电商老板陈泽宇就是后者中的一员。每年“双11”他都高度紧张&#xff0c;提前许多天就开始备货&#xff0c;每天既要紧盯前台销量数字&#…

Leetcode 学习记录 数组与字符串

基础不牢&#xff0c;地动山摇。这句话就是描述我现在的阶段&#xff0c;虽然这些天在csdn的练习上进展还比较顺利&#xff0c;但是内心还是没有底的。实话说&#xff0c;csdn的练习题和leetcode上的比起来&#xff0c;还是真的相差很远。可能是不和口味吧。我还是比较喜欢Leet…

厉害了我们的“中国制造”,新能源“智造”强大到你想象不到

“中国制造”是近日的一大“热词”。 “中国制造”的新能源汽车花式“出海”&#xff0c;产销量连续7年位居全球首位&#xff1b;“中国制造”的过冬八件套反向海淘&#xff0c;订单暴增&#xff1b;“中国制造”的高铁首次全产业链“出海”…… 显而易见&#xff0c;当前是中国…

Unity导表工具Luban插件的数据加载原理与优化

文章目录1. 配置Luban并测试2. Luban 数据加载原理3. 根据工程进行懒加载优化3.1 增加公共接口与接口实现方法3.2 外部提供获取数据的方法3.3 对Luban中的导表模板进行修改4. 优化后测试如果初入坑&#xff0c;Luban配置可以参考宝鱼大佬的视频教程 强力配置工具luban使用教程【…

消除卡顿 mac MATLAB2022b m1/m2原生下载安装 教程

目录 1、版本说明 2、下载链接 2.1、百度网盘链接 2.2、官网下载链接 3、安装 1、版本说明 之前苦于安装matlab 2022b的Intel Rosetta转译版本&#xff0c;真的是明显感受到了卡顿&#xff0c;今天终于等到了2022b的原生版本&#xff0c;安装后占用10.09G内存。 ​​​​…

0096 克鲁斯卡尔算法,迪杰斯特拉算法

/* * 克鲁斯卡尔算法 * 1.用来求加权连通图的最小生成树的算法 * 2.思想&#xff1a;按照权值从小到大的顺序&#xff0c;选择n-1条边&#xff0c;并保证这n-1条边不构成回路 * 3.先构造一个只含n个顶点的森林&#xff0c;依权值从小到大从连通网中选择边加入到森林中 * …

web自动化测试-webdriver实现

webdriver实现基本语法1.Select下拉列表1.1下拉列表单选1.2下拉列表多选2.非Select下拉列表3.文件上传1.1input单个文件上传1.2input多个文件上传1.3非input文件上传1.Select下拉列表 1.1下拉列表单选 from selenium import webdriver from selenium.webdriver.support.ui imp…

GPU驱动及CUDA安装流程介绍

安装前准备工作 确认GPU型号和操作系统版本   准备gpu驱动和CUDA软件包   在nvidia官网进行驱动包下载   GPU驱动下载链接    Linux系统均选择 Linux 64-bit、CUDA Toolkit选择最新版本   注意该版本一定要大于等于cuda软件的版本。   选择CUDA及GPU驱动版本&#…

m基于matlab的TDSCDMA系统性能仿真

目录 1.算法概述 2.仿真效果预览 3.核心MATLAB代码预览 4.完整MATLAB程序 1.算法概述 TD-SCDMA的中文含义为时分复用同步码分多址接入&#xff0c;是由中国第一次提出、在无线传输技术(RTT)的基础上完成并已正式成为被ITU接纳的国际移动通信标准。这是中国移动通信界的一次…

解读ESSumm: Extractive Speech Summarization from Untranscribed Meeting

ESSumm: Extractive Speech Summarization from Untranscribed Meeting 从非转录会议中提取语音摘要&#xff08;非转录&#xff1a;原音频&#xff0c;没有转化为文本或者其他格式&#xff09; 论文地址 https://arxiv.org/abs/2209.06913 摘要 在本文中&#xff0c;我们为…

JS中的迭代器、可迭代对象、生成器

迭代器 迭代器&#xff08;iterator&#xff09;&#xff0c;是使用户在容器对象&#xff08;container&#xff0c;例如链表或数组&#xff09;上可以遍历访问的对象&#xff0c;使用该接口无需关心容器对象的内部实现细节。 从上面定义上来看&#xff0c;迭代器是帮助我们对…

SpringMVC 源码学习 返回值处理

SpringMVC中对返回值的数据基本分为两类&#xff1a; 1、响应数据 2、响应页面 一、响应数据 响应数据大多数都是将返回值的格式转换为JSON格式然后展示在页面或者保存i起来。 第一步&#xff1a;在SpringBoot中需要引入json场景 <dependency><groupId>org.sprin…

如何设计存储架构

步骤 步骤1&#xff1a;估算性能需求 任务 基于具体的业务场景来估算性能需求&#xff0c;包括存储量、读写性能等 挑战 不知道如何估算担心估算不准 步骤2&#xff1a;选择存储系统 任务 根据技术储备、方案优缺点选择合适的存储系统 挑战 不知道有哪些存储系统知道…

UG NX二次开发(C#)-UI Styler-批量选择点

1、前言 在设计UG NX的对话框界面时,有时需要选择点,在UI Styler编辑器中已经设置好了可以指定点选择按钮,这个点选择对话框是单选,不是多选的,如果想选择多个点,那么可以采用对象选择按钮,本文介绍下选择点的操作。 2、指定点按钮 2.1 设计UI Styler UG NX的UI Sty…

vscode中Emmet语法的使用

每篇博文的浪漫主义 【镰仓旅拍|落日绝景 极致画质|沉浸式旅行 FX3】 https://www.bilibili.com/video/BV1jg411Y7vC/?share_sourcecopy_web&vd_source385ba0043075be7c24c4aeb4aaa73352 镰仓旅拍|落日绝景 极致画质|沉浸式旅行 FX31.1快速生成HTML结构语法 生成标签直接…

想要彻底卸载Mac应用程序,还得要用这些方法才行

Mac电脑如果有太多无用的应用程序&#xff0c;很有可能会拖垮Mac系统的运行速度。因此&#xff0c;卸载电脑中无用的软件是优化Mac系统运行速度的最佳方式之一。Mac删除应用程序特别简单&#xff0c;长点击应用点击x&#xff0c;或是直接将应用拖进废纸篓。但是有一些应用长按没…

Qt——(详细)“项目在Debug构建环境下能运行而在Release构建环境下不能运行”解决方案之一,以及 禁用(黄色)警告

系列文章目录 提示&#xff1a; 文章目录系列文章目录前言环境一、问题准备工作——为了在Release环境下可以进行断点调试分析二、解决1、根据需求&#xff0c;对函数类型进行更改2、根据需求&#xff0c;在函数内添加“return [int]”延伸——“禁用警告”消除 变量 的“黄色感…

现代修谱,如何看待支系单飞的现象?

族谱与支谱、房谱的区别 现代修谱&#xff0c;修的是什么谱&#xff0c;你知道吗&#xff1f;其实现代修谱的种类有很多种&#xff0c;有支谱、房谱、族谱、宗谱、统谱、通谱等等&#xff0c;而这些在生活中都被我们简称为家谱。 不过在现代修谱里&#xff0c;宗谱、统谱、通谱…