【若川视野 x 源码共读】第2期 | vue3 工具函数

960 阅读6分钟

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

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源码了。

image.png

2. 工具函数

  1. babel解析的默认插件, 一系列的babel插件,用在模版表达式的解析和SFC(单文件组件)的解析
const babelParserDefaultPlugins = [
    'bigInt',//a ?? b
    'optionalChaining', //a?.b
    'nullishCoalescingOperator',
]
  1. 空对象
let EMPTY_OBJ =(process.env.NODE_ENV !=='production')? 
Object.freeze({}):{}
  1. 空数组
let EMPTY_ARR = (process.env.NODE_ENV !=='production')? Object.freeze([]):[]

  1. 空函数
const NOOP = ()=>{};
  1. 永远返回false的函数
const NO = ()=>false;
  1. 检测字符串是否为on开头的函数,以on开头,并且剩余字符不为小写字母
const onRE = /^on[^a-z]/;
const isOn = (key)=>onRE.test(key)

console.log(isOn('pnclick'));//false
console.log(isOn('onClick'));//true
  1. 监听器,检测是key是否以onUpdate开头
const isModeListener = (key)=>key.startsWith('onUpdate')
isModeListener('onClick')//false;
isModeListener('onUpdate')//true;
  1. 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(),从一个或多个源对象将自有属性和可枚举属性,复制到目标对象中

  1. remove 从数组中移除某一项
const remove = (arr,el)=>{
    const i = arr.indexOf(el)
    if(i>-1){
        arr.splice(i,1)
    }
}
// splice很耗费性能,axios拦截器里的源码直接将
// 某一项设置为null,节省了性能
  1. hasOwn 是否为自身拥有的属性
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val,key)=>hasOwnProperty.call(val,key)
//hasOwn([],'toString') false;
// hasOwn({a:1},'a') true;
  1. 是否为数组
const isArray = Array.isArray;
// const fakerArr = {__proto__:Array.prototype,length:0}
// fakerArr instanceof Array  true
// isArray(fakerArr) false;
// __proto__属性,可以设置某个对象的原型,骗过instanceoof,所以Array.isArray判断更为准确
  1. 获取toString方法,缩短了字符,方便随时调用,
const objectToString = Object.prototype.toString;
  1. 将对象转化成[object xxx]形式的字符串,来判断对象的类型
const toTypeString = (value)=>objectToString.call(value)
  1. isMap判断是否为Map对象
const isMap = (val) =>toTypeString(val) === '[object Map]'
  1. isSet判断是否为set对象
const isSet = (val)=>toTypeString(val) ==='[object Set]'
  1. 判断是否为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()获取对象的原型

  1. 判断是否为函数
const isFunction = (val)=>typeof val ==='function'
  1. 判断是否为字符串
const isString = (val)=>typeof val === 'string'
  1. 判断是否为symbol
const isSymbol = (val)=> typeof val == 'symbol';
// symbol是基本类型,Symbol是函数,不是构造函数,不能使用new关键字
  1. 判断是否为object
const isObject = (val)=>val!==null &&typeof val ==='object'
// typeof null 为object,所以需要加上typeof val ==='object'的判断
  1. 判断是否为Promise
const isPromise = (val)=>{
   return isObject(val)&&isFunction(val.then)&&isFunction(val.catch)
}

  1. 获取对象类型的字符串表示
const toRawType = (val)=>{
    return toTypeString(val).slice(8,-1)
}
//例如:toRawType('abc')会返回 String 这个字符串 

  1. 是否为纯对象
const isPlainObject = (val)=>toTypeString(val) === '[object Object]'
let Ctor = function (){this.name ='构造函数'}
isPlainObject(new Ctor()) //true;
  1. 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没有值,则返回空字符串。
  1. 创建一个键值对映射并返回一个函数检查key,是否在在这个映射中
    1. 重要提示,makeMap的调用都要加上/*#__PURE__*/的前缀,方便rollup可以做tree-shake的操作
    2. 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]
    }
    
  2. 是否为保留属性,空字符串也是包括在内的
const isReservedProp = /*#__PRUE__*/ makeMap(
    ',key,ref,'+
    'onVnodeBeforeMount,onVnodeMounted,'+
    'onVnodeBefoeUpdate,onVnodeUpdated,'+
    'onVnodeBeforeUnmount,onVnodeUnmounted'
)
// isReservedProp('ref') true
// isReservedProp('') true
  1. 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))
    }
}
  1. 连字符转转驼峰
const camlizeRE = /-(\w)/g;
const camlize = cacheStringFunction((str)=>{
    return str.replace(camlizeRE,(_,c)=>(c? c.toUpperCase():''))
})
  1. 驼峰转连字符
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = cacheStringFunction((str)=>str.replace(hyphenateRE,'-$1').toLowerCase())
  1. 首字母转大写
const capitalize = cacheStringFunction((str)=>str.charAt(0).toUpperCase() + str.slice(1))
  1. 事件key转化,添加on前缀 click => onclick
const toHandlerKey = cacheStringFunction((str)=>(str? `on${capitalize(str)}`:''))

// toHandlerKey('click') //onClick
  1. // 判断值是否改变,包括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;
        }
    }
})
  1. 批量执行数组里的函数
const invokeArrayFns = (fns,arg)=>{
    for (let i = 0; i < fns.length; i++) {
            fns[i](arg)        
    }
}
  1. 定义对象,用Object.definedProperty
const def = (obj,key,value)=>{
    Object.defineProperty(obj,key,{
        configurable:true,
        enumerable:false;
        value
    })
}
  1. 转数字
const toNumber = (val)=>{
    const n = parseFloat(val);
    return isNaN(n)? val:n;
}
// isNaN会将非number类型的参数,转化为number,然后比较
// 所以当传递空字符串或值为真的布尔值的时候,isNaN会返回false
// 用Number.isNaN时,传递NaN才会返回true
  1. 获取全局对象
let _globalThis;
const getGlobalThis = ()=>{
    return (_globalThis||
        (_globalThis=typeof _globalThis!=='undefined'?
            _globalThis:typeof self !=='undefined'?
            self:typeof window!=='undefined'?
            window:typeof global !=='undefined'?
            global:{}))
}

  1. globalThis可以获取全局的this对象,不用担心平台的问题

  2. 再次调用getGlobalThis能够直接获取_globalThis,,这种写法值得学习

3. 总结

  • 在README.md和CONTRIBUTE.md里可以看到项目的详细信息
  • 设置dev:sourcemap命令可以生成sourcemap文件,方便调试源码
  • 通过globalThis可以跨平台获取全局的this对象
  • 一些常用的方法可以抽取出来作为公用 比如说Object.prototype.hasOwnProperty;抽取出来,能提高性能,节省代码
  • Number.isNaN比isNaN方法更能准确的判断是不是NaN
  • __proto__是被标准移除的属性,尽量不要去使用它
  • 正则对字符串的判断,是十分有用,要学正则了