本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
学习目标
1. Vue2 源码 shared 模块中的几十个实用工具函数
2. 如何学习源码中优秀代码和思想,投入到自己的项目中
3. 如何学习 JavaScript 基础知识,会推荐很多学习资料
源码地址
- 看文章:初学者也能看懂的 Vue2 源码中那些实用的基础工具函数
- 在线vscode 查看 github1s.com/vuejs/vue/b…
- 打包后的工具函数 github.com/vuejs/vue/b…
源码解读
源码中使用了Flow 类型,它是 JavaScript 代码的静态类型检查器,Flow 通过静态类型注释检查代码是否存在错误。所以js文件通过在文件开头加以下注释以开启flow类型检测
// @flow
1. emptyObject
const emptyObject = Object.freeze({})
控制对象状态的方法
JavaScript 提供了三种冻结方法,最弱的一种是Object.preventExtensions,其次是Object.seal,最强的是Object.freeze
Object.preventExtensions方法可以使得一个对象无法再添加新的属性。检测方法为Object.isExtensibleObject.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。实质是把属性描述对象的configurable属性设为false,检测方法为Object.isSealedObject.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。检测方法为Object.isFrozen
2. isPrimitive
/**
* Check if value is primitive.
*/
export function isPrimitive (value: any): boolean %checks {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
-
基础类型检测,js基础类型包含
string、number、symbol和boolean -
typeof作为检测类型的一种方式,返回值包括
number、string、boolean、function、undefined和object六种 实用技巧: 检查一个没有声明的变量,而不报错。
// 错误写法
if (v) { // ... }
// 正确写法
if(typeof v === "undefined") {...}
3. toRawType 获取值原始类型
Object.prototype.toString返回一个值到底是什么类型
- 数值:返回
[object Number]。 - 字符串:返回
[object String]。 - 布尔值:返回
[object Boolean]。 - undefined:返回
[object Undefined]。 - null:返回
[object Null]。 - 数组:返回
[object Array]。 - arguments 对象:返回
[object Arguments]。 - 函数:返回
[object Function]。 - Error 对象:返回
[object Error]。 - Date 对象:返回
[object Date]。 - RegExp 对象:返回
[object RegExp]。 - 其他对象:返回
[object Object]。
/**
* Get the raw type string of a value, e.g., [object Object].
*/
const _toString = Object.prototype.toString
export function toRawType (value: any): string {
return _toString.call(value).slice(8, -1)
}
4. toString 将所有类型转换为字符串的方法
- null -> ''
- 数组或有原始toString方法的纯对象通过
JSON.stringify转换 - 其余类型使用String函数直接转换
/**
* Convert a value to a string that is actually rendered.
*/
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
JSON.stringify使用
JSON.stringify(value[, replacer [, space]])
JSON.stringify的可选参数
replacer 可以为 function|Array|null
space 可以为 number|string|null space为2 意为每一级缩进的空格数为2
⚠️ 使用注意事项
JSON.stringify({x: undefined, y: Object, s: fuction(){}, z: Symbol("")});
// '{}'
JSON.stringify([undefined, Object,fuction(){}, Symbol("")],NaN, Infinity);
// '[null,null,null,null,null,null]'
JSON.stringify({[Symbol("foo")]: "foo"});
// '{}'
JSON.stringify({[Symbol.for("foo")]: "foo"}, [Symbol.for("foo")]);
// '{}'
// 不可枚举的属性默认会被忽略:
JSON.stringify(
Object.create(
null,
{
x: { value: 'x', enumerable: false },
y: { value: 'y', enumerable: true }
}
)
);
// "{"y":"y"}"
5. isValidArrayIndex
有效的数组索引为有穷的正整数
/**
* Check if val is a valid array index.
*/
export function isValidArrayIndex (val: any): boolean {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
6. isPromise
满足已定义并且它的then和catch都为函数类型
export function isPromise (val: any): boolean {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
7. makeMap
由,号分割的字符串生成map并返回function 用于检测键名是否存在于map中
/**
* Make a map and return a function for checking if a key
* is in that map.
*/
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
/**
* Check if a tag is a built-in tag.
*/
export const isBuiltInTag = makeMap('slot,component', true)
Object.create方法有两个参数
第一个参数指定对象的__proto__
第二个参数为描述对象 与Object.defineProperties第二个参数相同 包括configurable、enumerable、value、writable、get和set
8. 驼峰、连字符转换
/**
* Camelize a hyphen-delimited string.
*/
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
/**
* Capitalize a string.
*/
export const capitalize = cached((str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
/**
* Hyphenate a camelCase string.
*/
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
})
正则学习推荐:juejin.cn/post/684490…
- 连字符转驼峰 全局匹配以-开头的字符
- 驼峰转连字符需要全局匹配非单词开始或结束位置的大些字母
- charAt 从一个字符串中返回指定的字符
- replace(regexp|substr, newSubStr|function) 使用示例如下:
function replacer(match, p1, p2, p3, offset, string) {
//match为匹配的子串 p1 为组1匹配的串, p2 为组2匹配的串, and p3 为组3匹配的串
return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer);
console.log(newString); // abc - 12345 - #$*%
9. looseEqual 递归各数据类型判断值相等
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
/**
* Check if two values are loosely equal - that is,
* if they are plain objects, do they have the same shape?
*/
export function looseEqual (a: any, b: any): boolean {
//判断值类型的相等
if (a === b) return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB) {
// 都是非null的object类型
try {
const isArrayA = Array.isArray(a)
const isArrayB = Array.isArray(b)
if (isArrayA && isArrayB) {
// 数组 长度及各项值相等
return a.length === b.length && a.every((e, i) => {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
// 日期类型 毫秒数相等
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
// 纯对象 key长度及每项key的value相等
const keysA = Object.keys(a)
const keysB = Object.keys(b)
return keysA.length === keysB.length && keysA.every(key => {
return looseEqual(a[key], b[key])
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
// 函数类型/NaN 需转换为字符串再比较
return String(a) === String(b)
} else {
return false
}
}
10. once 让函数仅调用一次
通过闭包实现执行次数控制
/**
* Ensure a function is called only once.
*/
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
总结
- 看懂是一回事,编码是另一回事,学习最主要的还是应用,只有会用了,它才是你的。所以我们在学习的时候要多想想为什么,如何用
- 其次,大多数的基础知识是我们早就掌握的,但是通过读源码,仍然可以查漏补缺,把容易忘记的和不了解的地方做个记录,这样,你的知识网络才会越来越全
- 然后就是对于自己的知识总结,要多拿出来看,这样才能记得更牢固 学习才会变得快乐