Vue 原始碼中的工具函式

諾頓發表於2022-07-06

前言

Vue 原始碼中,封裝了很多工具函式,學習這些函式,一方面學習大佬們的實現方式,另一方面是溫習基礎知識,希望大家在日常工作中,簡單的函式也可以自己封裝,提高編碼能力。

本次涉及的工具函式 1-16 在 Vue3 的原始碼中,路徑是 core/packages/shared/src/index.ts

17-22 在 Vue2 的原始碼中,路徑是 vue/src/shared/util.ts

1. EMPTY_OBJ 空物件

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

注意:
Object.freeze 只能淺凍結,如果屬性是物件,對屬性的屬性的修改就無法凍結了

const obj = {
    name: '張三',
    info: {
        a: 1,
        b: 2
    }
};
Object.freeze(obj);
obj.name = '李四';
console.log(obj); // { name: '張三', info: { a: 1, b: 2 } }
obj.info.a = 66;
console.log(obj); // { name: '張三', info: { a: 66, b: 2 } }

原始碼中的使用:

可以看出基本都是作為初始化或者兜底使用,由此產生疑問:

  • 使用的地方有的是 options,有的是 props,不同地方用同一個物件,不會有問題麼?
    首先,很多初始化操作,後續都會重新賦值,EMPTY_OBJ 只是作為佔位使用。其次,因為 Object.freeze 的原因,無法修改 EMPTY_OBJ,所以任何引用這個物件的地方,都不會受到影響。
  • 為什麼判斷是 __DEV__(process.env.NODE_ENV !== 'production') 的時候才使用 Object.freeze?
    Object.freeze 更多的是 Vue 原始碼開發者在除錯時使用,可以通過報錯,防止對空物件操作,更快發現原始碼問題。也因此,開發環境最終會避免了對 EMPTY_OBJ 的賦值操作,所以在生產環境使用 Object.freeze 意義不大。

2. EMPTY_ARR 空陣列

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

3. NOOP 空函式

const NOOP = () => {}

依舊作為兜底和佔位使用:

4. NO 永遠返回 false 的函式

const NO = () => false

原始碼中的使用:

5. isOn 判斷字串是不是 on 開頭,並且 on 後首字母不是小寫字母

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

// 示例
isOn('onChange'); // true
isOn('onchange'); // false
isOn('on3change'); // true

6. 型別判斷

const isArray = Array.isArray

const isFunction = (val) => typeof val === 'function'
const isString = (val) => typeof val === 'string'
const isSymbol = (val) => typeof val === 'symbol'
const isObject = (val) => val !== null && typeof val === 'object'

const toTypeString = (value) => Object.prototype.toString.call(value)
const isMap = (val) => toTypeString(val) === '[object Map]'
const isSet = (val) => toTypeString(val) === '[object Set]'
const isDate = (val) => toTypeString(val) === '[object Date]'
const isPlainObject = (val) => Object.prototype.toString.call(val) === '[object Object]'

// isPlainObject 判斷是不是普通物件(排除正則、陣列、日期、new Boolean、new Number、new String 這些特殊的物件)
isObject([]) // true
isPlainObject([]) // false

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

7. toRawType 提取資料原始型別

const toRawType = (value) => {
  return Object.prototype.toString.call(value).slice(8, -1)
}

// 示例
toRawType('');  'String'
toRawType([]);  'Array'

原始碼中的使用:

8. isIntegerKey 判斷是不是數字型的字串

const isIntegerKey = (key) => isString(key) &&
    key !== 'NaN' &&
    key[0] !== '-' &&
    '' + parseInt(key, 10) === key;
  
// 例子:
isIntegerKey('a'); // false
isIntegerKey('0'); // true
isIntegerKey('011'); // false
isIntegerKey('11'); // true
isIntegerKey('-11'); // false
isIntegerKey(11); // false
isIntegerKey('NaN'); // false

9. makeMap 將字串分隔成 map,區分大小寫,返回一個函式來判斷 map 中是否含有某個 key

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];
}

10. isReservedProp 是否是保留屬性

const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,ref_for,ref_key,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted');
    
// ['', 'key', 'ref', 'ref_for', 'ref_key', 'onVnodeBeforeMount', 'onVnodeMounted', 'onVnodeBeforeUpdate', 'onVnodeUpdated', 'onVnodeBeforeUnmount', 'onVnodeUnmounted']

// 示例
isReservedProp('key'); // true
isReservedProp('onVnodeBeforeMount'); // true
isReservedProp(''); // true
isReservedProp(' '); // false

如果有 /*#__PURE__*/ 這個標誌,說明他是純函式,如果沒有呼叫它,打包工具會直接通 tree-shaking 把它刪除,減少程式碼體積。

11. isBuiltInDirective 是否是內建指令

const isBuiltInDirective = /*#__PURE__*/ makeMap(
  'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
)

12. cacheStringFunction 將函式變為可快取結果的函式

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

13. camelize & hyphenate 連字元與駝峰互轉

const camelizeRE = /-(\w)/g;
const camelize = cacheStringFunction((str) => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
// 清爽版
const camelize = str => str.replace(camelizeRE, (_, c) => {
    return c ? c.toUpperCase() : '';
});
// 舉例:on-click-a => onClickA
camelize('on-click-a');


const hyphenateRE = /\B([A-Z])/g;
const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());

// 清爽版
const hyphenate = str => str.replace(hyphenateRE, '-$1').toLowerCase();
// 仿照 camelize 寫法
const hyphenate = str => str.replace(hyphenateRE, (_, c) => {
    return c ? `-${c.toLowerCase()}` : '';
});
// 舉例:onClickA => on-click-a
hyphenate('onClickA');

14. hasChanged 判斷是不是有變化

const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);

// 示例
hasChanged(1, 1); // false
hasChanged(1, 2); // true
hasChanged(+0, -0); // false
hasChanged(NaN, NaN); // false
// 場景:watch 監測值是不是變化了

// 擴充套件 Object.is & ===
Object.is(+0, -0); // false           
Object.is(NaN, NaN); // true

+0 === -0 // true
NaN === NaN // false

15. invokeArrayFns 執行陣列裡的函式

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

// 示例
const arr = [
    function(val){
        console.log(val + '張三');
    },
    function(val){
        console.log(val + '李四');
    },
    function(val){
        console.log(val + '王五');
    },
]
invokeArrayFns(arr, '我是:');

原始碼中的使用:

16. toNumber 轉數字

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

toNumber('111'); // 111
toNumber('a111'); // 'a111'
toNumber('11a11'); // '11'
toNumber(NaN); // NaN

// isNaN vs Number.isNaN
// isNaN 判斷是不是數字 is Not a Number
// Number.isNaN 判斷是不是 NaN
isNaN(NaN); // true
isNaN('a'); // true
Number.isNaN(NaN); // true
Number.isNaN('a'); // false

// Number.isNaN 的 polyfill
if (!Number.isNaN) {
    Number.isNaN = function (n) {
        // 方法一
        return (window.isNaN(n) && typeof n === 'number');
        // 方法二 利用只有 NaN 不跟自己相等的特性
        return n !== n;
    };
}

17. isPrimitive 是否為原始資料

function isPrimitive(value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

18. isValidArrayIndex 是否為有效的陣列下標,整數並且不是無窮大

function isValidArrayIndex(val) {
  const n = parseFloat(String(val))
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}
// isFinite 如果引數是 NaN,正無窮大或者負無窮大,會返回 false,其他返回 true

19. bind 能相容的bind函式

function polyfillBind(fn, ctx) {
  function boundFn(a) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

function nativeBind(fn, ctx) {
  return fn.bind(ctx)
}

const bind = Function.prototype.bind ? nativeBind : polyfillBind

20. toArray 類陣列轉化為陣列

function toArray(list, start) {
  start = start || 0
  let i = list.length - start
  const ret = new Array(i)
  while (i--) {
    ret[i] = list[i + start]
  }
  return ret
}

21. once 只執行一次

function once(fn) {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

22. isNative 是否為原生系統函式

function isNative(Ctor) {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

參考資料

相關文章