本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

1. 前言

之前也有阅读过别的 源码,但是是第一次参与该活动记录笔记并发表..所以前面会先记录一些学习开源项目必要操作。(可能有点啰嗦


1.1 知识储备

虽然是读vue源码,但是没学过vue的也完全可以看懂,因为本次阅读的源码只是工具函数模块,只需要一定的原生JS基础即可,非常适合初学者。

  • 基础数据类型
  • 闭包
  • 原型
  • 正则表达式
  • Object API

    2. 准备

    2.1 阅读指南

    开源项目一般都能在README.md 或者 .github/contributing.md 找到贡献指南,一开始主要了解以下几点:
  1. 怎么把项目跑起来
  2. 项目目录是如何的
  3. 需要的知识储备

项目目录结构 描述中,找到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

    一个空的冻结了的对象
    1. var emptyObject = Object.freeze({});
    一个冻结对象,被禁止⛔:
  1. 修改
  2. 添加新属性
  3. 删除已有属性
  4. 修改属性的配置(configurable、enumerable、writable、value
  5. 修改原型

对于一个对象可以通过Object.isFrozen()来判断对象是否冻结。

  1. Object.isFrozen(emptyObject); // true

可以用来做一些判断

3.2 isUndef

判断是否是未定义

  1. // These helpers produce better VM code in JS engines due to their
  2. // explicitness and function inlining.
  3. function isUndef (v) {
  4. return v === undefined || v === null
  5. }

Javascript中有六种假值:

  1. false
  2. null
  3. undefined
  4. 0
  5. ''
  6. NaN

出于精准判断的考虑,vue中封装多个函数来进行唯一的判断,这些都顾名思义,非常简单。

underscore 源码中的 isUndefined

[isUndefined](https://github.com/jashkenas/underscore/blob/58df1085cdb05cb0888719c5fe5493948604ab69/modules/isUndefined.js#L2)

  1. // Is a given variable undefined?
  2. export default function isUndefined(obj) {
  3. return obj === void 0;
  4. }

两者的作用是一样的,但是或许underscore中的也许会更加安全——因为ES6之后全局的undefined虽然不能赋值了,但是局部的undefined仍是可以赋值的。

  1. let f = ()=>{let undefined = 1;console.log(undefined)}
  2. f() //1

3.3 isDef

判断是否是已经定义

  1. function isDef (v) {
  2. return v !== undefined && v !== null
  3. }

3.4 isTrue

判断是否是 true

  1. function isTrue (v) {
  2. return v === true
  3. }

3.5 isFalse

判断是否是 false

  1. function isFalse (v) {
  2. return v === false
  3. }

3.6 isPrimitive

利用typeof判断值是否是原始值

JS原始值:

  1. string
  2. number
  3. bool
  4. symbol
    1. /**
    2. * Check if value is primitive.
    3. */
    4. function isPrimitive (value) {
    5. return (
    6. typeof value === 'string' ||
    7. typeof value === 'number' ||
    8. typeof value === 'symbol' ||
    9. typeof value === 'boolean'
    10. )
    11. }

    3.7 isObject

    判断是否为对象,这个有很多种方法,面经种也经常出现。
    这里主要是挑出null,但是没有挑出其数组等一些东东,也就是仍然不一定是纯对象(下面有一个判断纯对象的
    1. /**
    2. * Quick object check - this is primarily used to tell
    3. * Objects from primitive values when we know the value
    4. * is a JSON-compliant type.
    5. */
    6. function isObject (obj) {
    7. return obj !== null && typeof obj === 'object'
    8. }

    3.8 toRawType

    得到传入的参数的成原始类型

    _toString

    先用_toStringObject.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) }

  1. 得到`[object xxxxx]`后用`slice`裁出来后面的`xxxxx`,从而得到原始类型
  2. <a name="pS6GH"></a>
  3. ## 3.9 isPlainObject
  4. 严格判断是否为纯对象,此时就把数组那些挑出去了
  5. ```javascript
  6. /**
  7. * Strict object type check. Only returns true
  8. * for plain JavaScript objects.
  9. */
  10. function isPlainObject (obj) {
  11. return _toString.call(obj) === '[object Object]'
  12. }

3.10 isRegExp

判断是否是正则表达式

  1. function isRegExp (v) {
  2. return _toString.call(v) === '[object RegExp]'
  3. }

3.11 isValidArrayIndex

判断val是否数组索引值是否有效,即parseFloat解析后为大于等于0的整数。
——'1'像这样的也是可以作为 JavaScript 数组索引值的。

  1. /**
  2. * Check if val is a valid array index.
  3. */
  4. function isValidArrayIndex (val) {
  5. var n = parseFloat(String(val));
  6. return n >= 0 && Math.floor(n) === n && isFinite(val)
  7. }

先将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

  1. function isPromise (val) {
  2. return (
  3. isDef(val) &&
  4. // 判断val是否存在then和catch方法
  5. typeof val.then === 'function' &&
  6. typeof val.catch === 'function'
  7. )
  8. }

通过检查其是否已经被定义并且有thencatch方法——一般其他东西我们也不会在里面写这两个方法。
当然如果这里判断其是否被定义改为 isObject判断是否为一个对象,应该会更为恰当以及准确。

3.13 toString 转字符串

转换为字符串

  1. /**
  2. * Convert a value to a string that is actually rendered.
  3. */
  4. function toString (val) {
  5. return val == null
  6. ? '' // 为空的情况
  7. : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
  8. //val为数组或者对象并且对象的 toString 方法是 Object.prototype.toString,用 JSON.stringify 转换。
  9. ? JSON.stringify(val, null, 2)
  10. : String(val)
  11. }

JSON.stringify() 和 String() 区别

这里如果val为对象并且val.toString 符合条件,就使用JSON.stringify()方法 ——将 JavaScript对象转换为JSON字符串。
语法:

  1. 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()
区别:

  1. toString({}) //'{}'
  2. toString([]) //'[]'
  3. String({}) //'[object Object]'
  4. String([]) //''

3.14 toNumber

利用parseFloat将字符串转换为数字,如果转换失败就返回原始值——意味着传入对象等也是返回原对象等

  1. function toNumber (val) {
  2. var n = parseFloat(val);
  3. return isNaN(n) ? val : n //isNaN 为 true 的话就说明转换失败
  4. }

3.15 makeMap

这里开始复杂度略微多了一丢丢😗

生成一个 map并且返回一个用于检查key是否有效(此map中有这个key)的方法

  1. /**
  2. * Make a map and return a function for checking if a key
  3. * is in that map.
  4. */
  5. function makeMap (
  6. str,//'a,b,12,nice,zhou'
  7. expectsLowerCase //小写选项
  8. ) {
  9. var map = Object.create(null); //返回一个空对象
  10. // ['a', 'b', '12', 'nice', 'zhou']
  11. var list = str.split(',');
  12. for (var i = 0; i < list.length; i++) {
  13. map[list[i]] = true;
  14. }
  15. return expectsLowerCase
  16. ? function (val) { return map[val.toLowerCase()]; }
  17. : function (val) { return map[val]; }
  18. }

Object.create(null)返回一个空对象——原型链都没有的那种。

3.16 isBuiltInTag

检查是否为内置的tag,实际上就是上一个makeMap方法的运用。

  1. /**
  2. * Check if a tag is a built-in tag.
  3. */
  4. var isBuiltInTag = makeMap('slot,component', true); //第二个选项为true,表示不区分大小写
  5. //效果:
  6. isBuiltInTag('component') // true
  7. isBuiltInTag('Slot') // true

3.17 isReservedAttribute

检查属性是否为保留属性,也是makeMap方法的运用

  1. /**
  2. * Check if an attribute is a reserved attribute.
  3. */
  4. var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); //不设置第二个参数默认为区分大小写
  5. //效果:
  6. isReservedAttribute('is') // true
  7. isReservedAttribute('IS') // undefined

3.18 remove

移除数组中的中一项

  1. /**
  2. * Remove an item from an array.
  3. */
  4. function remove (arr, item) { // 传入数组,待删除的值
  5. if (arr.length) {
  6. var index = arr.indexOf(item); //找到该值的下标
  7. if (index > -1) {
  8. return arr.splice(index, 1) // 用slice删除
  9. }
  10. }
  11. }

关于 splice 的性能问题

学过数据结构的应该知道,数组这种用连续的内存空间存储的数据结构,用splice删除第一项的话,数组后面的元素都需要移动位置。

axios 源码中拦截器的性能优化

因为拦截器是用户自定义的,理论上可以有无数个,所以做性能考虑是必要的

axios InterceptorManager拦截器源码 中拦截器也是用数组存储的,而移除拦截器的时候,只是将该项的值置为null

  1. /**
  2. * Remove an interceptor from the stack
  3. *
  4. * @param {Number} id The ID that was returned by `use`
  5. */
  6. InterceptorManager.prototype.eject = function eject(id) {
  7. if (this.handlers[id]) {
  8. this.handlers[id] = null;
  9. }
  10. };

最后执行时,值为null不执行

  1. /**
  2. * Iterate over all the registered interceptors
  3. *
  4. * This method is particularly useful for skipping over any
  5. * interceptors that may have become `null` calling `eject`.
  6. *
  7. * @param {Function} fn The function to call for each interceptor
  8. */
  9. InterceptorManager.prototype.forEach = function forEach(fn) {
  10. utils.forEach(this.handlers, function forEachHandler(h) {
  11. if (h !== null) {
  12. fn(h);
  13. }
  14. });
  15. };

实现用空间换时间的性能优化效果。

3.19 hasOwn

检测是否是自己的属性——自己本身就具有的,而不是在原型链上往少找的

  1. /**
  2. * Check whether an object has the property.
  3. */
  4. var hasOwnProperty = Object.prototype.hasOwnProperty; //这里也是存了一个变量来减少后面的代码量
  5. function hasOwn (obj, key) {
  6. return hasOwnProperty.call(obj, key)
  7. }

原型相关的API

这里利用Object.prototype.hasOwnProperty这个API+硬绑定call来判断key是否为自身的属性
除了这个API 以外还有

  • Object.getPrototypeOf: 得到原型
  • Object.setPrototypeOf:设置原型
  • Object.isPrototypeOf:判断是否是它的原型

    3.20 cached

    一个高阶函数,参数是函数,返回的也是一个函数,用闭包作为公共变量或缓存,感觉挺妙的
    1. /**
    2. * Create a cached version of a pure function.
    3. */
    4. function cached (fn) {
    5. var cache = Object.create(null);
    6. return (function cachedFn (str) {
    7. var hit = cache[str];
    8. return hit || (cache[str] = fn(str))
    9. })
    10. }
    11. //例子:
    12. let A = cached(f)
    13. A(1)
    返回的函数A每次执行的参数都会被cached的参数f执行,并且缓存到hit中——对于可能需要多次执行的相同操作的——即传入相同的参数,就可以起到性能优化的效果:直接从缓存中获取而不是再次执行f
    也是一种空间换时间的性能优化策略~这个方法在后面也用了很多次

    3.21 camelize

    接下来几个都是与正则表达式有关

连字符转小驼峰 camelize('border-radius') _// 'borderRadius'_

  1. /**
  2. * Camelize a hyphen-delimited string.
  3. */
  4. var camelizeRE = /-(\w)/g; //'-'+[^A-Za-z0-9_]且全文搜索
  5. var camelize = cached(function (str) { //这里就用到了上面的 cached 方法
  6. return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
  7. });

str.replace

replace()字符替换函数,跟正则结合实现强大的字符替换效果,非常常用,
第二个参数 replacement是函数而不是字符串时,每次匹配都调用该函数,将这个函数的返回的字符串将作为替换文本使用,非常实用

有一个陷阱:当 replace 的第一个参数是字符串时,它仅替换第一个匹配项。 对于需要“智能”替换的场景,第二个参数可以是一个函数。 每次匹配都会调用这个函数,并且返回的值将作为替换字符串插入。 该函数 func(match, p1, p2, ..., pn, offset, input, groups)带参数调用:

  1. match- 完整匹配项
  2. p1, p2, ..., pn- 分组的内容(如有),
  3. offset- 匹配项的位置,
  4. input- 源字符串,
  5. groups- 所指定分组的对象。

即如果正则表达式中没有括号,则只有 3 个参数:func(match, offset, input)

camelize这里是有括号分组的,所以才需要用_占掉一个位置—— 即完整匹配项,得到到c就第一个分组里的内容,即'-'后的第一个字母

3.22 capitalize

首字母转大写

  1. /**
  2. * Capitalize a string.
  3. */
  4. var capitalize = cached(function (str) {
  5. return str.charAt(0).toUpperCase() + str.slice(1)
  6. });

str.charAt(0)获得字符串中的首字母,toUpperCase()大写后拼接上后面的字符串

3.23 hyphenate

小驼峰转连字符,也就是camelize的逆操作:hyphenate('borderRadius') _// '_border-radius_'_

  1. /**
  2. * Hyphenate a camelCase string.
  3. */
  4. var hyphenateRE = /\B([A-Z])/g; //全部大写字母
  5. var hyphenate = cached(function (str) {
  6. return str.replace(hyphenateRE, '-$1').toLowerCase()
  7. });

'$1'即匹配到的那个组,也就是找到那个大写字母'R'后转为'-R',再用toLowerCase转为'-r'

3.24 polyfillBind

bind的垫片

垫片:在计算机编程中,垫片是一个小型库,它透明地拦截API,更改传递的参数,处理操作本身或将操作重定向到其他地方。 垫片通常在API的行为发生变化时出现,从而导致仍然依赖于旧功能的旧应用程序的兼容性问题。 在这些情况下,旧的API仍然可以通过较新代码之上的瘦兼容层来支持。 垫片也可用于在不同的软件平台上运行程序。

  1. /**
  2. * Simple bind polyfill for environments that do not support it,
  3. * e.g., PhantomJS 1.x. Technically, we don't need this anymore
  4. * since native bind is now performant enough in most browsers.
  5. * But removing it would mean breaking code that was able to run in
  6. * PhantomJS 1.x, so this must be kept for backward compatibility.
  7. */
  8. /* istanbul ignore next */
  9. function polyfillBind (fn, ctx) {
  10. function boundFn (a) {
  11. var l = arguments.length;
  12. return l
  13. ? l > 1
  14. //据说参数多用 apply 合适,少用 call 可以提高性能——其实从传参方式来看也确实apply会更方便
  15. ? fn.apply(ctx, arguments)
  16. : fn.call(ctx, a)
  17. : fn.call(ctx)
  18. }
  19. boundFn._length = fn.length;
  20. return boundFn
  21. }
  22. function nativeBind (fn, ctx) {
  23. return fn.bind(ctx)
  24. }
  25. var bind = Function.prototype.bind
  26. ? nativeBind //支持bind方法自然就直接用原生的
  27. : polyfillBind;

也就是兼容一些老旧到不能支持原生bind方法的浏览器,同时兼容传参方法

3.25 toArray

把类数组转成真正的数组,支持起始位置,默认起始位置为0

  1. /**
  2. * Convert an Array-like object to a real Array.
  3. */
  4. function toArray (list, start) {
  5. start = start || 0; //没有就默认为0
  6. var i = list.length - start; //总共要操作多少
  7. var ret = new Array(i);
  8. while (i--) {
  9. ret[i] = list[i + start];
  10. }
  11. return ret
  12. }

3.26 extend

将属性合并到目标对象上,并返回结果,目标对象本身也会改变

  1. /**
  2. * Mix properties into target object.
  3. */
  4. function extend (to, _from) {
  5. for (var key in _from) {
  6. to[key] = _from[key];
  7. }
  8. return to
  9. }

[Object.assign](https://zh.javascript.info/object-copy#cloning-and-merging-object-assign)差不多,它还可以合并多个

3.27 toObject

将一个数组转换为一个对象

  1. /**
  2. * Merge an Array of Objects into a single Object.
  3. */
  4. function toObject (arr) {
  5. var res = {};
  6. for (var i = 0; i < arr.length; i++) {
  7. if (arr[i]) {
  8. extend(res, arr[i]); //意味可以覆盖
  9. }
  10. }
  11. return res
  12. }
  13. //效果
  14. toObject(['被覆盖', '第四个4','留23','1'])//{0: '1', 1: '2', 2: '3', 3: '4'}

默认的key就是下标

这个方法感觉平时自己几乎没用过类似的,但是在源码中还是有多次出现过的

3.28 noop

空函数

  1. /**
  2. * Perform no operation.
  3. * Stubbing args to make Flow happy without leaving useless transpiled code
  4. * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
  5. */
  6. function noop (a, b, c) {}

3.29 no

始终返回 false

  1. /**
  2. * Always return false.
  3. */
  4. var no = function (a, b, c) { return false; };

3.30 identity

返回参数本身

  1. /**
  2. * Return the same value.
  3. */
  4. var identity = function (_) { return _; };

3.31 genStaticKeys

生成静态属性

  1. /**
  2. * Generate a string containing static keys from compiler modules.
  3. */
  4. function genStaticKeys (modules) {
  5. return modules.reduce(function (keys, m) { //keys上一次函数调用的结果,m:当前的数组元素
  6. return keys.concat(m.staticKeys || [])
  7. }, []).join(',') //最后用','粘合为字符串
  8. }

3.32 looseEqual

宽松相等,和==差不多,但这样也可以用于在其他方法中协助封装
原生JS 中会存在引用类型内容看起来相等但是严格相等时却不一样
但有些时候,我们并不需要如此严格相等:对数组、日期、对象进行递归比对。如果内容完全相等则宽松相等。

  1. /**
  2. * Check if two values are loosely equal - that is,
  3. * if they are plain objects, do they have the same shape?
  4. */
  5. function looseEqual (a, b) {
  6. if (a === b) { return true }
  7. var isObjectA = isObject(a);
  8. var isObjectB = isObject(b);
  9. if (isObjectA && isObjectB) { //两个都是对象的话
  10. try {
  11. var isArrayA = Array.isArray(a);
  12. var isArrayB = Array.isArray(b);
  13. if (isArrayA && isArrayB) {//两个都是数组
  14. return a.length === b.length && a.every(function (e, i) {
  15. return looseEqual(e, b[i])
  16. })
  17. } else if (a instanceof Date && b instanceof Date) {//两个都是日期类型
  18. return a.getTime() === b.getTime()//调用日期对象上的方法得到数据并进行比较
  19. } else if (!isArrayA && !isArrayB) {//两个都不是数组 也不是日期
  20. var keysA = Object.keys(a);
  21. var keysB = Object.keys(b);
  22. //属性数量相等 && 判断A中键值对B中是否存在
  23. return keysA.length === keysB.length && keysA.every(function (key) {
  24. return looseEqual(a[key], b[key]) //递归宽松相等比较
  25. })
  26. } else {
  27. /* istanbul ignore next */
  28. return false
  29. }
  30. } catch (e) {
  31. /* istanbul ignore next */
  32. return false
  33. }
  34. } else if (!isObjectA && !isObjectB) { //两个都不是对象类型的话
  35. return String(a) === String(b) //就转换为字符串进行比较
  36. } else {
  37. return false
  38. }
  39. }

3.33 looseIndexOf

宽松相等的 indexOf,原生的是严格相等

  1. /**
  2. * Return the first index at which a loosely equal value can be
  3. * found in the array (if value is a plain object, the array must
  4. * contain an object of the same shape), or -1 if it is not present.
  5. */
  6. function looseIndexOf (arr, val) {
  7. for (var i = 0; i < arr.length; i++) {
  8. if (looseEqual(arr[i], val)) { return i } //满足宽松相等就给了
  9. }
  10. return -1
  11. }

3.34 once

借助变量储存标记called,从而确保函数只执行一次

  1. /**
  2. * Ensure a function is called only once.
  3. */
  4. function once (fn) {
  5. var called = false;
  6. return function () {
  7. if (!called) {
  8. called = true;
  9. fn.apply(this, arguments);
  10. }
  11. }
  12. }

3.35 生命周期等

生命周期的一些变量就不粘代码了,模块剩下的部分也复杂起来了,暂且放下🤐

4. 学习资源