Skip to content

第一章:框架设计前瞻-框架设计的基本概念

01: 前言

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

  1. 我们需要同步并明确一些词汇的概念,比如:声明式、命令式、运行时、编译时...。这些词汇将会在后面的框架设计中被经常涉及到。

  2. 我们需要了解一些关于前端框架的一些基础的概念。框架的设计原则,开发者开发体验原则,还有一些同学比较关注的vue3中ts支持友好的问题,以此来帮助大家解决一些固有的疑惑,从而揭开 vue 神秘的面纱。 那么准备好了? 我们开始吧!

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

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

  1. 命令式编程
  2. 声明式编程 这两种范式一般是相对来去说的。

命令式

那么首先我们先来说什么叫做命令式。 具体例子:

张三的妈妈让张三去买酱油。 那么张三怎么做的呢? 1. 张三拿起钱 2. 打开门 3. 下了楼 4. 到商店 5. 拿钱买酱油 6. 回到家

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

那么如果把这样的方式放到具体的代码实现之中,又应该怎么做呢? 我们来看以下这样的一件事情: 在指定的div中展示"hello world"

那么如果我们想要完成这样的事情,通过命令式的方式我们如何实现? 我们知道命令式的核心在于:关注过程。 所以,以上事情通过命令式实现则可得出以下逻辑与代码:

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

该代码虽然只有两步,但是它清楚的描述了:完成这件事,所需要经历的过程 那么假如我们所做的事情,变得更加复杂了,则整个过程也会变得更加复杂。 比如:

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

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

js
	// 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:编程范式之声明式编程

当了解完成命令式之后,那么接下来我们来看声明式编程。 针对于声明式而言,大家其实都是非常熟悉了。 比如以下代码,就是一个典型的声明式

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

对于这个代码,大家是不是感觉有些熟悉? 没错,这就是 Vue 中非常常见的双大括号语法。所以当我们在写 Vue 模版语法的时候,其实一直写的就是声明式编程。

那么声明式编程具体指的是什么意思呢? 还是以刚才的例子为例:

张三的妈妈让张三去买酱油。 那么张三怎么做呢?

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

在这个例子中,我们说:张三所做的事情就是命令式。那么张三的妈妈做的事情就是声明式。 在这样一个事情中,张三妈妈只是发布了一个声明,她并不关心张三如何去买的酱油,只关心最后的结果。 所以说,所谓声明式指的是:不关注过程,只关注结果的范式。 同样,如果我们通过代码来进行表示的话,以下例子:

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

将会得出如下代码:

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

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

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

04: 命令式 VS 声明式

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

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

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

  1. 性能
  2. 可维护性

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

性能

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

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

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

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

你应该找不到比这个更简单的代码实现了。 那么此时我们把这个操作的耗时比作:1。(PS:耗时越少,性能越强) 然后我们来看声明式,声明式的代码为:

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

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

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

可维护性

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

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

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

js
	// 命令式
	// 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;
html
	<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 中的html是真实的 html 吗?

这一小节,我们首先需要先思考一个问题:在.vue文件的 template 中写入 html 是真实的 html 标签节点吗?

答案是:不是的! 原因非常简单,如果我们写入的是真实的 html 节点,对于 v-ifv-bindkeep-alive 这些东西,浏览器明显是不认识的,所以这些东西理应无法解析。

但是现实是这些指令或组件被正确解析了,所以vue一定在中间做了什么,让假的html标签节点被渲染成了真实的html标签节点

简单来说可以分成两件事(排序按执行顺序):

  1. 编译时:compiler
  2. 运行时:runtime

这两个东西对于大家而言,可能比较陌生,但是在 Vue 官网中早就提到了这几个概念。 这些概念一共有三个,如果我们想要学习 Vue 的框架设计,那么必须要了解它们,它们分别是:

  1. 运行时:runtime
  2. 编译时:compiler
  3. 运行时 + 编译时:runtime + compiler

08:什么是运行时?

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

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

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

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

html
  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.47/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>
  </html>

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

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

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

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

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

假设有一天你们领导跟你说: 我希望根据如下数据:

json
{ type: 'div', props: { class: test }, children: 'hello render'}

渲染出这样一个 div

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

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

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

<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>
</html>

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

但是你的领导用了一段时间你的 render 之后,却说:天天这样写也太麻烦了,每次都得写一个复杂的 vnode,能不能让我直接写 HTML标签结构的方式 你来进行渲染呢? 你想了想之后,说:如果是这样的话,那不就是以上运行时的代码可以解决的了!

没错!我们刚刚所编写的这样一个“框架”,就是运行时的代码框架。 那么最后,我们做一个总结:**运行时可以利用 rendervnode 渲染成真实 dom 节点。

09:什么是编译时?

在上一小节中,我们明确了,如果只靠运行时,那么是没有办法通过 HTML 标签结构的方式来进行解析的。 那么想要实现这一点,我们就需要借助另外一个东西,也就是 编译时Vue 中的编译时,更准确的说法应该是 编译器 的意思。它的代码主要存在于 compiler-core 模块。

我们来看如下代码:

html
	<!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.47/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 函数

10:运行时 + 编译时

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

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

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

即:为什么 vue 要设计成一个 运行时+编译时的框架呢? 那么想要理清楚这个问题,我们就需要知道dom渲染是如何进行的 对于 dom 渲染而言,可以被分为两部分:

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

初次渲染

那么什么是初次渲染呢? 当初试 divinnerHTML 为空时,

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

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

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

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

更新渲染

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

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

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

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

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

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

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

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

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

js
	const len = 10000
	// 增加一万个 dom 节点,耗时 3.923095703125 ms
	const len = 10000;
    console.time('ele');
    for(let i = 0; i < len; i++) {
        const div = document.createElement('div');
        document.body.appendChild(div);
    }
    console.timeEnd('ele')
		// 增加一万个 js 对象,耗时0.31103515625 ms
    console.time('js');
    const divList = [];
    for (let i = 0; i < len; i++) {
        const ele = {
            type: 'div'
        }
        divList.push(ele)
    }
    console.timeEnd('js')

从结果可以看出,dom 的操作要比 js的操作耗时多得多,即:dom 操作比 js 更加耗费性能。 那么根据这样的一个结论,回到我们刚才所说的场景中 :

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

根据结论可知:方式一会比方式二更加消耗性能 (即:性能更差) 那么得出这样的结论之后,我们回过头去再来看最初的问题:为什么 vue 要设计成一个 运行时+编译时的框架呢?

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

11:什么事副作用

在 vue 的源码中,会大量的涉及到一个概念,那就是副作用。 所以我们需要先了解一下副作用代表的是什么意思。 副作用指的是:当我们对数据进行 setter 或 getter 操作时,所产生的一系列后果。 那么具体是什么意思呢? 我们分别来说一下:

setter

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

	msg = '你好,世界'

这时 msg 就触发了一次 setter 的行为。 那么假如说,msg 是一个响应式数据,那么这样的一次数据改变,就会影响到对应的视图改变。 那么我们就可以说:msgsetter 的行为,触发了一次副作用,导致视图跟随发生了变化。

getter

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

	element.innerText = msg

此时对于变量 msg 而言,就触发了一次 getter 操作,那么这样的一次取值操作,同样会导致elementinnerText 发生改变。 所以我们可以说:msggetter 行为触发了一次副作用,导致 elementinnterText 发生了变化。

副作用会有多个吗?

那么明确好了副作用的基本概念之后,那么大家想一想:副作用可能会有多个吗? 答案是:可以的。 举个简单的例子:

html
<template>
  <div>
    <p>姓名: {{ obj.name }}</p>
    <p>年龄: {{ obj.age }}</px
  </div>
</template>
<script>
  const obj = ref({
  	name: '张三'
  	age: 30
  })
  obj.value = {
  	name:'李四'
  	age: 18
  }
</script>

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

总结

根据本小节我们知道了:

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

12:Vue3 框架设计概述

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

  1. 什么是声明式
  2. 什么是命令式
  3. 什么是运行时
  4. 什么是编译时
  5. 什么是运行时 + 编译时
  6. 同时也知道了框架的设计过程本身时一个不断取舍的过程 那么了解了这些内容之后,下来 vue3 的一个基本框架设计: 对于 vue3 而言,核心大致可以分为三大模块:
  7. 响应性:reactivity
  8. 运行时:runtime
  9. 编译器:compiler

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

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

13:扩展:所谓良好的 TypeScript 支持,是如何提供的?

目前在网上对于 vue3 的讨论是非常多的,我印象中看到过类似的如下内容 (包括在面试中也听到有人说过) :

说:“vue 3 对 ts 支持比较好,是因为 vue 3 本身是使用 ts 编写的”(PS:这是一个错误的概 念

所以说才会有这样的一块内容,其目的就是纠正这样一个问题。

良好的 TypeScript 支持,是如何提供的?

typescript (简称: ts)是微软发布的一个 js 的超集,大家可以理解为这是一个包含类型的JavaScript 。

我们来看如下通过 ts 来书写的一个例子 (**介于很多同学可能并不了解 ts ,所以我们将使用一个最简单的例子,大家不需要关注里面的代码,只需要理解我们在说什么即可 **):

ts
  /**
   * 使用 ts 声明一个方法,它期望接受两个数字,并且返回两者相加的和
   */
  function sum(s1: any, s2: any) {
      return s1 + s2;
  }

  // 触发 sum 方法,但是我们传入了两个字符串
  const total = sum('1', '2')

以上的代码通过 ts 进行编写,我们期望接受的 s1s2 应该是两个``number,并且返回值也应该是一个 number。 但是我们却发现,当我们传入了两个字符串时,代码并没有报错,并且返回一个 12`。这样的结果将会是一个非常令人疑惑的行为。

接下来我们可以把 sum 方法进行一下修改:

ts
  /**
   * 使用 ts 声明一个方法,它期望接受两个数字,并且返回两者相加的和
   */
  function sum(s1: number, s2: number) {
      return s1 + s2;
  }

  // 触发 sum 方法,但是我们传入了两个字符串
  const total = sum('1', '2')

此时大家就发现,在sum('1', '2')处,会抛出一个错误: image-20230809213059710

它很明确的告诉了我们:参数应该是一个 number 那么这样的例子告诉了我们:ts 编写的程序和 ts 类型支持友好是两回事。如果想要让你的程序拥有更好地 ts 支持,那么你需要做很多额外的事情。

为了 vue 拥有良好的 ts 支持,vue 内部其实做了非常多的事情,比如(截图来自:core/packages/runtime-core/src/componentPublicInstance.ts)这样的代码有很多,绝不仅限于一个文件。

image-20230809214059269

这些代码存在的目的只是为了进行更好地 ts 支持,所以说想要得到一个良好的 ts 支持,是需要进行非常多的努力的。

14:总结

在本章中,我们对整个 vue 框架设计中的一些基本概念都做了一个了解。

明确了如下基本概念:

  1. 命令式
  2. 声明式
  3. 心智负担
  4. 框架设计与取舍之间的关系
  5. 运行时
  6. 编译时
  7. 运行时 + 编译时
  8. 副作用
  9. reactivity、runtime、compiler 三者之间的运行关系
  10. 扩展:良好的 ts 支持

当我们把这些基本概念了解清楚之后,那么下一章我们就可以准备开始构建我们的 vue3 框架了。

Released under the MIT License.