第二期笔记: vue3 工具函数

434 阅读9分钟

第二期笔记: vue3 工具函数

Σ(っ°Д°;)っ 源码阅读

这是若川大佬源码共读活动的第二期 附上链接

打开 vue-next/packages/shared/src/index.ts

1. EMPTY_OBJ 空对象:

源码:

export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
  ? Object.freeze({})
  : {}

记录:

DEV

这是一个伪全局变量 ,用于管理开发环境中需运行的代码块,这在编译阶段会被内联,在 CommonJS 构建中,转化成 process.env.NODE_ENV !== 'production' 这样的判断,用于判断当前环境是开发环境还是生产环境。

Object.freeze():

MDN上描述,这个方法可以冻结一个对象。一个被冻结的对象再也不能被修改;被冻结对象自身的所有属性都不可能以任何方式被修改。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

测试:

const teacher = {
    name: 'tom',
    students: ['amy'],
    age: 40,
    feature: {
        face: 'big'
    }
}
const returnObj = Object.freeze(teacher);
console.log(returnObj === teacher); //true

teacher.age=30;
console.log(teacher.age) //40 没有改变

teacher.students=[];
console.log(teacher.students)//[ 'amy' ] 没有改变

teacher.students.push('jack')
console.log(teacher.students) //[ 'amy', 'jack' ] 成功添加了

teacher.feature.nose='small'
console.log(teacher.feature) //{ face: 'big', nose: 'small' } 成功添加了

结论:冻结的是基础类型以及引用类型的引用,不能修改基础类型的值和引用类型的引用地址,但是可以修改引用地址对象的值。

若要使对象不可变,需要递归冻结每个类型为对象的属性(深冻结),类似深拷贝,同时也要注意循环引用,下面MDN的例子没有做循环引用的处理:

function deepFreeze(obj) {

    // 取回定义在obj上的属性名
    const propNames = Object.getOwnPropertyNames(obj);
    // 在冻结自身之前冻结属性
    propNames.forEach((name)=> {
        const prop = obj[name];
        // 如果prop是个对象,冻结它
        if (typeof prop == 'object' && prop !== null)
            deepFreeze(prop);
    });
    // 冻结自身(no-op if already frozen)
    return Object.freeze(obj);
}
deepFreeze(teacher);
teacher.feature.mouth='small';
console.log(teacher.feature.mouth);//undefined

对比:

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性变的不可删除,以及一个数据属性不能被重新定义成为访问器属性,只要原来属性是可写的,值就能修改。

使用Object.freeze() 冻结的对象中现有属性是不可变的。

2. EMPTY_ARR 空数组

源码:

export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []
测试:
const arr=Object.freeze([]);
arr.push('a') //报错 TypeError: Cannot add property 0, object is not extensible

3.EMPTY_ARR 空数组

源码

export const NOOP = () => {}

这种写法广泛使用,箭头函数比用function(){}更简洁,代码压缩更方便

4.NO 永远返回 false 的函数

源码:

export const NO = () => false

就是一个永远返回false的匿名函数,方便压缩代码。

5. isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母

源码:

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

正则表达式:/on[a-z]/,匹配on开头的字符串,并且on后首字母不是小写字母。

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

console.log(isOn('onChange'));; // true
console.log(isOn('onchange'));; // false
console.log(isOn('on3change'));; // true

6.isModelListener 监听器

源码:

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

判断字符串是不是以onUpdate:开头,返回布尔类型

测试:
const isModelListener = (key) => key.startsWith('onUpdate:');
console.log(isModelListener('onUpdate:change'));; // true
console.log(isModelListener('1onUpdate:change'));; // false

startswith是es6新增的字符串方法,类似的有:

  • includes() :返回布尔值,表示是否找到了参数字符串。
  • endsWith() :返回布尔值,表示参数字符串是否在原字符串的尾部。
测试:
console.log('i am jack'.endsWith('jak'));// false
console.log('i am jack'.endsWith('jack'));//true
console.log('i am jack'.includes('am'));//true
console.log('i am jack'.includes('am2'));//false
console.log('i am jack'.indexOf('am'));//2

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束,即endsWith的n参数,表示保留前n个字符,按保留的字符算,进行末尾配对。而其他两个是用第n个后的字符串进行配对。

7.extend  合并

源码:

export const extend = Object.assign

Object.assign(target, ...sources)

参数:

target:目标对象。

sources:源对象。

返回值:

返回目标对象。

MDN描述:

如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。

简单来说,就是合并对象属性,后面对象的属性会覆盖前面对象的属性。

注意点:

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()Object.defineProperty()

String类型和 Symbol类型的属性都会被拷贝。

在出现错误的情况下,例如,如果属性不可写,会引发TypeError,如果在引发错误之前添加了任何属性,则可以更改target对象。

Object.assign 不会在那些source对象值为 nullundefined的时候抛出错误。

深拷贝问题

针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是(可枚举)属性值。

假如源值是一个对象的引用,它仅仅会复制其引用值。

测试
const obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

const copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }

Object.create() 是创建一个对象,第一个参数是新对象的原型对象,第二个参数是新对象自身的属性。

8.remove 移除数组的一项

源码:

export const remove = <T>(arr: T[], el: T) => {
  const i = arr.indexOf(el)
  if (i > -1) {
    arr.splice(i, 1)
  }
}

通过数组方法indexof找到目标的索引,然后通过splice进行删除数组的那一项。

splice,是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置

对于移除数组项,axios的拦截器的处理是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行。

axios处理示例:
this.handlers = [];

// 移除
if (this.handlers[id]) {
    this.handlers[id] = null;
}

// 执行
if (h !== null) {
    fn(h);
}

类似delete

测试:
let arr=[1,3,2]
delete arr[arr.indexOf(3)]
console.log(arr[1]) //undefined
console.log(arr.length)//3
console.log(arr)//[1,undefined,2]

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

源码:

const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
  val: object,
  key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)

hasOwnProperty方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键),而不会查找其原型链,通常 for...in 方法会继承的属性及自定义原型属性也遍历,而通过hasOwnProperty方法可以过滤这些属性

示例:

var triangle = {a: 1, b: 2, c: 3};

function ColoredTriangle() {
  this.color = 'red';
}

ColoredTriangle.prototype = triangle;

var obj = new ColoredTriangle();

for (var prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(`obj.${prop} = ${obj[prop]}`);// "obj.color = red"
  }
}

由于源码中的hasOwnProperty方法中,调用hasOwnProperty的是Object构造函数,而不是示例对象,所以需要call函数,将this绑定到需要判断属性的对象中。

10.isArray 判断数组

源码:

export const isArray = Array.isArray

当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes.

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

instanceof是通过原型链判断的,如果一个对象的原型指向了Array.prototype,同样会返回true,而isArray不会

const isArray = Array.isArray;

isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true

11.isMap ,isSet

源码:

export const isMap = (val: unknown): val is Map<any, any> =>
  toTypeString(val) === '[object Map]'
  
  export const isSet = (val: unknown): val is Set<any> =>
  toTypeString(val) === '[object Set]'
  
  export const objectToString = Object.prototype.toString 
  export const toTypeString = (value: unknown): string =>
  objectToString.call(value)

就是通过Object构造函数的toString方法判断类型,如果是map对象,toString方法会打印出'[object Map]',如果是Array对象,

则是'[object Array]' ,如果是set对象,则是'[object Set]',如果是date 属性,则是[object Date]。

测试:
let arr=[];
let map=new Map();
let date=new Date();
let date1={__proto__: new Date()}
console.log(Object.prototype.toString.call(arr));//[object Array]
console.log(Object.prototype.toString.call(map));//[object Map]
console.log(Object.prototype.toString.call(date));//[object Date]
console.log(Object.prototype.toString.call(date1));//[object Object]

可以通过 toString() 来获取每个对象的类型,会输出[object type]。为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg

12.toRawType 对象转字符串 截取后几位

源码:

export const toRawType = (value: unknown): string => {
  // extract "RawType" from strings like "[object RawType]"
  return toTypeString(value).slice(8, -1)
}

截取字符串中的[8,-1],-1指的是最后一位,区间左闭右开,其实是把[object type] 中的type截取出来返回。

13. isIntegerKey 判断是不是数字型的字符串key值

export const isIntegerKey = (key: unknown) =>
  isString(key) &&
  key !== 'NaN' &&
  key[0] !== '-' &&
  '' + parseInt(key, 10) === key

首先判断是否是字符串,对象的键都是字符串形式存在的,先判断是否是字符串,然后判断是否是NaN,因为''+parseInt('NAN',10)='NaN'为true,所以要先过滤,然后判断key[0]!=='-'是因为''+parseInt('0',10)==='0'为true,所以也要过滤

parseInt 第二个参数是进制。

14.cacheStringFunction 缓存

源码:

const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
  const cache: Record<string, string> = Object.create(null)
  return ((str: string) => {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }) as any
}

测试:

const cacheStringFunction = (fn) => {
    const cache = Object.create(null);
    return ((str) => {
        const hit = cache[str];
        console.log(cache)
        return hit || (cache[str] = fn(str));
    });
};

function test(str) {
    return str
};
let cache=cacheStringFunction(test);
cache('test1');//[Object: null prototype] {}
cache('test2');//[Object: null prototype] { test1: 'test1' }

将函数传入,返回一个函数,返回函数的参数作为第一个传入函数的参数传入,并将第一个传入函数的返回结果作为值,参数作为键存进对象中缓存。

15.hasChanged 判断是不是有变化

export const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue)

Object.is是es6新增的方法,

Object.is() 方法判断两个值是否为同一个值。如果满足以下条件则两个值相等:

  • 都是 undefined
  • 都是 null
  • 都是相同长度的字符串且相同字符按相同顺序排列
  • 都是相同对象(意味着每个对象有同一个引用)
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是NaN
    • 或都是非零而且非 NaN且为同一个值

注意的点: NaN===NaN   =>false
所以,如果要逻辑实现,就需要用 x !== x && y !== y 来判断

16.toNumber 转数字

export const toNumber = (val: any): any => {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

注意的点:isNaN只能判断是不是数,如果参数是字符串,一样会返回true,而Number.isNaN则会判断参数是不是NaN,字符串不是NaN,所以传入字符串是返回false。

总结:

大部分都是学过的基础api,不过也不是经常用,所以老是忘记,趁着这个写笔记机会记录一下。也有部分没见过的,

比如Object.freeze(),冻结对象。还有就是删除数组项的方法,用null代替或者undefined是个不错的选择。判断对象类型用:

Object.prototype.toString.call,可以很方便判断对象的类型,兼容性也还好。NaN的判断也是学到了,因为接触的不多,也不知道NaN===NaN是返回false的。

好记性不如烂笔头,把基础复习了一遍,不错不错~

Σ(っ°Д°;)っ