- 1. 前言
- 2. 准备
- 3 看起来
- 3.1 emptyObject
- 3.2 isUndef
- 3.3 isDef
- 3.4 isTrue
- 3.5 isFalse
- 3.6 isPrimitive
- 3.7 isObject
- 3.8 toRawType
- 3.10 isRegExp
- 3.11 isValidArrayIndex
- 3.12 isPromise
- 3.13 toString 转字符串
- 3.14 toNumber
- 3.15 makeMap
- 3.16 isBuiltInTag
- 3.17 isReservedAttribute
- 3.18 remove
- 3.19 hasOwn
- 3.20 cached
- 3.21 camelize
- 3.22 capitalize
- 3.23 hyphenate
- 3.24 polyfillBind
- 3.25 toArray
- 3.26 extend
- 3.27 toObject
- 3.28 noop
- 3.29 no
- 3.30 identity
- 3.31 genStaticKeys
- 3.32 looseEqual
- 3.33 looseIndexOf
- 3.34 once
- 3.35 生命周期等
- 4. 学习资源
- 5. 总结
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
之前也有阅读过别的 源码,但是是第一次参与该活动记录笔记并发表..所以前面会先记录一些学习开源项目必要操作。(可能有点啰嗦
1.1 知识储备
虽然是读vue
源码,但是没学过vue
的也完全可以看懂,因为本次阅读的源码只是工具函数模块,只需要一定的原生JS基础即可,非常适合初学者。
- 基础数据类型
- 闭包
- 原型
- 正则表达式
- Object API
2. 准备
2.1 阅读指南
开源项目一般都能在README.md
或者 .github/contributing.md 找到贡献指南,一开始主要了解以下几点:
- 怎么把项目跑起来
- 项目目录是如何的
- 需要的知识储备
在 项目目录结构 描述中,找到shared
模块。
shared
: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).
对应的文件路径是:vue/vue/src/shared ;
2.2 降低难度
源码中使用了Flow 类型,没学过的可能会感到有点吃力,所以我们可以直接看打包后的 dist/vue.js 14行到379行
3 看起来
- 前面的几个都是非常简单的
- 如果代码中有中文注释,那就是本人自己写的,而不是源码里的,如有错漏之处,敬请指正~
3.1 emptyObject
一个空的冻结了的对象
一个冻结对象,被禁止⛔:var emptyObject = Object.freeze({});
- 修改
- 添加新属性
- 删除已有属性
- 修改属性的配置(
configurable、enumerable、writable、value
) - 修改原型
对于一个对象可以通过Object.isFrozen()
来判断对象是否冻结。
Object.isFrozen(emptyObject); // true
3.2 isUndef
判断是否是未定义
// These helpers produce better VM code in JS engines due to their
// explicitness and function inlining.
function isUndef (v) {
return v === undefined || v === null
}
Javascript
中有六种假值:
false
null
undefined
0
''
NaN
出于精准判断的考虑,vue
中封装多个函数来进行唯一的判断,这些都顾名思义,非常简单。
underscore 源码中的 isUndefined
[isUndefined](https://github.com/jashkenas/underscore/blob/58df1085cdb05cb0888719c5fe5493948604ab69/modules/isUndefined.js#L2)
:
// Is a given variable undefined?
export default function isUndefined(obj) {
return obj === void 0;
}
两者的作用是一样的,但是或许underscore
中的也许会更加安全——因为ES6之后全局的undefined
虽然不能赋值了,但是局部的undefined
仍是可以赋值的。
let f = ()=>{let undefined = 1;console.log(undefined)}
f() //1
3.3 isDef
判断是否是已经定义
function isDef (v) {
return v !== undefined && v !== null
}
3.4 isTrue
判断是否是 true
function isTrue (v) {
return v === true
}
3.5 isFalse
判断是否是 false
function isFalse (v) {
return v === false
}
3.6 isPrimitive
JS原始值:
string
number
bool
symbol
/**
* Check if value is primitive.
*/
function isPrimitive (value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
3.7 isObject
判断是否为对象,这个有很多种方法,面经种也经常出现。
这里主要是挑出null
,但是没有挑出其数组等一些东东,也就是仍然不一定是纯对象(下面有一个判断纯对象的)/**
* Quick object check - this is primarily used to tell
* Objects from primitive values when we know the value
* is a JSON-compliant type.
*/
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
3.8 toRawType
得到传入的参数的成原始类型_toString
先用_toString
把Object.prototype.toString
方法存下来,可以当作是缩写了一下,因为后面经常用到这个方法。——也说明了这个方法用处之大。 ```javascript /**- Get the raw type string of a value, e.g., [object Object]. */ var _toString = Object.prototype.toString;
function toRawType (value) { return _toString.call(value).slice(8, -1) }
得到`[object xxxxx]`后用`slice`裁出来后面的`xxxxx`,从而得到原始类型
<a name="pS6GH"></a>
## 3.9 isPlainObject
严格判断是否为纯对象,此时就把数组那些挑出去了
```javascript
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}
3.10 isRegExp
判断是否是正则表达式
function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
3.11 isValidArrayIndex
判断val
是否数组索引值是否有效,即parseFloat
解析后为大于等于0的整数。
——'1'
像这样的也是可以作为 JavaScript
数组索引值的。
/**
* Check if val is a valid array index.
*/
function isValidArrayIndex (val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
先将val
转换为字符串,再用parseFloat
解析字符串并返回浮点数。
parseFloat
此函数确定指定字符串中的第一个字符是否为数字。如果是,它会解析字符串直到到达数字的末尾,并将数字作为数字而不是字符串返回。
- 只返回字符串中的第一个数字
- 允许前导和尾随空格。
- 如果第一个字符不能转换为数字,parseFloat() 返回 NaN。
**Math.floor()**
可以理解为向下取整,Math.floor(n) === n
表示n为整数。[isFinite()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/isFinite)
判断其是否为有限数值。
3.12 isPromise
判断是否是 promise
function isPromise (val) {
return (
isDef(val) &&
// 判断val是否存在then和catch方法
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
通过检查其是否已经被定义并且有then
和catch
方法——一般其他东西我们也不会在里面写这两个方法。
当然如果这里判断其是否被定义改为 isObject
判断是否为一个对象,应该会更为恰当以及准确。
3.13 toString 转字符串
转换为字符串
/**
* Convert a value to a string that is actually rendered.
*/
function toString (val) {
return val == null
? '' // 为空的情况
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
//val为数组或者对象并且对象的 toString 方法是 Object.prototype.toString,用 JSON.stringify 转换。
? JSON.stringify(val, null, 2)
: String(val)
}
JSON.stringify() 和 String() 区别
这里如果val
为对象并且val.toString
符合条件,就使用JSON.stringify()
方法 ——将 JavaScript
对象转换为JSON
字符串。
语法:
JSON.stringify(value[, replacer[, space]])
- value:必需, 要转换的
JavaScript
值(通常为对象或数组)。- replacer:可选。用于转换结果的函数或数组。如果 replacer 为函数,则 JSON.stringify 将调用该函数,并传入每个成员的键和值。使用返回值而不是原始值。如果此函数返回 undefined,则排除成员。根对象的键是一个空字符串:””。如果 replacer 是一个数组,则仅转换该数组中具有键值的成员。成员的转换顺序与键在数组中的顺序一样。当 value 参数也为数组时,将忽略 replacer 数组。
- space:可选,文本添加缩进、空格和换行符,如果 space 是一个数字,则返回值文本在每个级别缩进指定数目的空格,如果 space 大于 10,则文本缩进 10 个空格。space 也可以使用非数字,如:\t。
这里就是将val
转换为字符串,并且每个级别缩进2个空格。
如果不满足上面的条件,则用String()
区别:
toString({}) //'{}'
toString([]) //'[]'
String({}) //'[object Object]'
String([]) //''
3.14 toNumber
利用parseFloat
将字符串转换为数字,如果转换失败就返回原始值——意味着传入对象等也是返回原对象等
function toNumber (val) {
var n = parseFloat(val);
return isNaN(n) ? val : n //isNaN 为 true 的话就说明转换失败
}
3.15 makeMap
这里开始复杂度略微多了一丢丢😗
生成一个 map
并且返回一个用于检查key
是否有效(此map
中有这个key
)的方法
/**
* Make a map and return a function for checking if a key
* is in that map.
*/
function makeMap (
str,//'a,b,12,nice,zhou'
expectsLowerCase //小写选项
) {
var map = Object.create(null); //返回一个空对象
// ['a', 'b', '12', 'nice', 'zhou']
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}
Object.create(null)
返回一个空对象——原型链都没有的那种。
3.16 isBuiltInTag
检查是否为内置的tag
,实际上就是上一个makeMap
方法的运用。
/**
* Check if a tag is a built-in tag.
*/
var isBuiltInTag = makeMap('slot,component', true); //第二个选项为true,表示不区分大小写
//效果:
isBuiltInTag('component') // true
isBuiltInTag('Slot') // true
3.17 isReservedAttribute
检查属性是否为保留属性,也是makeMap
方法的运用
/**
* Check if an attribute is a reserved attribute.
*/
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); //不设置第二个参数默认为区分大小写
//效果:
isReservedAttribute('is') // true
isReservedAttribute('IS') // undefined
3.18 remove
移除数组中的中一项
/**
* Remove an item from an array.
*/
function remove (arr, item) { // 传入数组,待删除的值
if (arr.length) {
var index = arr.indexOf(item); //找到该值的下标
if (index > -1) {
return arr.splice(index, 1) // 用slice删除
}
}
}
关于 splice 的性能问题
学过数据结构的应该知道,数组这种用连续的内存空间存储的数据结构,用splice
删除第一项的话,数组后面的元素都需要移动位置。
axios 源码中拦截器的性能优化
因为拦截器是用户自定义的,理论上可以有无数个,所以做性能考虑是必要的。
axios InterceptorManager拦截器源码 中拦截器也是用数组存储的,而移除拦截器的时候,只是将该项的值置为null
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
最后执行时,值为null
不执行
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
3.19 hasOwn
检测是否是自己的属性——自己本身就具有的,而不是在原型链上往少找的
/**
* Check whether an object has the property.
*/
var hasOwnProperty = Object.prototype.hasOwnProperty; //这里也是存了一个变量来减少后面的代码量
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
原型相关的API
这里利用Object.prototype.hasOwnProperty
这个API+硬绑定call
来判断key
是否为自身的属性
除了这个API 以外还有
Object.getPrototypeOf
: 得到原型Object.setPrototypeOf
:设置原型Object.isPrototypeOf
:判断是否是它的原型3.20 cached
一个高阶函数,参数是函数,返回的也是一个函数,用闭包作为公共变量或缓存,感觉挺妙的
返回的函数/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
//例子:
let A = cached(f)
A(1)
A
每次执行的参数都会被cached
的参数f
执行,并且缓存到hit
中——对于可能需要多次执行的相同操作的——即传入相同的参数,就可以起到性能优化的效果:直接从缓存中获取而不是再次执行f
。
也是一种空间换时间的性能优化策略~这个方法在后面也用了很多次3.21 camelize
接下来几个都是与正则表达式有关
连字符转小驼峰 camelize('border-radius') _// 'borderRadius'_
/**
* Camelize a hyphen-delimited string.
*/
var camelizeRE = /-(\w)/g; //'-'+[^A-Za-z0-9_]且全文搜索
var camelize = cached(function (str) { //这里就用到了上面的 cached 方法
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
str.replace
replace()
字符替换函数,跟正则结合实现强大的字符替换效果,非常常用,
第二个参数 replacement是函数而不是字符串时,每次匹配都调用该函数,将这个函数的返回的字符串将作为替换文本使用,非常实用
有一个陷阱:当 replace 的第一个参数是字符串时,它仅替换第一个匹配项。 对于需要“智能”替换的场景,第二个参数可以是一个函数。 每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。 该函数
func(match, p1, p2, ..., pn, offset, input, groups)
带参数调用:
match
- 完整匹配项p1, p2, ..., pn
- 分组的内容(如有),offset
- 匹配项的位置,input
- 源字符串,groups
- 所指定分组的对象。即如果正则表达式中没有括号,则只有 3 个参数:
func(match, offset, input)
。
camelize
这里是有括号分组的,所以才需要用_
占掉一个位置—— 即完整匹配项,得到到c
就第一个分组里的内容,即'-'
后的第一个字母
3.22 capitalize
首字母转大写
/**
* Capitalize a string.
*/
var capitalize = cached(function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
});
str.charAt(0)
获得字符串中的首字母,toUpperCase()
大写后拼接上后面的字符串
3.23 hyphenate
小驼峰转连字符,也就是camelize
的逆操作:hyphenate('borderRadius') _// '_border-radius_'_
/**
* Hyphenate a camelCase string.
*/
var hyphenateRE = /\B([A-Z])/g; //全部大写字母
var hyphenate = cached(function (str) {
return str.replace(hyphenateRE, '-$1').toLowerCase()
});
'$1'
即匹配到的那个组,也就是找到那个大写字母'R'
后转为'-R'
,再用toLowerCase
转为'-r'
3.24 polyfillBind
bind
的垫片
垫片:在计算机编程中,垫片是一个小型库,它透明地拦截API,更改传递的参数,处理操作本身或将操作重定向到其他地方。 垫片通常在API的行为发生变化时出现,从而导致仍然依赖于旧功能的旧应用程序的兼容性问题。 在这些情况下,旧的API仍然可以通过较新代码之上的瘦兼容层来支持。 垫片也可用于在不同的软件平台上运行程序。
/**
* Simple bind polyfill for environments that do not support it,
* e.g., PhantomJS 1.x. Technically, we don't need this anymore
* since native bind is now performant enough in most browsers.
* But removing it would mean breaking code that was able to run in
* PhantomJS 1.x, so this must be kept for backward compatibility.
*/
/* istanbul ignore next */
function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
//据说参数多用 apply 合适,少用 call 可以提高性能——其实从传参方式来看也确实apply会更方便
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length;
return boundFn
}
function nativeBind (fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind //支持bind方法自然就直接用原生的
: polyfillBind;
也就是兼容一些老旧到不能支持原生bind
方法的浏览器,同时兼容传参方法
3.25 toArray
把类数组转成真正的数组,支持起始位置,默认起始位置为0
/**
* Convert an Array-like object to a real Array.
*/
function toArray (list, start) {
start = start || 0; //没有就默认为0
var i = list.length - start; //总共要操作多少
var ret = new Array(i);
while (i--) {
ret[i] = list[i + start];
}
return ret
}
3.26 extend
将属性合并到目标对象上,并返回结果,目标对象本身也会改变
/**
* Mix properties into target object.
*/
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
和[Object.assign](https://zh.javascript.info/object-copy#cloning-and-merging-object-assign)
差不多,它还可以合并多个
3.27 toObject
将一个数组转换为一个对象
/**
* Merge an Array of Objects into a single Object.
*/
function toObject (arr) {
var res = {};
for (var i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i]); //意味可以覆盖
}
}
return res
}
//效果
toObject(['被覆盖', '第四个4','留23','1'])//{0: '1', 1: '2', 2: '3', 3: '4'}
默认的key
就是下标
这个方法感觉平时自己几乎没用过类似的,但是在源码中还是有多次出现过的
3.28 noop
空函数
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
function noop (a, b, c) {}
3.29 no
始终返回 false
/**
* Always return false.
*/
var no = function (a, b, c) { return false; };
3.30 identity
返回参数本身
/**
* Return the same value.
*/
var identity = function (_) { return _; };
3.31 genStaticKeys
生成静态属性
/**
* Generate a string containing static keys from compiler modules.
*/
function genStaticKeys (modules) {
return modules.reduce(function (keys, m) { //keys上一次函数调用的结果,m:当前的数组元素
return keys.concat(m.staticKeys || [])
}, []).join(',') //最后用','粘合为字符串
}
3.32 looseEqual
宽松相等,和==
差不多,但这样也可以用于在其他方法中协助封装
原生JS 中会存在引用类型内容看起来相等但是严格相等时却不一样
但有些时候,我们并不需要如此严格相等:对数组、日期、对象进行递归比对。如果内容完全相等则宽松相等。
/**
* Check if two values are loosely equal - that is,
* if they are plain objects, do they have the same shape?
*/
function looseEqual (a, b) {
if (a === b) { return true }
var isObjectA = isObject(a);
var isObjectB = isObject(b);
if (isObjectA && isObjectB) { //两个都是对象的话
try {
var isArrayA = Array.isArray(a);
var isArrayB = Array.isArray(b);
if (isArrayA && isArrayB) {//两个都是数组
return a.length === b.length && a.every(function (e, i) {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {//两个都是日期类型
return a.getTime() === b.getTime()//调用日期对象上的方法得到数据并进行比较
} else if (!isArrayA && !isArrayB) {//两个都不是数组 也不是日期
var keysA = Object.keys(a);
var keysB = Object.keys(b);
//属性数量相等 && 判断A中键值对B中是否存在
return keysA.length === keysB.length && keysA.every(function (key) {
return looseEqual(a[key], b[key]) //递归宽松相等比较
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) { //两个都不是对象类型的话
return String(a) === String(b) //就转换为字符串进行比较
} else {
return false
}
}
3.33 looseIndexOf
宽松相等的 indexOf
,原生的是严格相等
/**
* Return the first index at which a loosely equal value can be
* found in the array (if value is a plain object, the array must
* contain an object of the same shape), or -1 if it is not present.
*/
function looseIndexOf (arr, val) {
for (var i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) { return i } //满足宽松相等就给了
}
return -1
}
3.34 once
借助变量储存标记called
,从而确保函数只执行一次
/**
* Ensure a function is called only once.
*/
function once (fn) {
var called = false;
return function () {
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
3.35 生命周期等
生命周期的一些变量就不粘代码了,模块剩下的部分也复杂起来了,暂且放下🤐
4. 学习资源
- 初学者也能看懂的 Vue2 源码中那些实用的基础工具函数
- 现代JavaScript教程
5. 总结
如果你是第一次阅读开源项目的源码,读完就会发现——这么简单?实际上确实如此,依据个人经验(不一定对🤣),一些工具函数的源码是最容易读懂,并且更实用——自己能在开发中用上的,完全可以打造一个属于自己的utils
。