这张图,把 vue3 的源码讲清楚了!!!

开发 前端
我们需要了解一些关于前端框架 的一些基础的概念。框架的设计原则,开发者开发体验原则。以此来帮助大家解决一些固有的疑惑,从而揭开 vue 神秘的面纱。

Hello,大家好,我是 Sunday。

最近一位同学在学习 vue3 源码的时候,把 vue 3 的大部分核心逻辑都整理到了脑图之中:

图片

整理的内容非常详细。应该会对所有还在学习 vue3 源码的同学都有所帮助。所以分享给大家!

那么今天,咱们就借助这位同学的脑图作为契机,来为大家捋一捋 【Vue3 框架设计原理】(看完设计原理之后,再看脑图收获会更大哦~)

01:前言

在了解 Vue3 框架设计之前,我们需要做两件事情,而这两件事情也是今天的主要内容。

  1. 我们需要同步并明确一些词汇的概念,比如:声明式、命令式、运行时、编译时...。这些词汇将会在后面的框架设计中被经常涉及到。
  2. 我们需要了解一些关于 前端框架 的一些基础的概念。框架的设计原则,开发者开发体验原则。以此来帮助大家解决一些固有的疑惑,从而揭开 vue 神秘的面纱。

那么准备好了?

我们开始吧!

02:编程范式之命令式编程

针对于目前的前端开发而言,主要存在两种 编程范式:

  1. 命令式编程
  2. 声明式编程

这两种 范式 一般是相对来去说的。

命令式

那么首先我们先来说什么叫做 命令式。

具体例子:

张三的妈妈让张三去买酱油。

那么张三怎么做的呢?

  1. 张三拿起钱
  2. 打开门
  3. 下了楼
  4. 到商店
  5. 拿钱买酱油
  6. 回到家

以上的流程详细的描述了,张三在买酱油的过程中,每一步都做了什么。那么这样一种:详细描述做事过程 的方式就可以被叫做 命令式。

那么如果把这样的方式放到具体的代码实现之中,又应该怎么做呢?

我们来看以下这样的一个事情:

在指定的 div 中展示 “hello world”

那么如果想要完成这样的事情,通过命令式的方式我们如何实现呢?

我们知道命令式的核心在于:关注过程。

所以,以上事情通过命令式实现则可得出以下逻辑与代码:

// 1. 获取到指定的 div
const divEle = document.querySelector('#app')
// 2. 为该 div 设置 innerHTML 为 hello world
divEle.innerHTML = 'hello world'

该代码虽然只有两步,但是它清楚的描述了:完成这件事情,所需要经历的过程

那么假如我们所做的事情,变得更加复杂了,则整个过程也会变得更加复杂。

比如:

为指定的 div 的子元素 div 的子元素 p 标签,展示变量 msg

那么通过命令式完成以上功能,则会得出如下逻辑与代码:

// 1. 获取到第一层的 div
const divEle = document.querySelector('#app')
// 2. 获取到它的子 div
const subDivEle = divEle.querySelector('div')
// 3. 获取第三层的 p
const subPEle = subDivEle.querySelector('p')
// 4. 定义变量 msg
const msg = 'hello world'
// 5. 为该 p 元素设置 innerHTML 为 hello world
subPEle.innerHTML = msg

那么通过以上例子,相信大家可以对命令式的概念有了一个基础的认识。

最后做一个总结,什么叫做命令式呢?

命令式是:关注过程 的一种编程范式,他描述了完成一个功能的 详细逻辑与步骤。

03:编程范式之声明式编程

当了解完命令式之后,那么接下来我们就来看 声明式 编程。

针对于声明式而言,大家其实都是非常熟悉的了。

比如以下代码,就是一个典型的 声明式 :

<div>{{ msg }}</div>

对于这个代码,大家是不是感觉有些熟悉?

没错,这就是 Vue 中非常常见的双大括号语法。所以当我们在写 Vue 模板语法 的时候,其实一直写的就是 声明式 编程。

那么声明式编程具体指的是什么意思呢?

还是以刚才的例子为例:

张三的妈妈让张三去买酱油。

那么张三怎么做的呢?

  1. 张三拿起钱
  2. 打开门
  3. 下了楼
  4. 到商店
  5. 拿钱买酱油
  6. 回到家

在这个例子中,我们说:张三所做的事情就是命令式。那么张三妈妈所做的事情就是 声明式。

在这样一个事情中,张三妈妈只是发布了一个声明,她并不关心张三如何去买的酱油,只关心最后的结果。

所以说,所谓声明式指的是:不关注过程,只关注结果 的范式。

同样,如果我们通过代码来进行表示的话,以下例子:

为指定的 div 的子元素 div 的子元素 p 标签,展示变量 msg

将会得出如下代码:

<div id="app">
  <div>
    <p>{{ msg }}</p>
  </div>
</div>

在这样的代码中,我们完全不关心 msg 是怎么被渲染到 p 标签中的,我们所关心的只是:在 p 标签中,渲染指定文本而已。

最后做一个总结,什么叫做声明式呢?

声明式是:关注结果 的一种编程范式,他 并不关心 完成一个功能的 详细逻辑与步骤。(注意:这并不意味着声明式不需要过程!声明式只是把过程进行了隐藏而已!)

04:命令式 VS 声明式

那么在我们讲解完成 命令式 和 声明式 之后,很多同学肯定会对这两种编程范式进行一个对比。

是命令式好呢?还是声明式好呢?

那么想要弄清楚这个问题,那么我们首先就需要先搞清楚,评价一种编程范式好还是不好的标准是什么?

通常情况下,我们评价一个编程范式通常会从两个方面入手:

  1. 性能
  2. 可维护性

那么接下来我们就通过这两个方面,来分析一下命令式和声明式。

性能

性能一直是我们在进行项目开发时特别关注的方向,那么我们通常如何来表述一个功能的性能好坏呢?

我们来看一个例子:

为指定 div 设置文本为 “hello world”

那么针对于这个需求而言,最简单的代码就是:

div.innerText = "hello world" // 耗时为:1

你应该找不到比这个更简单的代码实现了。

那么此时我们把这个操作的 耗时 比作 :1 。(PS:耗时越少,性能越强)

然后我们来看声明式,声明式的代码为:

<div>{{ msg }}</div>  <!-- 耗时为:1 + n -->
<!-- 将 msg 修改为 hello world -->

那么:已知修改text最简单的方式是innerText  ,所以说无论声明式的代码是如何实现的文本切换,那么它的耗时一定是 > 1 的,我们把它比作 1 + n(对比的性能消耗)。

所以,由以上举例可知:命令式的性能 > 声明式的性能

可维护性

可维护性代表的维度非常多,但是通常情况下,所谓的可维护性指的是:对代码可以方便的 阅读、修改、删除、增加 。

那么想要达到这个目的,说白了就是:代码的逻辑要足够简单,让人一看就懂。

那么明确了这个概念,我们来看下命令式和声明式在同一段业务下的代码逻辑:

// 命令式
// 1. 获取到第一层的 div
const divEle = document.querySelector('#app')
// 2. 获取到它的子 div
const subDivEle = divEle.querySelector('div')
// 3. 获取第三层的 p
const subPEle = subDivEle.querySelector('p')
// 4. 定义变量 msg
const msg = 'hello world'
// 5. 为该 p 元素设置 innerHTML 为 hello world
subPEle.innerHTML = msg
// 声明式
<div id="app">
  <div>
    <p>{{ msg }}</p>
  </div>
</div>

对于以上代码而言,声明式 的代码明显更加利于阅读,所以也更加利于维护。

所以,由以上举例可知:**命令式的可维护性 < 声明式的可维护性 **

小结一下

由以上分析可知两点内容:

  1. 命令式的性能 > 声明式的性能
  2. 命令式的可维护性 < 声明式的可维护性

那么双方各有优劣,我们在日常开发中应该使用哪种范式呢?

想要搞明白这点,那么我们还需要搞明白更多的知识。

05:企业应用的开发与设计原则

企业应用的设计原则,想要描述起来比较复杂,为什么呢?

因为对于 不同的企业类型(大厂、中小厂、人员外包、项目外包),不同的项目类型(前台、中台、后台)来说,对应的企业应用设计原则上可能会存在一些差异。

所以我们这里所做的描述,会抛弃一些细微的差异,仅抓住核心的重点来进行阐述。

无论什么类型的企业,也无论它们在开发什么类型的项目,那么最关注的点无非就是两个:

  1. 项目成本
  2. 开发体验

项目成本

项目成本非常好理解,它决定了一个公司完成“这件事”所付出的代价,从而直接决定了这个项目是否是可以盈利的(大厂的烧钱项目例外)。

那么既然项目成本如此重要,大家可以思考一下,决定项目成本的又是什么?

没错!就是你的 开发周期。

开发周期越长,所付出的人员成本就会越高,从而导致项目成本变得越高。

通过我们前面的分析可知,声明式的开发范式在 可维护性 上,是 大于 命令式的。

而可维护性从一定程度上就决定了,它会使项目的:开发周期变短、升级变得更容易 从而大量节约开发成本。

所以这也是为什么 Vue 会变得越来越受欢迎的原因。

开发体验

决定开发者开发体验的核心要素,主要是在开发时和阅读时的难度,这个被叫做:心智负担。

心智负担可以作为衡量开发难易度的一个标准,心智负担高则证明开发的难度较高,心智负担低则表示开发的难度较低,开发更加舒服。

那么根据我们之前所说,声明式的开发难度明显低于命令式的开发难度。

所以对于开发体验而言,声明式的开发体验更好,也就是 心智负担更低。

06:为什么说框架的设计过程其实是一个不断取舍的过程?

Vue 作者尤雨溪在一次演讲中说道:框架的设计过程其实是一个不断取舍的过程 。

这代表的是什么意思呢?

想要搞明白这个,那么再来明确一下之前说过的概念:

  1. 命令式的性能 > 声明式的性能
  2. 命令式的可维护性 < 声明式的可维护性
  3. 声明式的框架本质上是由命令式的代码来去实现的
  4. 企业项目开发时,大多使用声明式框架

当我们明确好了这样的一个问题之后,那么我们接下来来思考一个问题:框架的开发与设计原则是什么呢?

我们知道对于 Vue 而言,当我们使用它的是通过 声明式 的方式进行使用,但是对于 Vue 内部而言,是通过 命令式 来进行的实现。

所以我们可以理解为:Vue 封装了命令式的逻辑,而对外暴露出了声明式的接口

那么既然如此,我们明知 命令式的性能 > 声明式的性能 。那么 Vue 为什么还要选择声明式的方案呢?

其实原因非常的简单,那就是因为:命令式的可维护性 < 声明式的可维护性 。

为指定的 div 的子元素 div 的子元素 p 标签,展示变量 msg

以这个例子为例。

对于开发者而言,不需要关注实现过程,只需要关注最终的结果即可。

而对于 Vue 而言,他所需要做的就是:封装命令式逻辑,同时 尽可能的减少性能的损耗!它需要在 性能 与 可维护性 之间,找到一个平衡。从而找到一个 可维护性更好,性能相对更优 的一个点。

所以对于 Vue 而言,它的设计原则就是:在保证可维护性的基础上,尽可能的减少性能的损耗。

那么回到我们的标题:为什么说框架的设计过程其实是一个不断取舍的过程?

答案也就呼之欲出了,因为:

我们需要在可维护性和性能之间,找到一个平衡点。在保证可维护性的基础上,尽可能的减少性能的损耗。

所以框架的设计过程其实是一个不断在 可维护性和性能 之间进行取舍的过程

07:什么是运行时?

在 Vue 3 的 源代码 中存在一个 runtime-core 的文件夹,该文件夹内存放的就是 运行时 的核心代码逻辑。

runtime-core 中对外暴露了一个函数,叫做 渲染函数render

我们可以通过 render 代替 template 来完成 DOM 的渲染:

有些同学可能看不懂当前代码是什么意思,没有关系,这不重要,后面我们会详细去讲。

<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
</head>

<body>
  <div id="app"></div>
</body>

<script>
  const { render, h } = Vue
  // 生成 VNode
  const vnode = h('div', {
    class: 'test'
  }, 'hello render')

  // 承载的容器
  const container = document.querySelector('#app')

  // 渲染函数
  render(vnode, container)
</script>

我们知道,在 Vue 的项目中,我们可以通过 tempalte 渲染 DOM 节点,如下:

<template>
	<div class="test">hello render</div>
</template>

但是对于 render 的例子而言,我们并没有使用 tempalte,而是通过了一个名字叫做 render 的函数,返回了一个不知道是什么的东西,为什么也可以渲染出 DOM 呢?

带着这样的问题,我们来看:

我们知道在上面的代码中,存在一个核心函数:渲染函数 render,那么这个 render 在这里到底做了什么事情呢?

我们通过一段代码实例来去看下:

假设有一天你们领导跟你说:

我希望根据如下数据:

渲染出这样一个 div:

{
 type: 'div',
 props: {
  class: test
 },
 children: 'hello render'
}
<div class="test">hello render</div>

那么针对这样的一个需求你会如何进行实现呢?大家可以在这里先思考一下,尝试进行一下实现,然后我们再继续往下看..........

那么接下来我们根据这个需求来实现以下代码:

<script>
  const VNode = {
    type: 'div',
    props: {
      class: 'test'
    },
    children: 'hello render'
  }
  // 创建 render 渲染函数
  function render(vnode) {
    // 根据 type 生成 element
    const ele = document.createElement(vnode.type)
    // 把 props 中的 class 赋值给 ele 的 className
    ele.className = vnode.props.class
    // 把 children 赋值给 ele 的 innerText
    ele.innerText = vnode.children
    // 把 ele 作为子节点插入 body 中
    document.body.appendChild(ele)
  }

  render(VNode)
</script>

在这样的一个代码中,我们成功的通过一个 render 函数渲染出了对应的 DOM,和前面的 render 示例 类似,它们都是渲染了一个 vnode,你觉得这样的代码真是 妙极了!

但是你的领导用了一段时间你的 render 之后,却说:天天这样写也太麻烦了,每次都得写一个复杂的 vnode,能不能让我直接写 HTML 标签结构的方式 你来进行渲染呢?

你想了想之后,说:如果是这样的话,那就不是以上 运行时 的代码可以解决的了!

没错!我们刚刚所编写的这样的一个“框架”,就是 运行时 的代码框架。

那么最后,我们做一个总结:运行时可以利用render 把 vnode 渲染成真实  dom 节点。

08:什么是编译时?

在刚才,我们明确了,如果只靠 运行时,那么是没有办法通过 HTML 标签结构的方式 的方式来进行渲染解析的。

那么想要实现这一点,我们就需要借助另外一个东西,也就是 编译时。

Vue 中的编译时,更准确的说法应该是 编译器 的意思。它的代码主要存在于 compiler-core 模块下。

我们来看如下代码:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
</head>

<body>
  <div id="app"></div>
</body>

<script>
  const { compile, createApp } = Vue

  // 创建一个 html 结构
  const html = `
    <div class="test">hello compiler</div>
  `
  // 利用 compile 函数,生成 render 函数
  const renderFn = compile(html)

  // 创建实例
  const app = createApp({
    // 利用 render 函数进行渲染
    render: renderFn
  })
  // 挂载
  app.mount('#app')
</script>

</html>

对于编译器而言,它的主要作用就是:把 template 中的 html 编译成 render 函数。然后再利用 运行时 通过 render 挂载对应的 DOM。

那么最后,我们做一个总结:编译时可以把html 的节点,编译成 render函数

09:运行时 + 编译时

前面两小节我们已经分别了解了 运行时 和 编译时,同时我们也知道了:vue 是一个 运行时+编译时 的框架!

vue 通过 compiler 解析 html 模板,生成 render 函数,然后通过 runtime 解析 render,从而挂载真实 dom。

那么看到这里可能有些同学就会有疑惑了,既然 compiler 可以直接解析 html 模板,那么为什么还要生成 render 函数,然后再去进行渲染呢?为什么不直接利用 compiler 进行渲染呢?

即:为什么 vue 要设计成一个 运行时+编译时的框架呢?

那么想要理清楚这个问题,我们就需要知道 dom 渲染是如何进行的。

对于 dom  渲染而言,可以被分为两部分:

  1. 初次渲染 ,我们可以把它叫做 挂载
  2. 更新渲染 ,我们可以把它叫做 打补丁

初次渲染

那么什么是初次渲染呢?

当初始 div 的 innerHTML 为空时,

<div id="app"></div>

我们在该 div 中渲染如下节点:

<ul>
 <li>1</li>
 <li>2</li>
 <li>3</li>
</ul>

那么这样的一次渲染,就是 初始渲染。在这样的一次渲染中,我们会生成一个 ul 标签,同时生成三个 li 标签,并且把他们挂载到 div 中。

更新渲染

那么此时如果 ul 标签的内容发生了变化:

<ul>
 <li>3</li>
 <li>1</li>
 <li>2</li>
</ul>

li - 3 上升到了第一位,那么此时大家可以想一下:我们期望浏览器如何来更新这次渲染呢?

浏览器更新这次渲染无非有两种方式:

  1. 删除原有的所有节点,重新渲染新的节点
  2. 删除原位置的 li - 3,在新位置插入 li - 3

那么大家觉得这两种方式哪一种方式更好呢?那么我们来分析一下:

  1. 首先对于第一种方式而言:它的好处在于不需要进行任何的比对,需要执行 6 次(删除 3 次,重新渲染 3 次)dom 处理即可。
  2. 对于第二种方式而言:在逻辑上相对比较复杂。他需要分成两步来做:
  1. 对比 旧节点 和 新节点 之间的差异
  2. 根据差异,删除一个 旧节点,增加一个 新节点

那么根据以上分析,我们知道了:

  1. 第一种方式:会涉及到更多的 dom 操作
  2. 第二种方式:会涉及到 js 计算 + 少量的 dom 操作

那么这两种方式,哪一种更快呢?我们来实验一下:

const length = 10000
  // 增加一万个dom节点,耗时 3.992919921875 ms
  console.time('element')
  for (let i = 0; i < length; i++) {
    const newEle = document.createElement('div')
    document.body.appendChild(newEle)
  }
  console.timeEnd('element')

  // 增加一万个 js 对象,耗时 0.402099609375 ms
  console.time('js')
  const divList = []
  for (let i = 0; i < length; i++) {
    const newEle = {
      type: 'div'
    }
    divList.push(newEle)
  }
  console.timeEnd('js')

从结果可以看出,dom 的操作要比 js 的操作耗时多得多,即:dom** 操作比 js 更加耗费性能**。

那么根据这样的一个结论,回到我们刚才所说的场景中:

  1. 首先对于第一种方式而言:它的好处在于不需要进行任何的比对,仅需要执行 6 次(删除 3 次,重新渲染 3 次)dom 处理即可。
  2. 对于第二种方式而言:在逻辑上相对比较复杂。他需要分成两步来做:

对比 旧节点 和 新节点 之间的差异

根据差异,删除一个 旧节点,增加一个 新节点

根据结论可知:方式一会比方式二更加消耗性能(即:性能更差)。

那么得出这样的结论之后,我们回过头去再来看最初的问题:为什么 vue 要设计成一个 运行时+编译时的框架呢?

答:

  1. 针对于 纯运行时 而言:因为不存在编译器,所以我们只能够提供一个复杂的 JS 对象。
  2. 针对于 纯编译时 而言:因为缺少运行时,所以它只能把分析差异的操作,放到 编译时 进行,同样因为省略了运行时,所以速度可能会更快。但是这种方式这将损失灵活性(具体可查看第六章虚拟 DOM ,或可点击 这里 查看官方示例)。比如 svelte ,它就是一个纯编译时的框架,但是它的实际运行速度可能达不到理论上的速度。
  3. 运行时 + 编译时:比如 vue 或 react 都是通过这种方式来进行构建的,使其可以在保持灵活性的基础上,尽量的进行性能的优化,从而达到一种平衡。

10:什么是副作用

在 vue 的源码中,会大量的涉及到一个概念,那就 副作用。

所以我们需要先了解一下副作用代表的是什么意思。

副作用指的是:当我们 对数据进行 setter 或 getter 操作时,所产生的一系列后果。

那么具体是什么意思呢?我们分别来说一下:

setter

setter 所表示的是 赋值 操作,比如说,当我们执行如下代码时 :

msg = '你好,世界'

这时 msg 就触发了一次 setter 的行为。

那么假如说,msg 是一个响应性数据,那么这样的一次数据改变,就会影响到对应的视图改变。

那么我们就可以说:msg 的 setter 行为,触发了一次副作用,导致视图跟随发生了变化。

getter

getter 所表示的是 取值 操作,比如说,当我们执行如下代码时:

element.innerText = msg

此时对于变量 msg 而言,就触发了一次 getter 操作,那么这样的一次取值操作,同样会导致 element 的 innerText 发生改变。

所以我们可以说:msg 的 getter 行为触发了一次副作用,导致 element 的 innterText 发生了变化。

副作用会有多个吗?

那么明确好了副作用的基本概念之后,那么大家想一想:副作用可能会有多个吗?

答案是:可以的。

举个简单的例子:

<template>
  <div>
    <p>姓名:{{ obj.name }}</p>
    <p>年龄:{{ obj.age }}</p>
  </div>
</template>

<script>
 const obj = ref({
    name: '张三',
    age: 30
  })
  obj.value = {
    name: '李四',
    age: 18
  }
</script>

在这样的一个代码中 obj.value 触发了一次 setter 行为,但是会导致两个 p 标签的内容发生改变,也就是产生了两次副作用。

小节一下

根据本小节我们知道了:

  1. 副作用指的是:对数据进行 setter 或 getter 操作时,所产生的一系列后果
  2. 副作用可能是会有多个的。

11:Vue 3 框架设计概述

根据前面的学习我们已经知道了:

  1. 什么是声明式
  2. 什么是命令式
  3. 什么是运行时
  4. 什么是编译时
  5. 什么是运行时+编译时
  6. 同时也知道了 框架的设计过程本身是一个不断取舍的过程

那么了解了这些内容之后,下来 vue3 的一个基本框架设计:

对于 vue3 而言,核心大致可以分为三大模块:

  1. 响应性:reactivity
  2. 运行时:runtime
  3. 编译器:compiler

我们以以下基本结构来描述一下三者之间的基本关系:

<template>
	<div>{{ proxyTarget.name }}</div>
</template>

<script>
import { reactive } from 'vue'
export default {
	setup() {
		const target = {
			name: '张三'
		}
		const proxyTarget = reactive(target)
		return {
			proxyTarget
		}
	}
}
</script>

在以上代码中:

  1. 首先,我们通过 reactive 方法,声明了一个响应式数据。

该方法是 reactivity 模块对外暴露的一个方法

可以接收一个复杂数据类型,作为  Proxy (现在很多同学可能还不了解什么是 proxy ,没有关系后面我们会详细介绍它,现在只需要有个印象即可)的 被代理对象(target)

返回一个 Proxy 类型的 代理对象(proxyTarget)

当 proxyTarget 触发 setter 或 getter 行为时,会产生对应的副作用

  1. 然后,我们在 tempalte 标签中,写入了一个 div。我们知道这里所写入的 html 并不是真实的 html,我们可以把它叫做 模板,该模板的内容会被 编译器( compiler ) 进行编译,从而生成一个 render 函数
  2. 最后,vue 会利用 运行时(runtime) 来执行 render 函数,从而渲染出真实 dom

以上就是 reactivity、runtime、compiler 三者之间的运行关系。

当然除了这三者之外, vue 还提供了很多其他的模块,比如:SSR ,我们这里只是 概述了基本的运行逻辑。

责任编辑:武晓燕 来源: 程序员Sunday
相关推荐

2020-07-29 09:21:34

Docker集群部署隔离环境

2024-04-01 10:09:23

AutowiredSpring容器

2021-04-10 10:37:04

OSITCP互联网

2021-07-05 22:22:24

协议MQTT

2019-07-07 08:18:10

MySQL索引数据库

2022-01-05 09:27:24

读扩散写扩散feed

2024-02-19 00:00:00

后管系统权限

2024-02-27 14:27:16

2024-01-05 07:55:39

Linux虚拟内存

2019-05-22 08:43:45

指令集RISC-V开源

2024-02-22 12:20:23

Linux零拷贝技术

2020-12-24 15:18:27

大数据数据分析

2024-02-23 08:08:21

2021-10-29 11:30:31

补码二进制反码

2023-08-14 11:35:16

流程式转化率数据指标

2019-06-19 14:58:38

服务器负载均衡客户端

2019-06-20 17:49:51

RPCHTTP协议

2017-12-17 20:17:23

NoSQLSQL数据

2021-08-20 16:13:40

机器学习人工智能计算机

2018-08-13 09:20:21

NoSQLSQL数据
点赞
收藏

51CTO技术栈公众号