后话
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
对于面试,说几句个人观点。
面试,说到底是一种考试。正如我们一直批判应试教育脱离教育的本质,为了面试学习技术也脱离了技术的初心。但考试对于人才选拔的有效性是毋庸置疑的,几千年来一直如此。除非你有实力向公司证明你足够优秀,否则,还是得乖乖准备面试。这也并不妨碍你在通过面试之后按自己的方式学习。
其实在面试准备阶段,个人的收获是很大的,我也认为这是一种不错的学习方式。首先,面试问题大部分基础而且深入,这些是平时工作的基础。就好像我们之前一直不明白学习语文的意义,但它的意义就在每天的谈话间。
所谓面试造火箭,工作拧螺丝。面试往往有更高的要求,也迫使我们更专心更深入地去学习一些知识,也何尝不是一种好事。
// bundle.js
function foo(obj) {
obj && obj.foo
}
foo();
可以看到,其中并不包含 bar
函数,这说明 Tree-Shaking 起了作用,由于我们并没有使用 bar
函数,因此它作为 dead-code
被删除了。但是如果我们仔细观察会发现,foo
函数的执行也没啥意义呀,就是读取了对象的值,所以它执行还是不执行也没有本质的区别呀,所以即使把这段代码删了,也对我们的应用没啥影响,那为什么 rollup 不把这段代码也作为 dead-code
移除呢?
这就涉及到 Tree-Shaking 中的第二个关键点,即副作用。如果一个函数调用会产生副作用,那么就不能将其移除。什么是副作用?简单地说副作用的意思是当调用函数的时候,会对外部产生影响,例如修改了全局变量。这时你可能会说,上面的代码明显是读取对象的值怎么会产生副作用呢?其实是有可能的,想想一下如果 obj
对象是一个通过 Proxy
创建的代理对象那么当我们读取对象属性时就会触发 Getter
,在 Getter
中是可能产生副作用的,例如我们在 Getter
中修改了某个全局变量。而到底会不会产生副作用,这个只有代码真正运行的时候才能知道, JS 本身是动态语言,想要静态的分析哪些代码是 dead-code
是一件很有难度的事儿,上面只是举了一个简单的例子。
正因为静态分析 JS 代码很困难,所以诸如 rollup
等这类工具都会给我提供一个机制,让我们有能力明确的告诉 rollup
:”放心吧,这段代码不会产生副作用,你可以放心移除它“,那具体怎么做呢?如下代码所示,我们修改 input.js
文件:
import {foo} from ‘./utils’
/#PURE/ foo()
注意这段注释代码 /*#__PURE_*_/
,该注释的作用就是用来告诉 rollup
对于 foo()
函数的调用不会产生副作用,你可以放心的对其进行 Tree-Shaking,此时再次执行构建命令并查看 bundle.js
文件你会发现它的内容是空的,这说明 Tree-Shaking 生效了。
基于这个案例大家应该明白的是,在编写框架的时候我们需要合理的使用 /*#__PURE_*_/
注释,如果你去搜索 Vue 的源码会发现它大量的使用了该注释,例如下面这句:
export const isHTMLTag = /#PURE/ makeMap(HTML_TAGS)
也许你会觉得这会不会对编写代码带来很大的心智负担?其实不会,这是因为通常产生副作用的代码都是模块内函数的顶级调用,什么是顶级调用呢?如下代码所示:
foo() // 顶级调用
function bar() {
foo() // 函数内调用
}
可以看到对于顶级调用来说是可能产生副作用的,但对于函数内调用来说只要函数 bar
没有被调用,那么 foo
函数的调用当然不会产生副作用。因此你会发现在 Vue 的源码中,基本都是在一些顶级调用的函数上使用 /*#__PURE__*/
注释的。当然该注释不仅仅作用与函数,它可以使用在任何语句上,这个注释也不是只有 rollup 才能识别,webpack 以及压缩工具如 terser
都能识别它。
框架应该输出怎样的构建产物
上文中我们提到 Vue 会为开发环境和生产环境输出不同的包,例如 vue.global.js
用于开发环境,它包含了必要的警告信息,而 vue.global.prod.js
用于生产环境,不包含警告信息。实际上 Vue 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物,这一节我们将讨论这些产物的用途以及在构建阶段如何输出这些产物。
不同类型的产物一定是有对应的需求背景的,因此我们从需求讲起。首先我们希望用户可以直接在 html 页面中使用 <script>
标签引入框架并使用:
为了能够实现这个需求,我们就需要输出一种叫做 IIFE
格式的资源,IIFE
的全称是 Immediately Invoked Function Expression
,即”立即调用的函数表达式“,可以很容易的用 JS 来表达:
(function () {
// …
}())
如上代码所示,这就是一个立即执行的函数表达式。实际上 vue.globale.js
文件就是 IIFE
形式的资源,大家可以看一下它的代码结构:
var Vue = (function(exports){
// …
exports.createApp = createApp;
// …
return exports
}({}))
这样当我们使用 <script>
标签直接引入 vue.global.js
文件后,那么全局变量 Vue
就是可用的了。
在 rollup
中我们可以通过配置 format: 'iife'
来实现输出这种形式的资源:
// rollup.config.js
const config = {
input: ‘input.js’,
output: {
file: ‘output.js’,
format: ‘iife’ // 指定模块形式
}
}
export default config
不过随着技术的发展和浏览器的支持,现在主流浏览器对原生 ESM 模块的支持都不错,所以用户除了能够使用 <script>
标签引用 IIFE
格式的资源外,还可以直接引如 ESM
格式的资源,例如 Vue3 会输出 vue.esm-browser.js
文件,用户可以直接用 <script>
标签引入:
为了输出 ESM 格式的资源就需要我们配置 rollup 的输出格式为:format: 'esm'
。
你可能已经注意到了,为什么 vue.esm-browser.js
文件中会有 -browser
字样,其实对于 ESM 格式的资源来说,Vue 还会输出一个 vue.esm-bundler.js
文件,其中 -browser
变成了 -bundler
。为什么这么做呢?我们知道无论是 rollup 还是 webpack 在寻找资源时,如果 package.json
中存在 module
字段,那么会优先使用 module
字段指向的资源来代替 main
字段所指向的资源。我们可以打开 Vue 源码中的 packages/vue/package.json
文件看一下:
{
“main”: “index.js”,
“module”: “dist/vue.runtime.esm-bundler.js”,
}
其中 module
字段指向的是 vue.runtime.esm-bundler.js
文件,意思就是说如果你的项目是使用 webpack 构建的,那你使用的 Vue 资源就是 vue.runtime.esm-bundler.js
,也就是说带有 -bundler
字样的 ESM 资源是给 rollup 或 webpack 等打包工具使用的,而带有 -browser
字样的 ESM 资源是直接给 <script type="module">
去使用的。
那他们之间的区别是什么呢?那这就不得不提到上文中的 __DEV__
常量,当构建用于 <script>
标签的 ESM 资源时,如果是用于开发环境,那么 __DEV__
会设置为 true
;如果是用于生产环境,那么 __DEV__
常量会被设置为 false
,从而被 Tree-Shaking 移除。但是当我们构建提供给打包工具的 ESM 格式的资源时,我们不能直接把 __DEV__
设置为 true
或 false
,而是使用 (process.env.NODE_ENV !== 'production')
替换掉 __DEV__
常量。例如下面的源码:
if (DEV) {
warn(useCssModule() is not supported in the global build.
)
}
在带有 -bundler
字样的资源中会变成:
if ((process.env.NODE_ENV !== ‘production’)) {
warn(useCssModule() is not supported in the global build.
)
}
这样用户侧的 webpack 配置可以自己决定构建资源的目标环境,但是最终的效果其实是一样的,这段代码也只会出现在开发环境。
用户除了可以直接使用 <script>
标签引入资源,我们还希望用户可以在 Node.js
中通过 require
语句引用资源,例如:
const Vue = require(‘vue’)
为什么会有这种需求呢?答案是服务端渲染,当服务端渲染时 Vue 的代码是运行在 Node.js
环境的,而非浏览器环境,在 Node.js
环境下资源的模块格式应该是 CommonJS
,简称 cjs
。为了能够输出 cjs
模块的资源,我们可以修改 rollup 的配置:format: 'cjs'
来实现:
// rollup.config.js
const config = {
input: ‘input.js’,
output: {
file: ‘output.js’,
format: ‘cjs’ // 指定模块形式
}
}
export default config
特性开关
在设计框架时,框架会提供诸多特性(或功能)给用户,例如我们提供 A、B、C 三个特性给用户,同时呢我们还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 true
和 false
来代表开启和关闭,那么将会带来很多收益:
-
对于用户关闭的特性,我们可以利用 Tree-Shaking 机制让其不包含在最终的资源中。
-
该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性而不用担心用不到这些特性的用户侧资源体积变大,同时当框架升级时,我们也可以通过特性开关来支持遗留的 API,这样新的用户可以选择不适用遗留的 API,从而做到用户侧资源最小化。
那怎么实现特性开关呢?其实很简单,原理和上文提到的 __DEV__
常量一样,本质是利用 rollup 的预定义常量插件来实现,那一段 Vue3 的 rollup 配置来看:
{
FEATURE_OPTIONS_API: isBundlerESMBuild ? __VUE_OPTIONS_API__
: true,
}
其中 __FEATURE_OPTIONS_API__
类似于 __DEV__
,我们可以在 Vue3 的源码中搜索,可以找到很多类似如下代码这样的判断分支:
// support for 2.x options
if (FEATURE_OPTIONS_API) {
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
当 Vue 构建资源时,如果构建的资源是用于给打包工具使用的话(即带有 -bundler
字样的资源),那么上面代码在资源中会变成:
// support for 2.x options
if (VUE_OPTIONS_API) { // 这一这里
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
其中 __VUE_OPTIONS_API__
就是一个特性开关,用户侧就可以通过设置 __VUE_OPTIONS_API__
来控制是否包含这段代码。通常用户可以使用 webpack.DefinePlugin
插件实现:
// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
VUE_OPTIONS_API: JSON.stringify(true) // 开启特性
})
最后再来详细解释一下 __VUE_OPTIONS_API__
开关是干嘛用的,在 Vue2 中我们编写的组件叫做组件选项 API:
export default {
data() {}, // data 选项
computed: {}, // computed 选项
// 其他选项…
}
但是在 Vue3 中,更推荐使用 Composition API 来编写代码,例如:
export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2) // 相当于 Vue2 中的 computed 选项
}
}
但是为了兼容 Vue2,在 Vue3 中仍然可以使用选项 API 的方式编写代码,但是对于明确知道自己不会使用选项 API 的用户来说,它们就可以选择使用 __VUE_OPTIONS_API__
开关来关闭该特性,这样在打包的时候 Vue 的这部分代码就不会包含在最终的资源中,从而减小资源体积。
错误处理
错误处理是开发框架的过程中非常重要的环节,框架的错误处理做的好坏能够直接决定用户应用程序的健壮性,同时还决定了用户开发应用时处理错误的心智负担。
为了让大家对错误处理的重要性有更加直观的感受,我们从一个小例子说起。假设我们开发了一个工具模块,代码如下:
// utils.js
export default {
foo(fn) {
fn && fn()
}
}
该模块导出一个对象,其中 foo
属性是一个函数,接收一个回调函数作为参数,调用 foo
函数时会执行回调函数,在用户侧使用时:
import utils from ‘utils.js’
utils.foo(() => {
// …
})
大家思考一下如果用户提供的回调函数在执行的时候出错了怎么办?此时有两个办法,其一是让用户自行处理,这需要用户自己去 try...catch
:
import utils from ‘utils.js’
utils.foo(() => {
try {
// …
} catch (e) {
// …
}
})
但是这对用户来说是增加了负担,试想一下如果 utils.js
不是仅仅提供了一个 foo
函数,而是提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。
第二种办法是我们代替用户统一处理错误,如下代码所示:
// utils.js
export default {
foo(fn) {
try {
fn && fn()
} catch(e) {/* … */}
},
bar(fn) {
try {
fn && fn()
} catch(e) {/* … */}
},
}
这中办法其实就是我们代替用户编写错误处理程序,实际上我们可以进一步封装错误处理程序为一个函数,假设叫它 callWithErrorHandling
:
// utils.js
export default {
foo(fn) {
callWithErrorHandling(fn)
},
bar(fn) {
callWithErrorHandling(fn)
},
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch (e) {
console.log(e)
}
}
可以看到代码变得简洁多了,但简洁不是目的,这么做真正的好处是,我们有机会为用户提供统一的错误处理接口,如下代码所示:
// utils.js
let handleError = null
export default {
foo(fn) {
callWithErrorHandling(fn)
},
// 用户可以调用该函数注册统一的错误处理函数
resigterErrorHandler(fn) {
handleError = fn
}
学习笔记
主要内容包括html,css,html5,css3,JavaScript,正则表达式,函数,BOM,DOM,jQuery,AJAX,vue等等
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
HTML/CSS
**HTML:**HTML基本结构,标签属性,事件属性,文本标签,多媒体标签,列表 / 表格 / 表单标签,其他语义化标签,网页结构,模块划分
**CSS:**CSS代码语法,CSS 放置位置,CSS的继承,选择器的种类/优先级,背景样式,字体样式,文本属性,基本样式,样式重置,盒模型样式,浮动float,定位position,浏览器默认样式
HTML5 /CSS3
**HTML5:**HTML5 的优势,HTML5 废弃元素,HTML5 新增元素,HTML5 表单相关元素和属性
**CSS3:**CSS3 新增选择器,CSS3 新增属性,新增变形动画属性,3D变形属性,CSS3 的过渡属性,CSS3 的动画属性,CSS3 新增多列属性,CSS3新增单位,弹性盒模型
JavaScript
**JavaScript:**JavaScript基础,JavaScript数据类型,算术运算,强制转换,赋值运算,关系运算,逻辑运算,三元运算,分支循环,switch,while,do-while,for,break,continue,数组,数组方法,二维数组,字符串
频】](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)**
HTML/CSS
**HTML:**HTML基本结构,标签属性,事件属性,文本标签,多媒体标签,列表 / 表格 / 表单标签,其他语义化标签,网页结构,模块划分
**CSS:**CSS代码语法,CSS 放置位置,CSS的继承,选择器的种类/优先级,背景样式,字体样式,文本属性,基本样式,样式重置,盒模型样式,浮动float,定位position,浏览器默认样式
[外链图片转存中…(img-b2aleBXa-1715891630965)]
HTML5 /CSS3
**HTML5:**HTML5 的优势,HTML5 废弃元素,HTML5 新增元素,HTML5 表单相关元素和属性
**CSS3:**CSS3 新增选择器,CSS3 新增属性,新增变形动画属性,3D变形属性,CSS3 的过渡属性,CSS3 的动画属性,CSS3 新增多列属性,CSS3新增单位,弹性盒模型
[外链图片转存中…(img-EonV7nbO-1715891630967)]
JavaScript
**JavaScript:**JavaScript基础,JavaScript数据类型,算术运算,强制转换,赋值运算,关系运算,逻辑运算,三元运算,分支循环,switch,while,do-while,for,break,continue,数组,数组方法,二维数组,字符串
[外链图片转存中…(img-gQQqebB4-1715891630967)]