【若川视野 x 源码共读】第24期 | vue2 shared

123 阅读7分钟

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

1、空对象 Object.freeze({})

const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
    ? Object.freeze({})
    : {};
  • 1)Object.freeze 是什么?
  • Object.freeze 是 冻结对象,冻结的对象 最外层 无法修改。
  • 2)为什么 在 非线上环境 中 EMPTY_OBJ 要用 Object.freeze?而在 线上环境 只需 {} 即可?
  • 猜测:线上环境不会被修改,但是开发环境有可能会被修改
  • 3)在哪些实际的业务场景中可以用到空对象?
  • todo

2、空数组 Object.freeze([])

const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];
  • 1)在哪些实际的业务场景中可以用到空数组?
  • todo

3、空函数

const NOOP = () => {};
 * 使用场景?todo
 * 作用? 1) 方便判断, 2)方便压缩
  • 使用场景?todo
  • 作用? 1) 方便判断, 2)方便压缩

4、永远返回 false 的函数

const NO = () => false;

作用? 1)方便压缩

5、isOn 正则判断(判断字符串是不是 on 开头 且 on 后首字母不是小写字母)

知识点:regex 反向字符集 [^] 和 reg.text(str)

const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);

6、是否是监听器 str.startsWith('')(判断字符串是不是以onUpdate:开头)

知识点:str.startsWith('xx')

const isModelListener = (key) => key.startsWith('onUpdate:');

7、合并函数 Object.assign

  • 为什么不写成 const extend = (xx) => {Object.assign(xx)}?
  • 因为 Object.assign 本身就是个函数
const extend = Object.assign;

8、remove 移除数组的一项 arr.splice(i, 1);

  • 知识点:array 的 splice 方法
  • 引申:
  • 1)array的其他方法?
  • todo
  • 2)删除数组中的一项 的其他实现?及对比?
  • todo
const remove = (arr, el) => {
    const i = arr.indexOf(el);
    if (i > -1) {
        arr.splice(i, 1);
    }
};

9、 hasOwn 是不是自己本身所拥有的属性

  • 知识点:
  • 1)Object.prototype.hasOwnProperty
  • 2)call函数
  • fn.call(target, params) 将 this 显式指定给第一个参数 target,并执行函数fn(params)。
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);

10、isArray 判断是否是数组

const isArray = Array.isArray;

11、toString & typeof & instanceof 判断基本数据类型

typeof
  • typeof 操作符返回一个字符串,表示未经计算的操作数的类型。
  • 优点:能够检查undefined、number、boolean、string、function、symbol、bigint 7种类型
  • 缺点:无法区分object、array、null、date,都是返回 object
instanceof
  • instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
  • 优点:能够检查 Object、Array、Function、Date 类型
  • 缺点:检查不了number、boolean、string类型,结果都是返回false
object.prototype.toString.call
  • 优点:任何类型都能准确的检查出来
  • 缺点:写法复杂
const isMap = (val) => toTypeString(val) === '[object Map]';
const isSet = (val) => toTypeString(val) === '[object Set]';

const isDate = (val) => val instanceof Date;

const isFunction = (val) => typeof val === 'function';
const isString = (val) => typeof val === 'string';
const isSymbol = (val) => typeof val === 'symbol';

/** 
 * 非空对象?nonono,是因为 typeof null === 'object' 所以要把 null 剔掉
 */
const isObject = (val) => val !== null && typeof val === 'object';
const isPromise = (val) => {
    return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};

/** 
 * 为啥要 call 一下?这样写不行么?
 * toTypeString = (value) => Object.prototype.toString(value)
 * 不行,因为 toString方法是Object的原型链方法,这么写的结果就是 啥啥都是 object了
 */

const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);

const toRawType = (value) => {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};
/**
 * isObject 和 isPlainObject 有啥区别?
 * 
 * isObject 使用的是 typeof 但是无法区分 object、array、null、date等
 * isPlainObject 只识别 纯对象
 * 
 */
const isPlainObject = (val) => toTypeString(val) === '[object Object]';

12、isIntegerKey 判断是否是数字型的字符串key值

  • 字符串类型、不为NaN、不是负数、且为十进制
  • 最后一个判断 用了 '' + parseInt(key, 10) 是为了把数字转为 字符串
const isIntegerKey = (key) => isString(key) &&
    key !== 'NaN' &&
    key[0] !== '-' &&
    '' + parseInt(key, 10) === key;

13、makeMap & isReservedProp 保留属性

  • 如果让我实现我大概会写成
  • const isReservedProp = (val, expectsLowerCase) => {
  • const keys = ['key','ref','onVnodeBeforeMount', 'onVnodeMounted']
  • return expectsLowerCase ? keys.include(val.toLowerCase()) : keys.include(val)
  • }
  • 问题:可能会重复、每次都要遍历数组查询性能差、无可复用性
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted');

14、cacheStringFunction 缓存

const cacheStringFunction = (fn) => {
    // 创建 一个空的 cache 对象
    const cache = Object.create(null);
    // 返回 一个函数 这个函数接受 一个参数,并 返回一个 布尔值,表示 将传入的参数缓存起来,并告知 用户 缓存结果(成功 or 失败)
    return ((str) => {
        const hit = cache[str];
        // debugger
        return hit || (cache[str] = fn(str)); // 没存的话存一下 & 返回 true,存过了的话 直接返回 true
    });
};
// 上述函数的调用例子:
// const fn1 = x => x + 9 // 可以理解为 fn1(x) = x + 9 
// const cache1 = cacheStringFunction(fn1) // 声明一个 cache1 的 变换函数
// cache1(1) // 存储的cache 为 { 1: 10 },返回 true
// cache1(2) // 存储的cache 为 { 1: 10, 2: 11 },返回 true

// const fn2 = x => x * 2 // 可以理解为 fn2(x) = x * 2 
// const cache2 = cacheStringFunction(fn2) // 声明一个 cache2 的 变换函数
// cache2(1) // 存储的cache 为 { 1: 2 },返回 true
// cache2(2) // 存储的cache 为 { 1: 2, 2: 4 },返回 true

/** 
 * \w: 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。
 * -(pattern): 匹配pattern并获取这一匹配。且 pattern 前有 “-”
 * a-bbb-ccccc // 共找到 5 处匹配:-b -c
 */
const camelizeRE = /-(\w)/g; 
/** camelize('a-bbb-ccccc') => 'aBbbCcccc'
 * @private
 * str.replace(regexp|substr, newSubStr|function)
 * repalce 的第一个参数是 regexp, 第二个参数是 function,函数的返回值作为替换字符串(由于是全局匹配模式,这个方法每次匹配都会被调用)
 * function 的两个参数,第一个参数是匹配模式 -(\w) 的字符串,第二个参数是与模式中的子表达式(\w)匹配的字符串
 */
const camelize = cacheStringFunction((str) => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});

/** 
 * \B	匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
 * [A-Z] 字符范围。匹配指定范围内的任意字符。
 * ABbCccDdE // 共找到 4 处匹配:B C D E
 */
const hyphenateRE = /\B([A-Z])/g;
/** hyphenate('ABbCccDdE') => a-bb-ccc-dd-e
 * @private
 * str.replace(regexp|substr, newSubStr|function) 
 * repalce 的第一个参数是 regexp, 第二个参数是 newSubStr, $n 为变量名(n为匹配到的字符串的索引)。
 * 将所有匹配到的值添加前面加上短横线’-’:ABbCccDdE => A-Bb-Ccc-Dd-E
 * 再将整体 str toLowerCase: A-Bb-Ccc-Dd-E => a-bb-ccc-dd-e
 */
const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());

/** capitalize('aaaa') => Aaaa
 * @private
 * charAt() 方法从一个字符串中返回指定的字符
 * str.slice(beginIndex[, endIndex]) 提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串
 */
const capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));

/** toHandlerKey('aaaa') => onAaaa
 * @private
 */
const toHandlerKey = cacheStringFunction((str) => str ? `on${capitalize(str)}` : ``);

15、hasChanged 判断是不是有变化

/** 
 * Object.is(value1, value2); // 返回一个 Boolean 类型标示两个参数是否是同一个值。
 * 之前的写法:
 * const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
 * 不得不提的NaN:(value === value || oldValue === oldValue)
 * 为什么会有这句 因为要判断 NaN。js中认为 NaN 是不变的,所以 NaN === NaN 为 false
 * hasChanged(NaN, NaN); // false
 * 不得不说,新的api 理解成本下降很多。。。
 */
// compare whether a value has changed, accounting for NaN.
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

16、invokeArrayFns 执行数组里的函数

/** 
 * 这有啥用?怎会会有 要 传入一个函数列表,拿同样的参数执行一遍的?
 * 为什么这样写?我们一般都是一个函数执行就行。数组中存放函数,函数其实也算是数据。这种写法方便统一执行多个函数。
 */
const invokeArrayFns = (fns, arg) => {
    for (let i = 0; i < fns.length; i++) {
        fns[i](arg);
    }
};

17、def 定义对象属性

/** 
 * Object.defineProperty(obj, prop, descriptor) 
 * 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
 */
const def = (obj, key, value) => {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        value
    });
};

18、toNumber 转数字

/** 
 * parseFloat(string)
 * parseFloat() 函数解析一个参数(必要时先转换为字符串)并返回一个浮点数。
 * isNaN 本意 是判断是不是 NaN 值,但是不准确的。使用 Number.isNaN('a') 更准确。
 */
const toNumber = (val) => {
    const n = parseFloat(val);
    return isNaN(n) ? val : n;
};

19、getGlobalThis 全局对象

/** 
 * 会全局缓存一个 _globalThis,避免重复判断
 * 首次判断优先级:globalThis >> self >> window >> global >> {}
 * 为啥最后还有个空对象??如果都不存在,使用空对象。可能是微信小程序环境下。
 * 
 * 其他:
 * self:在 Web Worker 中不能访问到 window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。
 * global:Node环境下,使用global。
 * this:为什么不是 this? 在松散模式下,可以在函数中返回 this 来获取全局对象,但是在严格模式和模块环境下,this 会返回 undefined。
 * globalThis:
 *  - globalThis 提供了一个标准的方式来获取不同环境下的全局 this  对象(也就是全局对象自身)。
 *  - 不像 window 或者 self 这些属性,它确保可以在有无窗口的各种环境下正常工作。
 *  - 你可以安心的使用 globalThis,不必担心它的运行环境。
 *  - 为便于记忆,你只需要记住,全局作用域中的 this 就是 globalThis。
 */
let _globalThis;
const getGlobalThis = () => {
    return (_globalThis ||
        (_globalThis =
            typeof globalThis !== 'undefined'
                ? globalThis
                : typeof self !== 'undefined'
                    ? self
                    : typeof window !== 'undefined'
                        ? window
                        : typeof global !== 'undefined'
                            ? global
                            : {}));
};