笔记系列--Vue3源代码工具函数

419 阅读10分钟

前言

每个人都能做前端工程师,但达到山顶,却不是所有人。有些人一直在山底,有些人在山腰就没再往上爬,比如我,我一直在山腰那儿,做前端开发快有4年了,4年的应该是很强的,可惜我不是,还不懂Promise是怎么实现,还不会去读源代码。一看就没然后了,因为一般源代码是比较庞大且很多模块化,就要在脑子里画好连线。而且读的时间比较长,像我这种,有时会比较忙,过了几天再看就这么忘了,弃~就这么让焦虑伴随时间过去,然后偶然看到若川大佬发动的源码共读活动,有易到难的学习顺序,而且有大佬自己写的文章,还可以参考,觉得很不错的,就这么尝试的~再次感谢若川大佬组织的活动~~

这篇文章本来应该是2021年末完成的,因为有其他的重要的事要做,就这么拖到2022年,也算是希望2022年的开始,并且突破向上爬~加油咯~~

参考网址:

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

1.变量的解释:

  • __DEV__ 是指开发环境

2.工具函数的实现:

2.1.EMPTY_OBJ 空对象


export const EMPTY_OBJ = __DEV__ ? Object.freeze({}) : {}

这是在说,在开发环境下时,赋值给空对象时,为了防止开发人员增加属性,就用Object.freeze来冻结,注意这只能冻结最外层的属性(即使增加属性也不会报错),不影响内层的属性,比如:


const obj1 = Object.freeze({})
obj1.name = 'fency'
console.log(obj1) //{}, 没变化的,只是不会报错的

const obj2 = Object.freeze({info:{name:'fency', age:20, sex:'femal'}})
obj2.age = 18
console.log(obj2) //{info:{name:'fency', age:18, sex:'femal'}} 内层的age被变化

知识点:

看起来,Object.freeze()和const有点像,其实是不一样的,const不能再重新赋值,但里面的属性还能改。而Object.freeze能不能重新赋值,取决于声明的方式,它只是主要在于改变属性的操作符。

2.2.EMPRY_ARR 空数组


export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []

和上面的 EMPTY_OBJ差不多,但它有个神奇的地方,比如说:

const arr1 = Object.freeze([])
arr1.push(2) //会报错,因为被冻结了,属性就不能用
arr1[0] = 1 //不会报错的,只是无效
arr[0] //undefined

2.3.NOOP 空函数


export const NOOP = () => {}

2.4.NO 一直返回false


export const NO = () => false

2.5.isOn 判断字符串是不是以on开头,并且on后面的首字母不能是小写字母


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

知识点:
  • ^符号在开头,则表示必须以什么开头,比如o;
  • ^在其中,则是指,比如说[^a-z],这是表示不能小写字母

举🌰:


isOn('onChange') //true
isOn('onchange') //false
isOn('on') //false
isOn('on3change') //true

2.6.isModelListener 监听器


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

知识点:

startsWith是es6新增的方法,用来判断当前字符串的开头和给定的另一个字符串是不是一样,返回Boolean

举🌰:


// 语法

str.startsWith(srarchString, position)

//searchString: 给定的字符串,注意区分大小

//position: 可选,开始位置,默认0


const str = "Hello Fency"

str.startsWith('Hel') //true

str.startsWith('hel') //false

str.startsWith('Hel',2) //false, 从下标2开始,也就是l开始,llo和Hel不一样,所以返回false

2.7.extend 继承


export const extend = Object.assign

Object.assign是es6对象的新增方法,准确来说是合并,用于对象的合并

  • 第一个参数是目标对象,如果该参数不是对象类型,会自动转换为Object(只支持StringNumberBoolean),undefinednull会报错
  • 第二个以及后面的参数都是源对象

Tips:目标对象和源对象如果有相同的属性,源对象会覆盖目标对象,这个是浅拷贝

举🌰:


const obj1 = {name:'fency', info:{sex:'femal'}}

const obj2 = {name:'new fency', age:20}

const obj3 = extend(obj1, obj2)

console.log(obj3) //{name:'new fency',info:{sex:'femal'}, age: 20}

console.log(obj1===obj3) //true

常见用途:

  • 为对象添加属性
  • 为对象添加方法
  • 克隆对象
  • 合并多个对象
  • 为属性指定默认值

2.8.remove 移除某一项


export const remove = (arr, el)=>{
    const i = arr.indexOf(el)
    if(i > -1){
        arr.splice(i,1)
    }
}

2.9.hasOwn 判断是不是自身的属性


const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (val, key) => hasOwnProperty.call(val, key)

知识点:

hasOwnProperty,是一个Object的原型方法,它只有在自己本身的属性匹配到了才能返回 true,其他的或者它的原型属性返回false

举🌰:


hasOwn({name:'fency'}, 'name'); //true
hasOwn({}, 'name'}; //false
hasOwn({}.__proto__, 'toString'); //true,因为每个对象的原型都有一个toString()方法,才会返回true

2.10.isArray 判断是否是数组


export const isArray = Array.isArray

//举例子
isArray([]) //true
isArray({}) //false
isArray(12) //false

知识点:

typeof判断不出是否是数组,不管是Object或Array都会返回object;然后Object.prototype.toString.call也可以判断出的,但isArray是最简洁的。

2.11.objectToString 对象转换为字符串


export const objectToString = Object.prototype.toString

2.12.toTypeString


export const toTypeString = val => objectToString.call(val)

这往往用来判断类型,相比type更强大,有多强大呢?咱们看看下面的~

2.13.isMap 判断是否是Map


export const isMap = (val) => toTypeString(val) === '[object Map]'

还能判断是否是Map, Map是es6新增的属性

知识点:

es6为什么要新增Map?因为Object的键并不是都可以用的,比如说获取DOM节点作为一个键,但在Object的键中,会变成[object HTMLDivElement],这样就没法用。而在Map,能够正常获取DOM节点。Map的键值不限于字符串的。可以说Map是Object上更完善的一种数据结构。

2.14.isSet 判断是否是Set


export const isSet = (val) => toTypeString(val) === '[object Set']

Set是es6新增的属性,常用于去重,使每一个值都是唯一的。

举🌰:


const arr = [1,2,2,3,'a','a','b']

[...new Set(arr)] //[ 1, 2, 3, 'a', 'b' ]

2.15.isDate 判断是否是Date


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

知识点:

instanceof用来判断一个实例对象的原型链是否在构造函数的原型属性上。一般都是用来判断实例对象和构造函数的原型关系问题。

举🌰:


const date = new Date()

isDate(date) // true


function Person(){}
function Animal(){}

const p = new Person()
p instanceof Person //true
p instanceof Animal //false,因为p在Animal构造函数上并没有实例化

2.16.isFunction 判断是否是function


export const isFunction = (val) => typeof val === 'function'

知识点:

判断是否是function的方法有多种,Object.prototype.toString.call也是可以用的。但typeof是最好最简单最稳定的方法。

2.17.isString 判断是否是字符串


export const isString = (val) => typeof val === 'string'

知识点:

typeof是最简单的判断方法,但它比较适用于判断基本类型,比如String, Number, Boolean, Symbol, Undefined,还有特殊的function。如果要判断是否是Array,或者Object,就用其他的方法

2.18.isSymbol 判断是否是symbol


export const isSymbol = (val) => typeof val === 'symbol'

和上面一样的道理

2.19.isObject 判断是否是对象


export const isObject = (val) => val !== null && typeof val === 'object'

因为typeof null也会返回object, 所以要加上 val !== null😅

举🌰:


isObject({}) //true
isObject([]) //true
isObject(new Map()) //true

2.20.isPromise 判断是否是Promise


export const isPromise = (val) => {
    return isObject(val) && isFunction(val.then) && isFunction(val.catch)

}

Promise是es6新增的属性,解决之前最可怕的回调函数,现在变成最常见的异步执行解决方法(async/await也是如此),然后Promise必须是要有then和catch

举🌰:


const p=new Promise((resolve,reject)=>{
    resolve('执行成功')
})

isPromise(p) //true

2.21.toRawType 对象转字符串,然后截取


export const toRawType = (value) => {
    return toTypeString(value).slice(8,-1)
}

截取到后就是类型,比typeof判断还要准确的。

举🌰:


toRawType('a') //'String'
toRawType({a:1}) //'Object'
toRawType({null}) //'Null'
toRawType(function(){}) //'Function'
toRawType(undefined) //'Undefined'
toRawType(true) //'Boolean'

2.22.isPlainObject 判断是不是纯粹的对象


export const isPlainObject = (val) => toTypeString(val) === '[object Object]'

isPlainObject和上面的isObject类似的,但还是有几点不同的,看下面的🌰:


isObject([]) //true
isPlainObject([]) //false
isObject(new Map()) //true
isPlainObject(new Map()) //false
fucntion Person(){
    this.name = "fency"
}

const p = new Person() //实例化后就变成object
isPlainObject(p) //true

2.23.isIntegerKey 判断key是不是数字类型


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

判断key是不是数字类型,要满足以下所有的条件:

  • isString(key) 必须是字符串类型;
  • key !== 'NaN'
  • key[0] !== '-'
  • ' ' + parseInt(key, 10) === key
  • ' ' +为了保证和key是一样的类型,===包括比较类型
  • parseInt(key, 10) 第二个参数是进制,这要求十进制,也就是说必须是整数

举🌰:


isIntegerKey('a') //false, 因为parseInt(a,10)会返回NaN
isIntegerKey('011') //false, 因为parseInt('011',10)会返回11,和'011'不一样
isIntegerKey(11) //false, 因为isString(11)会返回false
isIntegerKey('11') //true

2.24.makeMap && isReservedProp


//做一个map 键值对

export function makeMap(str, expectsLowerCase){
    const map = Object.create(null)
    //字符串根据逗号来分隔成为一个数组
    const list = str.split(',')
    //存到map,并且赋给true,表示存在的。
    for(let i = 0; i < list.length; i++){
        map[list[i]] = true
    }
    //返回一个函数的,需要检查时就根据key来判断是否在里面的
    return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
}

export const isReservedProp = makeMap(
    ',key,ref,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted'
)

isReservedProp先保留需要的属性,然后返回一个函数的,然后需要检查某个key是否在里面的。


isReservedProp('key') //true
isReservedProp('ref') //true

2.25.cacheStringFunction 缓存


const cacheStringFunction = fn => {
    //创建对象,作为数据字典
    const cache = Object.create(null)
    //利用闭包的思想,以至于别的函数能访问到这个变量
    return str => {
        const hit = cache[str]
        return hit || (cache[str] = fn(str))
    }
}

2.26. camelize 连字符转驼峰

// /- 匹配连字符-;\w [0-9a-zA-Z_];/g 全局的意思
// 圆括号()是为了提取字符串,比如说`on-click`,那么提取的就是c
const camelizeRE = /-(\w)/g
export const camelize = cacheStringFunction(str => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})

第一次见正则表达式还有函数作为第二个参数的,然后我就查询有关它的使用方法~

知识点:

  • replace是替换匹配的字符串
  • 第一个参数可以是字符串,或者RegExp
  • 第二个参数可以是新的字符串,或者函数(返回的值),用来替换掉第一个参数。
  • 一个函数作为第二个参数时,前提是要匹配成功后才能执行函数的。
  • 它的实现方法是这样:比如说on-click根据RegExp获取到c
  • 这个是利用高阶函数,如果接触这个太少了,可能会一时没搞懂,比如说我。建议认真研究的~
  • cacheStringFunction 利用str => {return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))}作为函数。先替换字符串后,再存到缓存列表的。如果之前存过的,则直接返回的。这么高级的写法,一定要get下~

3.27.hyphenate 驼峰转连字符


// \B是\b的相反,\b是单词的边界,\B是非单词的边界
// 然后后面[A-Z],是说根据大写字母来划分边界的。
const hyphenateRE = /\B([A-Z])/g

// 其实就是把 onClick 处理成为 on-click
export const hyphenate = cacheStringFunction(str => {
    return str.replace(hyphenateRE, '-$1').toLowerCase()
})

3.28.capitalize 首字母转化为大写


export const capitalize = cacheStringFunction(str => str.charAt(0).toUpperCase() + str.slice(1))

charAt就是根据位置返回指定的字符串,这里面用0,也就是取首字母,然后转化为大写字母,slice(1),从位置1开始到最后的字符串,然后组成一个新的字符串并返回去。

3.29.toHandlerKey click -> onClick


export const toHandlerKey = cacheStringFunction(str => str ? `on${capitalize(str)}` : ``)

就字符串前面添加on

3.30.hasChanged 判断是否有变化


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

知识点:

Object.is 是es6新增的属性,和之前的比较两个值是否相等: =====。一般来说推荐用===,因为具有严格的特性,但有个奇怪的问题,比如NaN===NaN居然返回false,所以出现Object.is()


NaN === NaN //false

Object.is(NaN, NaN) //true



+0 === -0 //true

Object.is(+0, -0) //false

3.31.invokeArrayFns 执行数组里的函数


export const invokeArrayFns = (fns, arg) => {
    for(let i=0; i<fns.length; i++){
        fns[i](arg)
    }
}

就是如果需要执行多个函数的话,可以用这个来执行。高明哇~之前做的业务任务遇到过的,但我却一个一个执行的,多笨哪~😂

3.32.def


export const def = (obj,key, value) => {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        value
    })
}

Object.defineProperty是vue2.x版本主要实现方法之一,用于添加或者修改对象的属性,configurable是可修改,enumeable不可枚举,value就是值。也就是表示对一个对象赋值后,这个对象的属性可以改变,但不可枚举的。

3.33. toNumber 转数字


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

用parseFloat为了保持小数点正常输出,然后判断是否是数值。一般来说isNaN返回true的就是数值类型的。

3.34.getGlobalThis 获取全局对象


let _globalThis;

export const getGlobalThis = () => {
    return (
        _globalThis ||
        (_globalThis =
        typeof globalThis !== 'undefined'
        ? globalThis
        : typeof self !== 'undefined'
        ? self
        : typeof window !== 'undefined'
        ? window
        : typeof global !== 'undefined'
        ? global
        : {})
    )
}

顺序:_globalThis(这个有值的话,会直接返回) > self > window > global > {}

总结:

  • 了解了工具函数的写法;
  • 认识了不常用的API,希望后面能多运用下;
  • 了解了正则表达式还有另外函数作为参数的;
  • 了解了高阶函数的使用方法。