简介
本文档记录了我参加 vue3源码中的基础工具函数
源码共读的过程中的学习和思考。
- 本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
- 这是源码共读的第2期,链接:初学者也能看懂的 Vue3 源码中那些实用的基础工具函数。
参与目的:
- 探索源码,理解工作原理
- 学习和成长
- 提升技术水平
作为一名前端开发者,参与这次源码共读活动。这是一个很好的机会,与志同道合的开发者一起深入探索源码、理解工作原理,并在过程中学习和成长。
源码分析
1、源码准备
1.1、学习shared模块
可参考代码贡献指南 对应的文件路径是:vue-next/packages/shared/src/index.ts
也可以用github1s
访问,速度更快。github1s packages/shared/src/index.ts
1.2、按照项目指南 打包构建代码
我是直接使用的若川老师的源码。
如果不想克隆以下源码,也可以自己打包,可以参考以下要求进行打包构建。
Node.js 版本要求是 10+
Yarn 的版本要求是 1.x
Yarn 1.x 可自行下载
源码克隆
bash
#若川老师源码
git clone https://github.com/lxchuan12/vue-next-analysis.git
cd vue-next-analysis/vue-next
#官网源码
git clone https://github.com/vuejs/vue-next.git
cd vue-next
#全局安装yarn
npm install --global yarn
#安装工具包
yarn # install the dependencies of the project
#打包
yarn build
node -v
查看电脑上的node.js版本
克隆源码可以得到 vue-next/packages/shared/dist/shared.esm-bundler.js
,文件是纯js
文件。
2、工具函数
本文主要按照源码 vue-next/packages/shared/src/index.ts
的顺序来学习的
是通过ts
文件,查看使用函数的位置。同时在VSCode
运行调试JS代码,并使用了韩老师写的code runner
插件实现的共读调试
2.1、babelParserDefaultPlugins babel 解析默认插件
javascript
const babelParserDefaultPlugins = [
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
];
2.2、EMPTY_OBJ 空对象
javascript
const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {};
通常是一个预先定义好的空对象常量,用于在需要一个空对象作为默认值、占位符或者在某些特定场景下优化性能时使用。
通过Object.freeze
是 冻结对象,注:冻结的对象最外层是无法修改的
2.3、### EMPTY_ARR 空数组
javascript
const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];
是一个预先定义好的空数组常量,通常用于初始化变量、作为默认值或者在特定场景下优化性能,所以生产环境一般使用 []
2.4、NOOP 空函数
javascript
export const NOOP = () => {}
表示一个空函数,它不执行任何操作,仅仅是为了满足某种需要函数的接口设计或者用于占位等场景
2.5、NO 永远返回 false 的函数
javascript
const NO = () => false;
2.6、isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母
javascript
const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);
onRE
是正则。^
符号在开头,表示是什么开头。而在其他地方是指非。
$
符合在结尾,表示是以什么结尾。
[^a-z]
是指不是a
到z
的小写字母。
注:
插播一条若川老师推荐的正则迷你书,感兴趣的可以看一下
2.7、isModelListener 监听器
判断字符串是不是以onUpdate:
开头
javascript
const isModelListener = (key) => key.startsWith('onUpdate:');
是一个用于检测或验证某个对象是否符合模型变更监听器接口的函数。在某些编程框架或数据驱动的应用中,模型对象的数据变化时,可能会触发一系列事件或者通知给注册了这些变化事件的监听器。
可以参考阅读以下内容 ES6入门教程:字符串的新增方法
2.8、extend 继承 合并
javascript
const extend = Object.assign;
这个函数的主要目的是将一个或多个源对象(source object)的所有可枚举属性复制到目标对象(target object)上
2.9、remove 移除数组的一项
javascript
const remove = (arr, el) => {
const i = arr.indexOf(el);
if (i > -1) { arr.splice(i, 1);
}
};
Vue3 源码中并未直接提供一个名为 remove
的函数来移除数组的一项,但在 Vue2 中有一个 $set
和 $delete
方法可以间接实现数组元素的添加和删除。而在 Vue3 中,你可以直接使用 JavaScript 内置的数组方法如 splice
来实现
在 Vue3 的响应式编程中,由于它采用 Proxy 实现了深度监听,所以直接操作数组的这些原生方法(如 push
、pop
、shift
、unshift
、splice
等)会触发视图更新
2.10、 hasOwn 是不是自己本身所拥有的属性
javascript
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
hasOwn
是 Vue3 源码中的一个工具函数,用于判断对象是否拥有某个自身属性(不包括原型链上的属性)。这个函数主要用于避免在检查对象属性时触发 getter,它接收两个参数:一个对象和一个属性名。如果对象直接拥有指定的属性(非继承自原型),则返回 true
,否则返回 false
2.11、isArray 判断数组
javascript
const isArray = Array.isArray;
在 Vue3 源码中,并没有直接定义名为 isArray
的工具函数,因为 JavaScript 自身已经提供了这个全局方法。
2.12、isMap 判断是不是 Map 对象
javascript
const isMap = (val) => toTypeString(val) === '[object Map]';
isMap
函数用于判断一个值是否为 JavaScript 的 Map 对象。在 Vue3 源码中虽然没有直接提供 isMap
函数,但你可以根据以下示例实现:
2.13、isSet 判断是不是 Set 对象
javascript
const isSet = (val) => toTypeString(val) === '[object Set]';
// 使用示例
let mySet = new Set();
console.log(isSet(mySet)); // 输出: true
console.log(isSet([])); // 输出: false
Set
本身是一个构造函数,用来生成 Set
数据结构。
2.14、isDate 判断是不是 Date 对象
javascript
const isDate = (val) => val instanceof Date;
isDate
函数用于判断一个值是否为 JavaScript 的 Date 对象。这个函数通过 instanceof
操作符来检查传入的值是否是 Date 类型的实例
2.15、isFunction 判断是不是函数
javascript
const isFunction = (val) => typeof val === 'function';
isFunction
函数用于判断一个值是否为 JavaScript 的函数类型。这个函数通过 typeof
操作符来检查传入的值是否为 'function' 类型
2.16、isString 判断是不是字符串
javascript
onst isString = (val) => typeof val === 'string';
// 例子: isString(123) // false
// 例子: isString('123') // true
sString
函数用于判断一个值是否为 JavaScript 的字符串类型
2.17、isSymbol 判断是不是 Symbol
javascript
const isSymbol = (val) => typeof val === 'symbol';
// 使用示例
console.log(isSymbol(Symbol('key'))); // 输出: true
console.log(isSymbol('not a symbol')); // 输出: false
let sym = Symbol();
console.log(isSymbol(sym)); // 输出: true
通过 typeof
操作符来检查传入的值是否为 'symbol' 类型
2.18、isObject 判断是不是对象
javascript
const isObject = (val) => val !== null && typeof val === 'object';
// 使用示例
console.log(isObject({ a: 1 })); // 输出: true
console.log(isObject([1, 2, 3])); // 输出: true
console.log(isObject(function() {})); // 输出: true
console.log(isObject('not an object')); // 输出: false
console.log(isObject(null)); // 输出: false
let obj = {};
console.log(isObject(obj)); // 输出: true
用于判断一个值是否为 JavaScript 的对象类型(包括普通对象、数组、函数等,但不包括 null
)。这个函数首先检查传入的值是否不等于 null
,然后通过 typeof
操作符确认其类型是否为 'object'
2.19、isPromise 判断是不是 Promise
javascript
const isPromise = (val) => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
用于判断一个值是否为 JavaScript 的 Promise 对象。这个函数首先检查传入的值是否为对象且不为 null(使用 isObject(val)
),然后进一步确认该对象是否具有 then
和 catch
方法,并且这两个方法都必须是函数类型(使用 isFunction(val.then)
和 isFunction(val.catch)
)
2.20、objectToString 对象转字符串
javascript
const objectToString = Object.prototype.toString;
2.21、toTypeString 对象转字符串
javascript
const toTypeString = (value) => objectToString.call(value);
// 使用示例
console.log(toTypeString({})); // 输出: "[object Object]"
console.log(toTypeString([])); // 输出: "[object Array]"
call
是一个函数,第一个参数是 执行函数里面 this 指向
2.22、toRawType 对象转字符串 截取后几位
javascript
const toRawType = (value) => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1);
};
// 使用示例
console.log(toRawType({})); // 输出: "Object"
console.log(toRawType([])); // 输出: "Array"
toTypeString(value)
来获取一个值的详细类型字符串,然后截取其中的原始类型名称部分
2.23、isPlainObject 判断是不是纯粹的对象
javascript
const isPlainObject = (val) => toTypeString(val) === '[object Object]';
isPlainObject
函数用于判断一个值是否为普通对象(即由 {}
或 new Object()
创建的对象,不包括数组、函数、null、Date 等其他类型)。它通过调用 toTypeString(val)
并检查返回结果是否等于 [object Object]
来完成判断
2.24、isIntegerKey 判断是不是数字型的字符串key值
javascript
const isIntegerKey = (key) => isString(key) &&
key !== 'NaN' && key[0] !== '-' &&
'' + parseInt(key, 10) === key;
// 使用示例
console.log(isIntegerKey('123')); // 输出: true
console.log(isIntegerKey('-123')); // 输出: false(这里假设只接受非负整数)
console.log(isIntegerKey('abc')); // 输出: false
console.log(isIntegerKey('0')); // 输出: true
console.log(isIntegerKey('NaN')); // 输出: false
isIntegerKey
函数用于判断一个字符串是否表示一个整数值。函数执行以下检查:
- 使用
isString(key)
验证输入的key
是否为字符串类型。 - 检查字符串是否不等于
'NaN'
,因为这不是一个有效的整数。 - 确保字符串的第一个字符不是减号(
-
),因为在 JavaScript 中负数也可以表示整数,但在这里可能仅希望处理非负整数键值。 - 最后,将字符串转换为整数并用字符串形式比较原始输入。如果转换后的整数字符串与原始输入相同,则认为这个键是一个整数值。
2.25、makeMap && isReservedProp
javascript
/** * Make a map and return a function for checking if a key
* is in that map.
* IMPORTANT: all calls of this function must be prefixed with
* \/\*#\_\_PURE\_\_\*\/
* So that rollup can tree-shake them if necessary.
*/
function makeMap(str, expectsLowerCase) {
const map = Object.create(null);
const list = str.split(',');
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val];
}
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' + 'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted'
);
makeMap
函数内部:
- 创建一个空的
map
对象来存储键值对。 - 将
str
字符串按逗号分割成list
数组。 - 遍历
list
数组,并将每个元素作为键添加到map
中,其值设为true
。 - 根据
expectsLowerCase
参数创建并返回一个新函数:- 如果
expectsLowerCase
为true
,则返回的函数会接收一个参数val
,将其转换为小写后查找在map
中是否存在; - 如果
expectsLowerCase
为false
或未提供,则返回的函数直接接收一个参数val
查找在map
中是否存在。
- 如果
然后,通过调用 makeMap
函数创建了 isReservedProp
变量,它是一个用来判断 Vue 组件属性是否为保留属性(reserved prop)的函数。在这个例子中,提供的字符串包含了 Vue 生命周期钩子以及 "key" 和 "ref" 等特殊属性名。这意味着当我们使用 isReservedProp
函数检查一个属性名时,如果该属性名出现在上述列表中,函数将返回 true
,否则返回 false
2.26、 cacheStringFunction 缓存
javascript
const cacheStringFunction = (fn) => {
const cache = Object.create(null);
return ((str) => {
const hit = cache[str];
return hit || (cache[str] = fn(str)
);
});
};
cacheStringFunction
是一种优化策略,通常用于缓存字符串操作结果以提高性能
2.27、hasChanged 判断是不是有变化
javascript
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
hasChanged
函数在 Vue3 源码中并未直接出现,但在响应式原理中,Vue 会判断一个依赖属性值是否发生了变化。这种逻辑可以体现在 reactive
、ref
等响应式数据对象的更新检测机制中。
例如,在 Vue3 的响应式系统内部(以 reactive
为例),当一个被代理的对象属性被访问时,它会被追踪并记录到对应的依赖收集链路中;当这个属性值发生改变时,Vue 会触发相应的副作用函数执行,这些副作用函数通常关联着视图的更新或其他计算属性的重新求值。
2.28、invokeArrayFns 执行数组里的函数
javascript
const invokeArrayFns = (fns, arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg);
}
};
// 例子
const callbacks = [
(arg) => console.log('Callback 1:', arg),
(arg) => console.log('Callback 2:', arg),
(arg) => console.log('Callback 3:', arg)
];
// 调用 invokeArrayFns 并传入回调函数数组和参数
invokeArrayFns(callbacks, 'Hello, World!');
// 输出:
// Callback 1: Hello, World!
// Callback 2: Hello, World!
// Callback 3: Hello, World!
invokeArrayFns
函数用于遍历一个函数数组,并依次调用每个函数并将相同的参数传递给它们
2.29、def 定义对象属性
javascript
const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true, enumerable: false, value
});
};
def
函数是用于定义对象属性的一个便捷工具,它使用 Object.defineProperty
方法来设置对象的属性,并且指定了该属性的一些特性。在 Vue3 源码中(或与之相关的库)可能会用到这样的函数,用于实现一些特定需求。
这个函数接收三个参数:
obj
:要定义属性的对象。key
:要定义的属性名。value
:要赋予该属性的值。 通过Object.defineProperty
设置的属性具有以下特性:
configurable: true
表示可以修改属性的描述符,或者可以删除该属性。enumerable: false
表示该属性不会出现在for...in
循环和Object.keys()
中。value
被赋值为传入的第三个参数,即所要定义的属性值。
2.30、toNumber 转数字
javascript
const toNumber = (val) => {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
// 示例用法:
console.log(toNumber('123')); // 输出: 123 (转换成功)
console.log(toNumber('abc')); // 输出: "abc" (转换失败,返回原始值)
console.log(toNumber('3.14')); // 输出: 3.14 (转换成功)
console.log(toNumber(null)); // 输出: 0 (转换失败,但 parseFloat 返回的是 0,所以返回 0)
console.log(toNumber(undefined)); // 输出: NaN (转换失败,但 parseFloat 返回的是 NaN,所以返回原始值 undefined)
toNumber
函数的作用是将传入的值转换为数字类型。函数内部首先使用 parseFloat
方法尝试将输入值转换为浮点数,然后通过 isNaN
函数检查转换后的结果是否为 NaN(Not a Number),即无法表示为数字的情况。
如果转换成功且得到的是一个有效的数字,则返回这个数字;否则,说明原始值无法有效转换为数字,函数则返回原始值本身。
2.31、getGlobalThis 全局对象
javascript
let _globalThis;
const getGlobalThis = () => {
return (_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined' ?
globalThis : typeof self !== 'undefined' ?
self : typeof window !== 'undefined' ?
window : typeof global !== 'undefined' ?
global : {}));
};
getGlobalThis
函数的主要目的是获取全局对象(global object),即当前执行环境下的全局上下文。在不同的 JavaScript 环境中,全局对象可能有不同的名称:在浏览器环境中是 window
,在 Node.js 中是 global
,而在现代浏览器和一些支持 ES6 的环境中则是 globalThis
。 函数首先检查是否已经存在 _globalThis
变量缓存了全局对象,如果存在则直接返回;否则,它会按照以下顺序依次尝试查找并赋值给 _globalThis
:
- 检查是否存在全局变量
globalThis
。 - 如果不存在,则检查是否存在全局变量
self
(通常在 Web Workers 和浏览器环境下可用)。 - 再次检查是否存在全局变量
window
(仅在浏览器环境下可用)。 - 最后检查是否存在全局变量
global
(通常在 Node.js 环境下可用)
总结
在通过本次 Vue3 源码中的基础工具函数源码共读的过程中,我了解了以下要点:
-
类型判断 :Vue3 源码中包含了多种用于检查变量类型的工具函数,如
isObject
、isString
、isFunction
、isArray
、isMap
、isSet
等。这些函数利用 JavaScript 内置的typeof
、instanceof
或者Object.prototype.toString.call()
方法来准确识别各种数据类型。 -
对象操作工具:
hasOwn(obj, key)
:检查对象自身(不包括原型链)是否具有某个属性。def(obj, key, value)
:使用Object.defineProperty
定义对象的属性,并控制其可配置性、可枚举性和值。
-
字符串处理与转换:
camelize(str)
:将短横线分隔的字符串转为驼峰式命名。toTypeString(value)
和toRawType(value)
:获取并解析值的类型字符串,以进行更精确的类型判断。
-
数组和集合操作:
- 虽然没有直接提供
remove
函数移除数组元素,但可以通过原生的Array.prototype.splice
方法实现。 - 类似的,提供了
isMap
和isSet
函数来判断一个值是否为 Map 或 Set 对象。
- 虽然没有直接提供
-
响应式系统相关:
reactive
、ref
和computed
等函数构建了 Vue3 的响应式核心,它们内部会使用到很多基础工具函数来确保数据变化时视图更新。
-
缓存策略:
- 虽未找到名为
cacheStringFunction
的具体函数,但在 Vue3 源码中有类似的思想,即通过映射或缓存机制优化重复计算,提升性能。
- 虽未找到名为
-
执行回调函数:
invokeArrayFns(fns, arg)
用于遍历并同步执行数组中的所有函数,传入相同的参数。
-
全局上下文获取:
getGlobalThis()
函数用于在不同环境(浏览器、Node.js 等)下兼容地获取全局对象。
通过对 Vue3 源码中的基础工具函数进行深入阅读,不仅能够了解 Vue3 内部的工作原理,还能学习到如何高效地编写高质量且具有跨环境兼容性的工具函数。