一文看完Vue3的渲染过程

news2024/9/23 23:33:41

Vue3官网中有下面这样一张图,基本展现出了Vue3的渲染原理:

render pipeline

本文会从源码角度来草率的看一下Vue3的运行全流程,旨在加深对上图的理解,从下面这个很简单的使用示例开始:

import { createApp, ref } from "vue";

createApp({
  template: `
        <div class="card">
            <button type="button" @click="count++">count is {{ count }}</button>
        </div>
    `,
  setup() {
    const count = ref(0);
    return {
      count,
    };
  },
}).mount("#app");

通过createApp方法创建应用实例,传了一个组件的选项对象,包括模板template、组合式 API 的入口setup函数,在setup函数里使用ref创建了一个响应式数据,然后return给模板使用,最后调用实例的mount方法将模板渲染到idapp的元素内。后续只要修改count的值页面就会自动刷新,麻雀虽小,但是也代表了Vue的核心。

首先调用了createApp方法:

const createApp = ((...args) => {
    const app = createRenderer(rendererOptions).createApp(...args);
    return app;
});

通过createRenderer创建了一个渲染器,rendererOptions是一个对象,上面主要是操作DOM的方法:

{
    insert: (child, parent, anchor) => {
        parent.insertBefore(child, anchor || null);
    },
    //...
}

这么做主要是方便跨平台,比如在其他非浏览器环境,可以替换成对应的节点操作方法。

function createRenderer(options) {
    return baseCreateRenderer(options);
}

function baseCreateRenderer(options, createHydrationFns) {
    // ...
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    };
}

baseCreateRenderer方法非常长,包含了渲染器的所有方法,比如mountpatch等,createApp是通过createAppAPI方法调用返回的:

function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        if (!isFunction(rootComponent)) {
            rootComponent = Object.assign({}, rootComponent);
        }
        const context = createAppContext();
        let isMounted = false;
        const app = (context.app = {
            _uid: uid$1++,
            _component: rootComponent,
            _props: rootProps,
            _container: null,
            _context: context,
            _instance: null,
            version,
            get config() {},
            set config() {},
            use(){},
            mixin(){},
            component(){},
            directive(){},
            mount(){},
            unmount(){},
            provide(){}
        });
        return app;
    }
}

这个就是最终的createApp方法,所谓的应用实例app其实就是一个对象,我们传进去的组件选项作为根组件存储在_component属性上,另外还可以看到应用实例提供的一些方法,比如注册插件的use方法,挂载实例的mount方法等。

context其实也是一个普通对象:

function createAppContext() {
    return {
        app: null,
        config: {
            isNativeTag: NO,
            performance: false,
            globalProperties: {},
            optionMergeStrategies: {},
            errorHandler: undefined,
            warnHandler: undefined,
            compilerOptions: {}
        },
        mixins: [],
        components: {},
        directives: {},
        provides: Object.create(null),
        optionsCache: new WeakMap(),
        propsCache: new WeakMap(),
        emitsCache: new WeakMap()
    };
}

这个上下文对象会保存在应用实例和根VNode上,可能是后续渲染时会用到。

接下来看一下创建实例后挂载的mount方法:

mount(rootContainer, isHydrate, isSVG) {
    // 没有挂载过
    if (!isMounted) {
        // 创建虚拟DOM
        const vnode = createVNode(rootComponent, rootProps);
        vnode.appContext = context;
        // 渲染
        render(vnode, rootContainer, isSVG);
        isMounted = true;
        // 实例和容器元素互相关联
        app._container = rootContainer;
        rootContainer.__vue_app__ = app;
        // 返回根组件的实例
        return getExposeProxy(vnode.component) || vnode.component.proxy;
    }
}

主要就是做了两件事,创建虚拟DOM,然后渲染。

createVNode方法:

const createVNode = _createVNode;
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
    const shapeFlag = isString(type)
        ? 1 /* ShapeFlags.ELEMENT */
        : isSuspense(type)
            ? 128 /* ShapeFlags.SUSPENSE */
            : isTeleport(type)
                ? 64 /* ShapeFlags.TELEPORT */
                : isObject(type)
                    ? 4 /* ShapeFlags.STATEFUL_COMPONENT */
                    : isFunction(type)
                        ? 2 /* ShapeFlags.FUNCTIONAL_COMPONENT */
                        : 0;
    return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true);
}

createVNode方法会根据组件的类型生成一个标志,后续会通过这个标志做一些优化之类的处理。我们传的是一个组件选项,也就是一个普通对象,shapeFlag的值为4

然后调用了createBaseVNode方法:

function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ShapeFlags.ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) {
    const vnode = {
        __v_isVNode: true,
        __v_skip: true,
        type,
        props,
        key: props && normalizeKey(props),
        ref: props && normalizeRef(props),
        scopeId: currentScopeId,
        slotScopeIds: null,
        children,
        component: null,
        suspense: null,
        ssContent: null,
        ssFallback: null,
        dirs: null,
        transition: null,
        el: null,
        anchor: null,
        target: null,
        targetAnchor: null,
        staticCount: 0,
        shapeFlag,
        patchFlag,
        dynamicProps,
        dynamicChildren: null,
        appContext: null,
        ctx: currentRenderingInstance
    };
    return vnode;
}

可以看到返回的虚拟DOM也是一个普通对象,我们传进去的组件选项会存储在type属性上。

虚拟DOM创建完后就会调用render方法将虚拟DOM渲染为实际的DOM节点,render方法是通过参数传给createAppAPI的:

const render = (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, isSVG);
    }
    flushPreFlushCbs();
    flushPostFlushCbs();
    container._vnode = vnode;
};

如果要渲染的新VNode不存在,那么从容器元素上获取之前VNode进行卸载,否则调用patch方法进行打补丁,如果是首次渲染,container._vnode不存在,那么直接将新VNode渲染为DOM元素即可,否则会对比新旧VNode,使用diff算法进行打补丁,Vue2中使用的是双端diff算法,Vue3中使用的是快速diff算法。

打补丁结束后清空了两个回调队列,可以看到事件队列还分为前后两个,那么我们常用的nextTick方法注册的回调在哪个队列呢,实际上,两个都不在:

const resolvedPromise = Promise.resolve();
let currentFlushPromise = null;

function nextTick(fn) {
    const p = currentFlushPromise || resolvedPromise;
    return fn ? p.then(this ? fn.bind(this) : fn) : p;
}

Promise.resolve()方法会创建一个Resolved状态的Promise对象。

nextTick方法就是这么简单,如果currentFlushPromise有值,那么使用这个Promise注册回调,否则使用默认的resolvedPromise将回调放到微任务队列。

currentFlushPromise会在调用queueFlush方法时赋值,也就是生成一个新的Promise对象:

function queueFlush() {
    if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvedPromise.then(flushJobs);
    }
}

flushJobs和前面的flushPreFlushCbs方法里冲刷的都是queue队列,而flushPostFlushCbs方法里冲刷的是pendingPostFlushCbs队列,flushJobs方法在冲刷完queue队列后才会冲刷pendingPostFlushCbs队列。而如果是冲刷中调用nextTick添加的回调会在这两个队列都清空后才会执行。

扯远了,回到render方法,接下来看看render方法里调用的patch方法:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = (process.env.NODE_ENV !== 'production') && isHmrUpdating ? false : !!n2.dynamicChildren) => {
    	// 新旧VNode相同直接返回
        if (n1 === n2) {
            return;
        }
    	// 如果新旧VNode的类型不同,那么也不需要打补丁了,直接卸载旧的,挂载新的
        if (n1 && !isSameVNodeType(n1, n2)) {
            anchor = getNextHostNode(n1);
            unmount(n1, parentComponent, parentSuspense, true);
            n1 = null;
        }
        const { type, ref, shapeFlag } = n2;
        switch (type) {
              case Text:
                // ...
                break;
              // ...
              default:
                // ...
                else if (shapeFlag & 6 /* ShapeFlags.COMPONENT */) {
                    processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
                }
                // ...
        }
}

patch方法就是用来打补丁更新实际DOM的,switch里面根据VNode的类型不同做的处理也不同,因为我们的例子传的是一个组件选项对象,所以会走processComponent处理分支:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    // 如果旧的VNode不存在,那么调用挂载方法
    if (n1 == null) {
        mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
    }
    // 新旧都存在,那么进行更新操作
    else {
        updateComponent(n1, n2, optimized);
    }
};

根据是否存在旧的VNode判断是调用挂载方法还是更新方法,先看mountComponent方法:

const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
    const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
    setupComponent(instance);
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
}

首先调用createComponentInstance方法创建组件实例,返回的其实也是一个普通对象:

function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        uid: uid++,
        vnode,
        type,
        parent,
        appContext,
        // 还有非常多属性
        // ...
    }
    return instance;
}

然后调用了setupComponent方法:

function setupComponent(instance, isSSR = false) {
    const { props, children } = instance.vnode;
    const isStateful = instance.vnode.shapeFlag & 4;
    initProps(instance, props, isStateful, isSSR);
    initSlots(instance, children);
    const setupResult = isStateful
        ? setupStatefulComponent(instance, isSSR)
        : undefined;
    return setupResult;
}

初始化propsslots,然后如果shapeFlag4会调用setupStatefulComponent方法,前面说了我们传的组件选项对应的shapeFlag就是4,所以会走setupStatefulComponent方法:

function setupStatefulComponent(instance, isSSR) {
    const { setup } = Component;
    if (setup) {
        const setupResult = callWithErrorHandling(setup, instance, 0, [instance.props, setupContext]);
        handleSetupResult(instance, setupResult, isSSR);
    }
}

在这个方法里会调用组件选项的setup方法,这个函数中返回的对象会暴露给模板和组件实例,看一下handleSetupResult方法:

function handleSetupResult(instance, setupResult, isSSR) {
    if (isFunction(setupResult)) {
        instance.render = setupResult;
    } else if (isObject(setupResult)) {
        instance.setupState = proxyRefs(setupResult);
    }
    finishComponentSetup(instance, isSSR);
}

如果setup返回的是一个函数,那么这个函数会直接被作为渲染函数。否则如果返回的是一个对象,会使用proxyRefs将这个对象转为Proxy代理的响应式对象。

最后又调用了finishComponentSetup方法:

function finishComponentSetup(instance, isSSR) {
    const Component = instance.type;
    if (!instance.render) {
        if (!isSSR && compile && !Component.render) {
            const template = Component.template ||
                  resolveMergedOptions(instance).template;
            if (template) {
                const { isCustomElement, compilerOptions } = instance.appContext.config;
                const { delimiters, compilerOptions: componentCompilerOptions } = Component;
                const finalCompilerOptions = extend(extend({
                    isCustomElement,
                    delimiters
                }, compilerOptions), componentCompilerOptions);
                Component.render = compile(template, finalCompilerOptions);
            }
        }
        instance.render = (Component.render || NOOP);
    }
}

这个函数主要是判断组件是否存在渲染函数render,如果不存在则判断是否存在template选项,我们传的组件选项显然是没有render属性,而是传的模板template,所以会使用compile方法来将模板编译成渲染函数。

回到mountComponent方法,最后调用了setupRenderEffect,这个方法很重要:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
    // 组件更新方法
    const componentUpdateFn = () => {}
    // 创建一个effect
    const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance.scope));
    // 调用effect的run方法执行componentUpdateFn方法
    const update = (instance.update = () => effect.run());
    update();
}

这一步就涉及到Vue3的响应式原理了,核心就是使用Proxy拦截数据,然后在属性读取时将属性和读取该属性的函数(称为副作用函数)关联起来,然后在更新该属性时取出该属性关联的副作用函数出来执行,详细的内容网上已经有非常多的文章了,有兴趣的可以自己搜一搜,或者直接看源码也是可以的。

简化后的ReactiveEffect类就是这样的:

let activeEffect;
class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        this.fn = fn;
    }
    run() {
        activeEffect = this;
        try {
            return this.fn();
        } finally {
            activeEffect = null
        }
    } 
}

执行它的run方法时会把自身赋值给全局的activeEffect变量,然后执行副作用函数时如果读取了Proxy代理后的对象的某个属性时就会将对象、属性和这个ReactiveEffect示例关联存储起来,如果属性发生改变,会取出关联的ReactiveEffect实例,执行它的run方法,达到自动更新的目的。

我们使用的是ref方法创建的数据,ref方法返回的响应式数据虽然不是通过Proxy代理的,但是读取修改操作同样是会被拦截的,和Proxy代理的数据拦截时做的事情是一样的。

接下来看看传给它的组件更新方法componentUpdateFn

const componentUpdateFn = () => {
    // 组件没有挂载过
    if (!instance.isMounted) {
        const subTree = (instance.subTree = renderComponentRoot(instance));
        patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
        initialVNode.el = subTree.el;
        instance.isMounted = true;
    } else {// 组件已经挂载过
        const nextTree = renderComponentRoot(instance);
        patch(prevTree, nextTree, hostParentNode(prevTree.el), getNextHostNode(prevTree), instance, parentSuspense, isSVG);
        next.el = nextTree.el;
    }
}

组件无论是首次挂载,还是更新,做的事情核心是一样的,先调用renderComponentRoot方法生成组件模板的虚拟DOM,然后调用patch方法打补丁。

function renderComponentRoot(instance) {
    const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx, inheritAttrs } = instance;
    let result = render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx)
    return result
}

renderComponentRoot核心就是调用组件的渲染函数render方法生成组件模板的虚拟DOM,然后扔给patch方法更新就好了。

看完了mountComponent方法,再来看看updateComponent方法:

const updateComponent = (n1, n2, optimized) => {
    const instance = (n2.component = n1.component);
    if (shouldUpdateComponent(n1, n2, optimized)) {
        // 需要更新
        instance.next = n2;
        instance.update();
    }else {
        // 不需要更新
        n2.el = n1.el;
        instance.vnode = n2;
    }
}

先调用shouldUpdateComponent方法判断组件是否需要更新,大致是通过是否存在过渡效果、是否存在动态slotsprops是否发生改变、子节点是否发改变等来判断。

如果需要更新,那么会执行instance.update方法,这个方法就是前面setupRenderEffect方法里保存的effect.run方法,所以最终执行的也是componentUpdateFn方法。

到这里,从我们创建实例到页面渲染,再到更新的全流程就讲完了,总结一下,大致就是:

1.每个Vue组件都需要产出一份虚拟DOM,也就是组件的render函数的返回值,render函数你可以直接手写,也可以通过template传递模板字符串,由Vue内部来编译成渲染函数,平常我们开发时写的Vue单文件,最终也会编译成普通的Vue组件选项对象;

2.render函数会作为副作用函数执行,也就是如果在模板中使用到了响应式数据(所谓响应式数据就是能拦截到它的各种读取、修改操作),那么响应式数据和属性会与render函数关联起来,那么当响应式数据被修改以后,就能找到依赖它的render函数,那么就可以通知依赖的组件进行更新;

2.有了虚拟DOM之后,Vue内部的渲染器就能将它渲染成真实的DOM,如果是更新的情况,也就是存在新旧两个虚拟DOM,那么Vue会通过比较,必要时会使用diff算法进行高效的更新真实DOM

所以只要你实现一个渲染器,能将虚拟DOM渲染成真实DOM,并且能高效的根据新旧虚拟DOM对比完成更新,再实现一个编译器,能将模板编译成渲染函数,最后再基于Proxy实现一个响应系统就可以实现一个Vue3了,是不是很简单,心动不如行动,下一个框架等你来创造!

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

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

相关文章

Python3 列表与元组 | 菜鸟教程(六)

目录 一、Python3 列表 &#xff08;一&#xff09;简介相关 1、序列是 Python 中最基本的数据结构。 2、序列中的每个值都有对应的位置值&#xff0c;称之为索引&#xff0c;第一个索引是 0&#xff0c;第二个索引是 1&#xff0c;依此类推。 3、Python 有 6 个序列的内置…

Qt编写手机版本视频播放器和Onvif工具(可云台和录像)

一、前言 用Qtffmpeg写播放器很多人有疑问&#xff0c;为何不用Qt自己的多媒体框架来写&#xff0c;最重要的原因是Qt自带的目前都依赖具体的本地解码器&#xff0c;如果解码器不支持&#xff0c;那就是歇菜的&#xff0c;最多支持个MP4格式&#xff0c;而且在手机上也都是支持…

有效的括号

数据结构与算法应用往往隐藏在我们看不到的地方 20. 有效的括号 力扣题目链接 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&#xff0c;‘[’&#xff0c;‘]’ 的字符串&#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括…

【Python 随练】打印楼梯与笑脸

题目&#xff1a; 打印楼梯&#xff0c;并在楼梯上方打印两个笑脸 简介&#xff1a; 在本篇博客中&#xff0c;我们将使用 Python 代码打印一个楼梯&#xff0c;并在楼梯上方打印两个笑脸。我们将给出问题的解析&#xff0c;并提供一个完整的代码示例来实现这个效果。 问题…

多目标优化算法:多目标浣熊优化算法(multi-objective Coati Optimization Algorithm,MOCOA)

一、浣熊优化算法COA 浣熊优化算法&#xff08;Coati Optimization Algorithm&#xff0c;COA&#xff09;由Dehghani Mohammad等人于2022年提出的模拟浣熊狩猎行为的优化算法&#xff0c;该算法具有进化能力强&#xff0c;收敛速度快&#xff0c;收敛精度高等特点。 COA具体…

【算法与数据结构】454、LeetCode 四数相加 II

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;这道题不仅要计算元素的和为0&#xff0c;还要计算元素和为零出现的次数&#xff0c;说明这道题map比较…

【Python 随练】输出国际象棋棋盘

题目&#xff1a; 输出国际象棋棋盘 简介&#xff1a; 在本篇博客中&#xff0c;我们将使用 Python 代码输出国际象棋棋盘。国际象棋棋盘是一个8x8的方格矩阵&#xff0c;交替使用黑色和白色方格。我们将给出问题的解析&#xff0c;并提供一个完整的代码示例来生成这个棋盘。…

C语言之指针详解(1)

目录 本章重点 1. 字符指针 2. 数组指针 3. 指针数组 4. 数组传参和指针传参 5. 函数指针 6. 函数指针数组 7. 指向函数指针数组的指针 8. 回调函数 9. 指针和数组面试题的解析 大家对比前面C语言之指针初阶来看&#xff0c;指针进阶明显看起来难度大了很多&#xff…

TLS协议详解,一文带你了解TLS协议

前言 TLS&#xff08;Transport Layer Security&#xff09;是一种安全协议&#xff0c;用于保护网络通信的安全性和隐私性。它是SSL&#xff08;Secure Sockets Layer&#xff09;的后继者&#xff0c;用于在互联网上建立安全的通信连接。本文将介绍TLS的概论、工作原理、发展…

计算机网络——物理层-数据通信的基础知识

物理层的基本概念 在计算机网络中&#xff0c;物理层是网络协议栈中的第一层&#xff0c;负责处理网络中传输数据的物理介质和信号传输的细节。它定义了传输数据的电气、光学和机械特性&#xff0c;以及物理连接的规范和接口标准。 物理层的主要任务是将比特流&#xff08;0和…

C++之AVL树

目录 一.介绍二.简单实现AVL树1. 基本框架2. 插入结点(Insert)a. 更新平衡因子b. 左单旋c. 右单旋d. 左右双旋e. 右左双旋 3. 删除节点(Erase)a. 更新平衡因子b. 旋转c. 代码 4. 测试 一.介绍 作为对二叉搜索树的优化版本。AVL树是由俄罗斯的两位数学家G.M.Adelson-Velskii和E.…

Python 3 基本语法与基本数据类型 | 菜鸟教程(二)

目录 一、Python3 基础语法 &#xff08;一&#xff09;编码 &#xff08;二&#xff09;标识符 &#xff08;三&#xff09;python保留字 &#xff08;四&#xff09;注释 ​&#xff08;五&#xff09;行与缩进 &#xff08;六&#xff09;多行语句 &#xff08;七&am…

操作系统复习笔记3

1、条件变量和互斥锁 条件变量一般和互斥锁一起使用&#xff0c;来弥补互斥锁的不足。总得来说&#xff0c;互斥锁用来规范线程对共享数据的竞争使用&#xff0c;条件变量用来协调各个线程合作完成任务。 2、enum枚举类型 enum typeName { valueName1, valueName2, valueName3…

Workerman在线客服系统源码 附搭建文档

Workerman在线客服系统源码 模块化开发 强大的一键生成功能极速简化你的开发流程&#xff0c;加快你的项目开发 响应式布局 自动适配&#xff0c;无需要担心兼容性问题 完善的权限管理 自由分配子级权限、一个管理员司同时属于多个组别 通用的会员和API模块 共用同一账…

【MySQL多表查询】:让你的数据检索更高效

前言 ✨欢迎来到小K的MySQL专栏&#xff0c;本节将为大家带来MySQL中多表查询相关知识的讲解 目录 前言一、多表关系二、多表查询1、交叉连接2、内连接3、外连接 三、集合运算四、七种JOINS实现五、多表查询练习六、总结 一、多表关系 ✨项目开发中&#xff0c;在进行数据库表结…

6月人工智能论文推荐

Prompt Space Optimizing Few-shot Reasoning Success with Large Language Models https://arxiv.org/abs/2306.03799 Prompt engineering 是通过提供明确和具体的指令来增强大型语言模型(llm)能力的基本技术。它使LLM能够在各种任务中脱颖而出&#xff0c;例如算术推理、问…

列表、表格、表单

day02&#xff1a;列表、表格、表单 目标&#xff1a;掌握嵌套关系标签的写法&#xff0c;使用列表标签布局网页 01-列表 作用&#xff1a;布局内容排列整齐的区域。 列表分类&#xff1a;无序列表、有序列表、定义列表。 无序列表 作用&#xff1a;布局排列整齐的不需要规…

Bean 的生命周期

观前提示:本篇博客演示使用的 IDEA 版本为2021.3.3版本,使用的是Java8(又名jdk1.8) 电脑使用的操作系统版本为 Windows 10 目录 Spring 的执行流程 1. 启动容器 2. 根据配置完成 Bean 的初始化 3. 注册 Bean 对象到容器中 4. 装配 Bean 的属性 Bean 的生命周期 Bean 的…

掌握Python的X篇_2_Python的安装

掌握Python的X篇_2_Python的安装 1. 软件安装包下载1.1 Python版本的区别1.2 同一版本不同安装包的区别 2. 安装过程3. 验证是否安装成功 1. 软件安装包下载 下载地址&#xff1a;https://www.python.org/downloads/ 可以看到最新版本和历史版本 1.1 Python版本的区别 就像…

express的使用(五) 简单的使用mongodb

原文链接 express的使用(五) 简单的使用mongodb 不要脸的求关注&#xff0c;希望能让大家批评我的不足点&#xff0c;一键三连最好了 看前提示 本篇主要描述关于express中使用mongodb以及mongodb的简单介绍还有初步使用&#xff0c;不涉及任何常用的语法&#xff0c;不建议有…