【Vue.js设计与实现】第三章:Vue.js3的设计思路

869 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

框架设计讲究全局视角的把控,一个项目就算再大,也是存在一条核心思路,并围绕核心展开。

3.1 声明式地描述UI

Vue3是一个 声明式的UI框架。 就是说我们在使用Vue3开发页面的时候,是声明式的描述UI的。

我们在编写前端页面的时候会涉及到以下内容:

  1. DOM元素:也就是各种标签, 比如 <div>\ <a>等等;
  2. 属性:标签所具有的属性,比如id、class等等;
  3. 事件:DOM元素所绑定的事件,比如点击事件click、按键事件keydown等等;
  4. 元素的层级结构:DOM树的层级结构,即DOM元素的子节点、父节点等等;

想要实现一个声明式的UI框架,就要思考 如何去声明式的描述 上面的这些内容。

下面这是Vue3的解决方案:

  1. 使用与原生HTML一致的方式来描述DOM元素,比如也是使用 <div> 来描述一个div标签;
  2. 使用与原生HTML一致的方式来描述DOM元素的属性,比如 <div id="app"></div>
  3. 通过 v-bind 来描述动态绑定的属性,比如 <div v-bind:id="dynamicId"></div>
  4. 通过 v-on 来描述元素绑定的事件,比如 <div v-on:click="handleCilck"></div>
  5. 使用与原生HTML一致的方式来描述DOM元素的层级关系,比如 <div><span></span></div>

上面这种解决方案叫做 模板。 Vue中还可以使用 JavaScript对象 来描述:

image.png

与模板相比,使用JS对象来描述UI更加的灵活。

比如我们要描述 h1 ~ h6 的标签,使用JS对象,只需要将标签的 level 设成一个变量就可以了,但是使用模板来描述时,就不得不把 h1 ~ h6 的标签都穷举在页面上,再通过条件判断来选择使用哪个,这种写法远远没有使用JS对象灵活。

使用JS对象来描述UI的方式,其实就是所谓的虚拟DOM。

我们在Vue组件中手写的渲染函数 就是使用虚拟DOM来描述UI的:

image.png

上面代码中的 h 函数的返回值就是一个对象(虚拟DOM),这个函数的作用就是让我们编写虚拟DOM更加轻松,它仅仅只是一个辅助创建虚拟DOM的工具函数。

组件中的渲染函数,也就是上面代码中的 render 函数,一个组件要渲染的内容是通过渲染函数来描述的,平时我们写在 <template> 标签中的会通过编译器编译成虚拟DOM,再通过渲染函数渲染成真实DOM,所以有时候我们的组件可以不写 <template> 标签,而是直接使用虚拟DOM来描述组件内容,最后使用渲染函数渲染。

3.2 初识渲染器

渲染器的作用就是把虚拟DOM渲染成真实DOM

虚拟DOM,就是用JS对象来描述页面中真实的DOM结构,虚拟DOM通过渲染器变成真实DOM并渲染到浏览器页面中。

一个简单的虚拟DOM我们在上面的代码中已经展现过了,解释一下其中的属性:

  • tag 用来描述标签的名称;
  • props 是一个对象,用来描述 tag所描述的标签所拥有的属性、事件等等;
  • children 是一个数组,也可以是一个字符串,它用来描述标签的子节点;
    • 当是一个数组的时候,就会递归的调用render函数继续渲染;
    • 当时一个字符串的时候,就会使用 createTextNode 函数创建一个文本节点,并将其添加到新创建的元素内。

通过上面对虚拟DOM的属性的介绍,我们可以想一想,如果我们想写一个渲染器应该要怎么写呢?只要回答下面这几个问题,我们就明白如何实现了:

  1. 如何创建元素: 使用 createElement 方法;
  2. 如何为元素添加属性和事件:虚拟DOM的props属性是一个对象,我们循环对象使用的是 for-in 循环,给元素添加事件可以使用 addEventListener 方法;
  3. 如何处理其层级关系:因为children的类型有字符串和数组两种,所以我们可以使用 typeof 关键字来判断其类型,并根据判断结果来选择是递归调用render还是使用 createTextNode 来创建文本节点;

上面的问题其实就是一个简单的渲染器的实现,我们将其编写成代码,如下:

image.png

通过上面的代码我们就实现了一个简单的渲染器了,我们可以试着把上面简单的虚拟DOM传入到这个渲染器中,并将其挂载到body元素上,就可以看到效果啦。其实渲染器的工作原理很简单,都是一些我们熟悉的DOM操作API。

渲染器的精髓其实并不是在于它的渲染阶段,而是在于它的更新节点阶段。

3.3 组件的本质

上面两点都是在围绕虚拟DOM来讲的,这一点来想想,什么是组件。 虚拟DOM除了描述真实DOM之外,也可以是用它来描述组件,组件并不是真实的DOM元素。

一句话总结就是:组件就是一组DOM元素的封装。

组件的返回值也是虚拟DOM,它代表了组件要渲染的内容。因此,我们可以定义一个函数来代表组件,然后这个函数的返回值就代表组件要渲染的内容。有了这个定义,我们就可以修改一下我们的渲染器,使得它支持组件渲染了。

image.png

上面代码中的 mountElement 函数就是我们最原始版本的渲染器,而 mountComponent 函数的实现则如下

image.png

到这里停一下,我们上面是 我们选择把组件定义为一个函数的,这其实也就是意味着,我们可以自定义组件的数据类型, 我们除了把它定义为函数,我们也可以把它定义为一个JS对象,Vue中有状态的组件就是被定义为一个JS对象来表达的。

3.4 模板的工作原理

开头的时候我们知道Vue中有两种方式来声明式的描述UI,上面讲了其中的一种,虚拟DOM,那么另一种,模板是如何工作的呢?

模板的工作离不开编译器。

编译器的作用就是将模板编译成渲染函数。

编译器会把模板内容编译成渲染函数,并且添加 script 标签块的组件对象上。

image.png

对于一个组件而言,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟DOM渲染为真实DOM,这就是模板的工作原理,也是Vue渲染页面的流程。

3.5 Vue.js是各个模块组成的有机整体

组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟DOM的设计而定的,因此,vue的各个模块之间是相互关联、相互制约的,共同构成一个有机的整体。

比如说,渲染器更新节点的时候,它要花费力气去找哪些节点变更,但是如果编译器在编译的时候,就告诉渲染器哪些内容是动态的,是会发生变化的,并为之做上标记,这样到时候更新的时候,渲染器就不需要再花费大力气去寻找变更点了。

比如说,编译器在生成代码时,给虚拟DOM附带一个 patchFlag 属性,并赋值为1,代表这个DOM的class属性是可变的,这样渲染器就知道了,到时候更新时就直接跑去变更class,就省去寻找变更点的力气,这就是vue各个模块的配合。