- 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中有六种假值:
falsenullundefined0''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原始值:
stringnumberboolsymbol/*** 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') // trueisBuiltInTag('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') // trueisReservedAttribute('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; //没有就默认为0var 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。
