面试总问Vue3原理和实现,读Vue3源码前,先了解这些 事倍功半

前言

    在前端界,大多数人都说Vue.js上手门槛很低,并且在工作中大多数场景只需要简单的熟练的运用API和依赖其开源的生态就能完成前端需求的开发。但面试早已不只是考察你应用层面的掌握情况,面试官还喜欢考察技术背后的实现原理来判断你对技术的掌握程度,以及是否有对技术的钻研精神。但是学习源码很枯燥,而且抽象,理解起来也更难,很多人就放弃了,本文将结合《Vue.js设计与实现》,以全局视角去看框架设计的思路,希望对读Vue3源码前的你有所帮助。

本文目录

(一)框架基本知识
从两种框架的范式的介绍到虚拟DOM的性能情况,到运行时和编译时的相关知识,并介绍Vue.js3是个运行时和编译时的框架
(二)框架设计的核心要素
从框架设计者在设计框架考虑的用户开发体验,控制框架代码体积,Tree-Shaking的工作机制、框架产物、特性开关、错误处理
(三)Vue3的设计思路
从全局视角了解Vue.js3的设计思路,以及各个模块之间如何协作的

(四)Vue3的优化集合
分别从源码优化、性能优化、语法API优化三个维度通过思维导图的形式展现 Vue.js 3.0 的优化

备注:本文引用的Vue3源码版本是 "version": "3.2.31"

一、框架基本知识

1.1 框架的范式

命令式

关注过程。比如JQuery,自然语言描述能够与代码一一对应关系

$('#app').text('hello world').on('click',()=>{console.log('你好 世界')})
复制代码

声明式

关注结果。像Vue帮我们封装了过程,内部实现是命令式,暴露给用户的更加声明式

<div @click="()=>{console.log('你好 世界')}">hello world</div>
复制代码

1.2 性能与可维护性的权衡

命令式代码的更新性能消耗 = 直接修改的性能消耗 声明式代码的更新性能消耗 = 直接修改的性能消耗 + 找出差异的性能消耗

可以看出理论上来说,声明式代码性能不优于命令式代码性能,但是为什么Vue.js还是选择声明式框架呢?原因是声明式代码的可维护性更强,在开发的时候,我们不需要关注过程,展示的直接是我们的想要的结果,看上去更直观。当然声明式代码提升可维护的同时,性能会有一定的损失,所以大多数框架的版本的迭代更新,都会在性能损失上做最小化的优化。

1.3 虚拟DOM的性能到底如何

虚拟DOM是为了最小化差异化性能消耗而出现的,让声明式代码的性能无限接近命令式代码的性能。虚拟DOM的更新技术理论上不能比原生js操作DOM更高。

image.png

从上图上,我们大概可以看到:
(1)原生JS操作DOM性能是最高的,但是心智负担最高,代码可维护性也很低,你需要手动去创建,删除修改大量的DOM元素。
(2)innerHML,是通过拼接字符串和原生JS绑定事件之类事情,如果模板很大,更新的性能最差,特别是少量的更新的时候。
(3)虚拟DOM心智负担小,可维护性强

运行和编译

作为前端,我们运用的框架时候往往有三个类型,一个是纯运行时,运行时+编译时,编译时。

运行时,框架做的工作是对一个树形结构的数据对象进行递归,将数据渲染成DOM元素,像在Vue.js提供一个渲染函数render。树形结构的数据对象,本质上就是用来描述DOM的JS对象,在Vue.js中可以描述不同类型的节点,比如普通元素节点、组件节点等。就像如下:

const vnode = {
  type: 'button',
  props: { 
    'class': 'btn',
    style: {
      width: '100px',
      height: '50px'
    }
  },
  children: 'click me'
}
复制代码

其中,type 属性表示 DOM 的标签类型,props 属性表示 DOM 的一些附加信息,比如 style 、class 等,children 属性表示 DOM 的子节点,它也可以是一个数组(代表子节点),也可以是用字符串表示简单的文本。

当然我们会发现手写树形结构的数据对象太麻烦了,而且不直观,能不能支持类似HTML标签的方式描述树形结构对象呢? 就像下面:

<div> 
  <span>hello world</span>
</div>
复制代码

转换为

const vnode = {
  type: 'div',
  children:[
    {type:'span', children:'hello world'}
  ]
}
复制代码

为此,我们就要编写了一个Compiler函数,来编译得到树形结构的数据对象,然后再去调用render函数进行渲染。

此时可能你会思考,既然编译器可以把HTML字符串编译成数据对象,为什么不能直接编译成命令式代码呢? 就像Vue.js是一个编译时+运行时的框架,它是为了保持灵活性的基础上,又能通过编译手段分析用户提供的内容,从而进一步提升更新性能。

二、框架设计的核心要素

image.png

2.1 提升用户的开发体验

比如友好的警告信息,让用户更好的快速定位问题,节省用户时间

image.png

2.2 控制框架代码的体积

在实现同样功能的情况下,代码越少越好,这样体积就会越小,最后浏览器加载资源的时间就也少。Vue.js的打印警告信息只在开发环境,生产环境不会提供警告信息。Vue.js每个打印警告信息都是有一个_DEV_常量

2.3 框架要做好良好的Tree-Shaking

Tree-Shaking指的是消除那些永远不会被执行的代码,在前端领域这个概念因rollup.js而普及,webpack都支持。但是模块必须是ESM(ES module),因为Tree-Shaking依赖ESM的静态结构

Tree-Shaking 如果一个函数的调用会参数副作用,就不能将其移出,rollup.js和webpack以及一些压缩工具都会识别 /*#__PURE__*/, vue3源码中有大量使用

image.png

框架应该输出怎么样的构建产物

(1) 立即调用的函数表达式(IIFE)

(function () {
// ...
}())
复制代码

在rollup.js的输出格式中配置:format:'iife' ,在Vue里vue.global.js就是IIFE形式的资源

(2) ESM

现在主流浏览器对原生ESM的支持都不错,例如Vue3中输出 vue.ems-browser.js文件,用户可以直接 <script type='module'> 标签引入

<script type='module' src='/path/to/vue.ems-browser.js'></script>
复制代码

在rollup.js的输出格式中配置:format:'es',即可输出EMS格式的资源,但是你在Vue源码中,也许你看到,Vue中也会有一个输出为esm.-bundler.js文件(可看下图)

为什么这样设计呢?答案是带有-bundler字样的EMS资源是给rollup.js、webpack、vite等打包工具使用的,而带有-browser字样的是给<script type='module'> 使用的,这样设计的好处就是本质上是让用户可以自行觉得构建资源的目标环境和有利于Tree-Shaking压缩输出资源代码体积。

(3)cjs

在rollup.js的输出格式中配置:format:'cjs', 即可输出require语句引用资源,服务端渲染场景,适用于在node.js中运行

image.png (备注:该截图源码路径:core-main\rollup.config.js)

2.4 特性开关

(1)用户可以设置对框架功能特性的开启和关闭,也可以通过Tree-Shaking机制让其不包含在最终的项目资源中

(2)使得框架设计带来灵活性,可以通过特性开关任意为框架添加新的特性

在Vue.js3源码中,我们可以看到很多实现特性开关的地方,比如 __FEATURE_OPTIONS_API__控制使用选项API还是 Composition API 的方式来编写代码

  globals: {
    __DEV__: true,
    __TEST__: true,
    __VERSION__: require('./package.json').version,
    __BROWSER__: false,
    __GLOBAL__: false,
    __ESM_BUNDLER__: true,
    __ESM_BROWSER__: false,
    __NODE_JS__: true,
    __SSR__: true,
    __FEATURE_OPTIONS_API__: true,
    __FEATURE_SUSPENSE__: true,
    __FEATURE_PROD_DEVTOOLS__: false,
    __COMPAT__: true,
    'ts-jest': {
      tsconfig: {
        target: 'esnext',
        sourceMap: true
      }
    }
  },
复制代码

2.5 错误处理

通过统一封装错误处理函数,将捕获的错误信息传递给用户,方便用户使用时对问题的追踪和定位,在Vue.js3源码中,我们可以看到统一处理错误的函数callWithErrorHandling

image.png

2.6 良好的TypeScript类型支持

代码即文档,编译器自动提示、一定程度上能够避免低级BUG,代码可维护性更强。

三、Vue3的设计思路

3.1 声明式UI框架

image.png

Vue.js3对应的方案(1)模板式

image.png

Vue.js3对应的方案 (2)虚拟DOM,JS对象来描述

import {h} from 'vue'
export default {
  render(){
   return h('h1',{onClick:handler})
  }
}
复制代码

h 函数就是一个辅助创建虚拟DOM的工具函数,render函数是组件的渲染函数

3.2 渲染器

渲染器的作用就是把虚拟DOM渲染成真实的DOM,工作原理是递归虚拟DOM对象,并调用原生的DOM API 来完成真是的DOM的创建。渲染器的精髓其实在于后续的更新,它会通过Diff算法找出更变点,并且只更新需要更新的内容。

image.png

3.3 组件的本质

组件就是一组DOM元素的封装,它可以是一个返回虚拟DOM的函数,也可以是一个对象,这个对象有一个函数用来产出组件需要渲染的虚拟DOM。所以组件的实现依赖于渲染器。

3.4 模板的工作原理

编译器把模板解析成一个渲染函数,渲染函数返回的虚拟DOM渲染成真实的DOM,Vue渲染页面的流程。 Vue.js的各个模块直接是相互关联、相互制约的,共同构成一个有机框架整体。

四 Vue.js 3.0 的优化集合

4.1 源码优化

image.png

4.2 性能优化

(1)源码体积优化

image.png

(2)数据劫持优化

image.png

(3)编译优化

image.png

4.3 语法 API 优化:Composition API

(1)优化逻辑组织

image.png

(2)优化逻辑复用

image.png

最后

正如标题所言,当你看完本文是否对Vue.js3源码阅读学习,会更加豁然呢,源码学习是个枯燥的过程,但是当你悟到了框架的门道,品味框架设计者的实现思路和细节,便会拨的云雾见月明,相应的你的原生 JavaScript 的功力也会得到提升,因此你的技术能力一定会往上迈一个台,手握升职加薪和跳槽的资本~

猜你喜欢

转载自juejin.im/post/7077369940038123550