本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 环境准备
今天要看的vue3的工具函数,打开vuejs/core,在README.md和CONTRIBUTE.md中可以看到项目中的各种信息。看到shared模块就是就是工具函数的所在之处
`shared`: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).
vue3的代码是ts写的,ts我不咋会,需要打包成js查看
要求:Node需要10+,Yarn需要1.x版本。推荐用nvm控制node版本
步骤:
git clone https://github.com/lxchuan12/vue-next-analysis.git
cd vue-next-analysis/vue-next
npm install --global yarn
yarn //下载依赖
yarn build //打包
可以得到vue-next/packages/shared/dist/shared.esm-bundler.js 在vue-next的package.json中添加脚本,生成sourcemap
"scripts":{
"dev:sourcemap":"node scripts/dev.js --sourcemap",
}
运行yarn dev:sourcemap 命令,控制台输出如下信息
/vue/vue-next-analysis/vue-next/packages/vue/src/index.ts →
packages/vue/dist/vue.global.js
vue.global.js.map就是输出的sourcemap文件,vue.global.js就是调试的js文件,引入vue.global.js,运行yarn serve文件,在浏览器中就可以调试vue3源码了。
2. 工具函数
- babel解析的默认插件, 一系列的babel插件,用在模版表达式的解析和SFC(单文件组件)的解析
const babelParserDefaultPlugins = [
'bigInt',//a ?? b
'optionalChaining', //a?.b
'nullishCoalescingOperator',
]
- 空对象
let EMPTY_OBJ =(process.env.NODE_ENV !=='production')?
Object.freeze({}):{}
- 空数组
let EMPTY_ARR = (process.env.NODE_ENV !=='production')? Object.freeze([]):[]
- 空函数
const NOOP = ()=>{};
- 永远返回false的函数
const NO = ()=>false;
- 检测字符串是否为on开头的函数,以on开头,并且剩余字符不为小写字母
const onRE = /^on[^a-z]/;
const isOn = (key)=>onRE.test(key)
console.log(isOn('pnclick'));//false
console.log(isOn('onClick'));//true
- 监听器,检测是key是否以onUpdate开头
const isModeListener = (key)=>key.startsWith('onUpdate')
isModeListener('onClick')//false;
isModeListener('onUpdate')//true;
- extend 合并对象
const extend = Object.assign;
var a = {a:1};
var b = {b:1};
var c = extend(a,b); //c:{a:1,b:1} //a:{a:1,b:1} //b:{b:1}
// Object.assign(),从一个或多个源对象将自有属性和可枚举属性,复制到目标对象中
- remove 从数组中移除某一项
const remove = (arr,el)=>{
const i = arr.indexOf(el)
if(i>-1){
arr.splice(i,1)
}
}
// splice很耗费性能,axios拦截器里的源码直接将
// 某一项设置为null,节省了性能
- hasOwn 是否为自身拥有的属性
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val,key)=>hasOwnProperty.call(val,key)
//hasOwn([],'toString') false;
// hasOwn({a:1},'a') true;
- 是否为数组
const isArray = Array.isArray;
// const fakerArr = {__proto__:Array.prototype,length:0}
// fakerArr instanceof Array true
// isArray(fakerArr) false;
// __proto__属性,可以设置某个对象的原型,骗过instanceoof,所以Array.isArray判断更为准确
- 获取toString方法,缩短了字符,方便随时调用,
const objectToString = Object.prototype.toString;
- 将对象转化成[object xxx]形式的字符串,来判断对象的类型
const toTypeString = (value)=>objectToString.call(value)
- isMap判断是否为Map对象
const isMap = (val) =>toTypeString(val) === '[object Map]'
- isSet判断是否为set对象
const isSet = (val)=>toTypeString(val) ==='[object Set]'
- 判断是否为Date对象
const isDate = (val)=>val instanceof Date;
// instanceof判断某个构造函数的原型是否在实例的原型上
// isDate(new Date()) true
// isDate({__proto__:new Date()}) true,实际上,它的原型是Object
// ({__proto__:[]}) instanceof Array true,实际上它的原型是Object
// __proto__: 可以访问对象的原型,也可以设置对象的原型,尽量不要去使用它,它已经从web标准中移除了,可以使用Object.getPrototypeOf()获取对象的原型
- 判断是否为函数
const isFunction = (val)=>typeof val ==='function'
- 判断是否为字符串
const isString = (val)=>typeof val === 'string'
- 判断是否为symbol
const isSymbol = (val)=> typeof val == 'symbol';
// symbol是基本类型,Symbol是函数,不是构造函数,不能使用new关键字
- 判断是否为object
const isObject = (val)=>val!==null &&typeof val ==='object'
// typeof null 为object,所以需要加上typeof val ==='object'的判断
- 判断是否为Promise
const isPromise = (val)=>{
return isObject(val)&&isFunction(val.then)&&isFunction(val.catch)
}
- 获取对象类型的字符串表示
const toRawType = (val)=>{
return toTypeString(val).slice(8,-1)
}
//例如:toRawType('abc')会返回 String 这个字符串
- 是否为纯对象
const isPlainObject = (val)=>toTypeString(val) === '[object Object]'
let Ctor = function (){this.name ='构造函数'}
isPlainObject(new Ctor()) //true;
- key是否为数字型的字符串
const isIntegerKey = (key)=>isString(key)&&
key!=='NAN'&&
key[0]!=='-'&&
''+parseInt(key,10) === key;
// isIntegerKey(011) false;
//''+parseInt(key,10) === key;,这个是为了确保key为10进制的字符串
// key[0]也可以用key.charAt(0)代替,key[0]没有值,则返回undefined,charAt没有值,则返回空字符串。
- 创建一个键值对映射并返回一个函数检查key,是否在在这个映射中
- 重要提示,makeMap的调用都要加上/*#__PURE__*/的前缀,方便rollup可以做tree-shake的操作
- expectsLowerCase为true时,val会转化成小写
function makeMap(str,expectsLowerCase){ const map = Object.create(null); const list = str.split(','); for (let i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase? val=>!!map[val.toLowerCase()] : val=>!!map[val] } - 是否为保留属性,空字符串也是包括在内的
const isReservedProp = /*#__PRUE__*/ makeMap(
',key,ref,'+
'onVnodeBeforeMount,onVnodeMounted,'+
'onVnodeBefoeUpdate,onVnodeUpdated,'+
'onVnodeBeforeUnmount,onVnodeUnmounted'
)
// isReservedProp('ref') true
// isReservedProp('') true
- cacheStringFunction缓存
const cacheStringFunction = (fn)=>{
const cache = Object.create(null)
return ((str)=>{
const hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
// js的单例模式也是类似的
var getSingle = function(fn){
var result;
return function(){
return result || (result = fn.apply(this,arguments))
}
}
- 连字符转转驼峰
const camlizeRE = /-(\w)/g;
const camlize = cacheStringFunction((str)=>{
return str.replace(camlizeRE,(_,c)=>(c? c.toUpperCase():''))
})
- 驼峰转连字符
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = cacheStringFunction((str)=>str.replace(hyphenateRE,'-$1').toLowerCase())
- 首字母转大写
const capitalize = cacheStringFunction((str)=>str.charAt(0).toUpperCase() + str.slice(1))
- 事件key转化,添加on前缀 click => onclick
const toHandlerKey = cacheStringFunction((str)=>(str? `on${capitalize(str)}`:''))
// toHandlerKey('click') //onClick
- // 判断值是否改变,包括NaN、+0、-0
const hasChanged = (value,oldValue) =>!Object.is(value,oldValue);
// Object.is判断两个值是否为同一个值,+0和-0相比为false,NaN和自身相同,
// 但是 ===、将+0和-0相比,会被视为相等、将NaN自身相比,视为不相等。
//ES5部署
Object.defineProperty(Object,'is',{
value:function(x,y){
if(x===y){
// 当x,y不为0时,返回true;
// 当x,y同时为0时,并且前缀一致时,返回true
return x!==0 || 1/x===1/y;
}else{
// 判断NaN
return x!==x && y!==y;
}
}
})
- 批量执行数组里的函数
const invokeArrayFns = (fns,arg)=>{
for (let i = 0; i < fns.length; i++) {
fns[i](arg)
}
}
- 定义对象,用Object.definedProperty
const def = (obj,key,value)=>{
Object.defineProperty(obj,key,{
configurable:true,
enumerable:false;
value
})
}
- 转数字
const toNumber = (val)=>{
const n = parseFloat(val);
return isNaN(n)? val:n;
}
// isNaN会将非number类型的参数,转化为number,然后比较
// 所以当传递空字符串或值为真的布尔值的时候,isNaN会返回false
// 用Number.isNaN时,传递NaN才会返回true
- 获取全局对象
let _globalThis;
const getGlobalThis = ()=>{
return (_globalThis||
(_globalThis=typeof _globalThis!=='undefined'?
_globalThis:typeof self !=='undefined'?
self:typeof window!=='undefined'?
window:typeof global !=='undefined'?
global:{}))
}
-
globalThis可以获取全局的this对象,不用担心平台的问题
-
再次调用getGlobalThis能够直接获取_globalThis,,这种写法值得学习
3. 总结
- 在README.md和CONTRIBUTE.md里可以看到项目的详细信息
- 设置dev:sourcemap命令可以生成sourcemap文件,方便调试源码
- 通过globalThis可以跨平台获取全局的this对象
- 一些常用的方法可以抽取出来作为公用 比如说
Object.prototype.hasOwnProperty;抽取出来,能提高性能,节省代码 - Number.isNaN比isNaN方法更能准确的判断是不是NaN
__proto__是被标准移除的属性,尽量不要去使用它- 正则对字符串的判断,是十分有用,要学正则了