源码学习目录

1. 前言

1.1 调试环境

  1. 操作系统: macOS 11.5.2
  2. 浏览器: Chrome 94.0.4606.81
  3. shell: zsh (如何安装zsh,可以自行百度 ohmyzsh,windows系统需要使用WSL子系统安装)
  4. vue-next 3.2.4
  5. vscode 1.62.2
  6. 语言 typescript

    1.2 源码位置

    克隆代码 git clone https://github.com/vuejs/vue-next.git, 在packages/shared/index.ts中
    image.png

    1.3 阅读该文章可以get以下知识点

  7. Vue 3 源码 shared 模块中的一些实用工具函数

2. 工具函数

从入口函数index.ts开始

2.1 EMPTY_OBJ 空对象

  1. export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
  2. ? Object.freeze({})
  3. : {}
  4. EMPTY_OBJ.name = '11'
  5. // 赋值会报错
  6. // Cannot add property name, object is not extensible

Object.freeze方法冻结了对象,无法进行赋值操作,赋值会报错,但是做了环境区分,只针对dev环境

2.2 DEV 环境变量

  1. // global.d.ts 全局声明
  2. // Global compile-time constants
  3. declare var __DEV__: boolean
  4. // rollup.config.js 具体设置值
  5. import replace from '@rollup/plugin-replace'
  6. const replacements = {
  7. __DEV__: isBundlerESMBuild
  8. ? // preserve to be handled by bundlers
  9. `(process.env.NODE_ENV !== 'production')`
  10. : // hard coded dev/prod builds
  11. !isProduction,
  12. }
  13. // allow inline overrides like
  14. //__RUNTIME_COMPILE__=true yarn build runtime-core
  15. Object.keys(replacements).forEach(key => {
  16. if (key in process.env) {
  17. replacements[key] = process.env[key]
  18. }
  19. })
  20. return replace({
  21. // @ts-ignore
  22. values: replacements,
  23. preventAssignment: true
  24. })

环境变量DEV,在rollup.config.js中配置,使用了插件@rollup/plugin-replace,生成了全局环境变量,可以直接在代码中使用,这种用法可以学习一下在工具库中使用,需要结合rollup

2.3 EMPTY_ARR 空数组

  1. // 和空对象EMPTY_OBJ一样,不能操作
  2. export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []

2.4 NOOP 空函数

  1. export const NOOP = () => {}
  2. // 很多库的源码中都有这样的定义函数,比如 jQuery、underscore、lodash 等
  3. // 使用场景:1. 方便判断, 2. 方便压缩
  4. // 1. 比如:
  5. const instance = {
  6. render: NOOP
  7. };
  8. // 条件
  9. const dev = true;
  10. if(dev){
  11. instance.render = function(){
  12. console.log('render');
  13. }
  14. }
  15. // 可以用作判断。
  16. if(instance.render === NOOP){
  17. console.log('i');
  18. }
  19. // 2. 再比如:
  20. // 方便压缩代码
  21. // 如果是 function(){} ,不方便压缩代码

2.5 NO 返回false的函数

  1. /**
  2. * Always return false.
  3. */
  4. export const NO = () => false
  5. // 方便压缩代码

2.6 isOn 判断字符串是否以on开头,并且on后首字母不是小写字母

  1. const onRE = /^on[^a-z]/
  2. const isOn = (key: string) => onRE.test(key)
  3. // exapmle:
  4. isOn('onChange'); // true
  5. isOn('onchange'); // false
  6. isOn('on3change'); // true

正则 ^on 表示以on开头,[^a-z]表示on后面下一个字符不是小写

2.7 isModelListener 监听器

  1. // 判断字符串是不是以onUpdate:开头
  2. export const isModelListener = (key: string) => key.startsWith('onUpdate:')

2.8 extend = Object.assign

  1. // 合并 es5语法, 方便维护,如果需要对对象合并做一些操作,可以直接修改这里
  2. export const extend = Object.assign

2.9 remove 移除数组中的一项

  1. // 移除el这一项,函数存在副作用,通过splice移除,没有返回新的数组
  2. export const remove = <T>(arr: T[], el: T) => {
  3. const i = arr.indexOf(el)
  4. if (i > -1) {
  5. arr.splice(i, 1)
  6. }
  7. }
  8. // example:
  9. const arr = [1, 2, 3];
  10. remove(arr, 3);
  11. console.log(arr); // [1, 2]

2.10 hasOwn 判断是不是自身拥有的属性

  1. const hasOwnProperty = Object.prototype.hasOwnProperty
  2. export const hasOwn = (
  3. val: object,
  4. key: string | symbol
  5. ): key is keyof typeof val => hasOwnProperty.call(val, key)
  6. // example:
  7. hasOwn({ a: undefined }, 'a') // true
  8. hasOwn({}, 'a') // false
  9. hasOwn({}, 'hasOwnProperty') // false
  10. hasOwn({}, 'toString') // false
  11. // 是自己的本身拥有的属性,不是通过原型链向上查找的。

2.11 类型判断

  1. // 对象转化为字符串
  2. export const objectToString = Object.prototype.toString
  3. // 获取字符串的类型
  4. export const toTypeString = (value: unknown): string =>
  5. objectToString.call(value)
  6. // toTypeString 可以得到 "[object String]" 这种类型的字符串
  7. // 截取[object String]类型的String这一段,用来做类型判断
  8. export const toRawType = (value: unknown): string => {
  9. // extract "RawType" from strings like "[object RawType]"
  10. return toTypeString(value).slice(8, -1)
  11. }
  12. // 是不是纯粹的对象
  13. export const isPlainObject = (val: unknown): val is object =>
  14. toTypeString(val) === '[object Object]'
  15. // 判断是不是数组,伪数组不行
  16. export const isArray = Array.isArray
  17. // example:
  18. isArray([]); // true
  19. isArray({0: 1, length: 1}); // false
  20. // 是不是map类型
  21. export const isMap = (val: unknown): val is Map<any, any> =>
  22. toTypeString(val) === '[object Map]'
  23. // 是不是set类型
  24. export const isSet = (val: unknown): val is Set<any> =>
  25. toTypeString(val) === '[object Set]'
  26. // 是不是Date类型
  27. export const isDate = (val: unknown): val is Date => val instanceof Date
  28. // 是不是function类型
  29. export const isFunction = (val: unknown): val is Function =>
  30. typeof val === 'function'
  31. // 是不是String类型
  32. export const isString = (val: unknown): val is string => typeof val === 'string'
  33. // 是不是Symbol类型
  34. export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
  35. // 是不是object类型
  36. export const isObject = (val: unknown): val is Record<any, any> =>
  37. val !== null && typeof val === 'object'
  38. // 是不是Promise类型
  39. export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
  40. return isObject(val) && isFunction(val.then) && isFunction(val.catch)
  41. }
  42. // 判断有没有then和catch方法
  43. // 基本数据类型可以通过typeof来判断,复杂数据类型,需要通过Object.prototype.toString.call的方式来判断

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

  1. export const isIntegerKey = (key: unknown) =>
  2. isString(key) &&
  3. key !== 'NaN' &&
  4. key[0] !== '-' &&
  5. '' + parseInt(key, 10) === key
  6. // example:
  7. isIntegerKey('a'); // false
  8. isIntegerKey('0'); // true
  9. isIntegerKey('011'); // false
  10. isIntegerKey('11'); // true
  11. // parseInt第二个参数是进制,默认采用的是10进制

2.13 makeMap && isReservedProp

传入一个以逗号分隔的字符串,生成一个map(键值对),并返回一个函数检测key值是否存在map中,第二个参数将检测函数的key转换成小写

  1. /**
  2. * Make a map and return a function for checking if a key
  3. * is in that map.
  4. * IMPORTANT: all calls of this function must be prefixed with
  5. * \/\*#\_\_PURE\_\_\*\/
  6. * So that rollup can tree-shake them if necessary.
  7. */
  8. export function makeMap(
  9. str: string,
  10. expectsLowerCase?: boolean
  11. ): (key: string) => boolean {
  12. // 返回一个空对象 {}
  13. const map: Record<string, boolean> = Object.create(null)
  14. // 字符串以逗号分隔为数组1,2,3 => ['1','2','3']
  15. const list: Array<string> = str.split(',')
  16. for (let i = 0; i < list.length; i++) {
  17. map[list[i]] = true
  18. }
  19. // expectsLowerCase为true,对参数进行小写转换,否则不变, 最后判断是不是在str中存在
  20. return expectsLowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val]
  21. }
  22. export const isReservedProp = /*#__PURE__*/ makeMap(
  23. // the leading comma is intentional so empty string "" is also included
  24. ',key,ref,' +
  25. 'onVnodeBeforeMount,onVnodeMounted,' +
  26. 'onVnodeBeforeUpdate,onVnodeUpdated,' +
  27. 'onVnodeBeforeUnmount,onVnodeUnmounted'
  28. )
  29. // example:
  30. isReservedProp(''); // true
  31. isReservedProp('key'); // true
  32. isReservedProp('ref'); // true
  33. isReservedProp('onVnodeBeforeMount'); // true
  34. isReservedProp('onVnodeMounted'); // true
  35. isReservedProp('onVnodeBeforeUpdate'); // true
  36. isReservedProp('onVnodeUpdated'); // true
  37. isReservedProp('onVnodeBeforeUnmount'); // true
  38. isReservedProp('onVnodeUnmounted'); // true

2.14 cacheStringFunction 缓存

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

2.15 一些正则函数

  1. const camelizeRE = /-(\w)/g
  2. /**
  3. * 连字符转换驼峰
  4. * @private
  5. */
  6. export const camelize = cacheStringFunction((str: string): string => {
  7. return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
  8. })
  9. // example:
  10. camelize('list-item') // listItem
  11. const hyphenateRE = /\B([A-Z])/g
  12. /**
  13. * \B 是指 非 \b 单词边界
  14. * 驼峰转换连字符
  15. * @private
  16. */
  17. export const hyphenate = cacheStringFunction((str: string) =>
  18. str.replace(hyphenateRE, '-$1').toLowerCase()
  19. )
  20. // example:
  21. camelize('listItem') // list-item
  22. /**
  23. * 首字母大写
  24. * @private
  25. */
  26. export const capitalize = cacheStringFunction(
  27. (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
  28. )
  29. /**
  30. * 给key加一个on并且单子首字母大写
  31. * @private
  32. */
  33. export const toHandlerKey = cacheStringFunction((str: string) =>
  34. str ? `on${capitalize(str)}` : ``
  35. )
  36. const result = toHandlerKey('click');
  37. console.log(result, 'result'); // 'onClick'

2.16 hasChanged 判断是否有变化

  1. // compare whether a value has changed, accounting for NaN.
  2. export const hasChanged = (value: any, oldValue: any): boolean =>
  3. !Object.is(value, oldValue)
  4. // example:
  5. // NaN 是不变的
  6. hasChanged(NaN, NaN); // false
  7. hasChanged(1, 1); // false
  8. hasChanged(1, 2); // true
  9. hasChanged(+0, -0); // false
  10. // Obect.is 认为 +0 和 -0 不是同一个值
  11. Object.is(+0, -0); // false
  12. // Object.is 认为 NaN 和 本身 相比 是同一个值
  13. Object.is(NaN, NaN); // true

2.17 invokeArrayFns 执行数组里的函数,参数相同

  1. export const invokeArrayFns = (fns: Function[], arg?: any) => {
  2. for (let i = 0; i < fns.length; i++) {
  3. fns[i](arg)
  4. }
  5. }
  6. // example
  7. const arr = [
  8. function(val){
  9. console.log(val + '1');
  10. },
  11. function(val){
  12. console.log(val + '2');
  13. },
  14. function(val){
  15. console.log(val + '3');
  16. },
  17. ]
  18. invokeArrayFns(arr, 'i'); // i1 i2 i3

2.18 def 定义对象属性

  1. export const def = (obj: object, key: string | symbol, value: any) => {
  2. Object.defineProperty(obj, key, {
  3. configurable: true,
  4. enumerable: false,
  5. value
  6. })
  7. }

Object.defineProperty的一些属性

value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。

2.19 toNumber 转数字

  1. export const toNumber = (val: any): any => {
  2. const n = parseFloat(val)
  3. return isNaN(n) ? val : n
  4. }
  5. toNumber('111'); // 111
  6. toNumber('a111'); // 'a111'
  7. parseFloat('a111'); // NaN
  8. isNaN(NaN); // true

es6有个方法专门判断是不是NaN值

  1. Number.isNaN('a') // false
  2. Number.isNaN(NaN); // true

2.20 getGlobalThis 全局对象

  1. let _globalThis: any
  2. export const getGlobalThis = (): any => {
  3. return (
  4. _globalThis ||
  5. // 下次执行就直接返回 _globalThis,不需要第二次继续判断了。这种写法值得我们学习。
  6. (_globalThis =
  7. typeof globalThis !== 'undefined'
  8. ? globalThis
  9. : typeof self !== 'undefined'
  10. ? self
  11. : typeof window !== 'undefined'
  12. ? window
  13. : typeof global !== 'undefined'
  14. ? global
  15. : {})
  16. )
  17. }
  18. // 如果都不存在,使用空对象。可能是微信小程序环境下。

3. 总结

  1. 写对象类型常量时可以在dev环境添加Object.freeze,防止被注入改写
  2. getGlobalThis用了大量三目运算符,其实可以用switch或者其他方式来代替,阅读性更强
  3. 写一些转换函数,截取字符,可以使用正则,正则很强大,需要好好学习

    参考

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

  5. 尤雨溪开发的 vue-devtools 如何安装,为何打开文件的功能鲜有人知?
  6. 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数
  7. Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?
  8. vue-next/
  9. 正则表达式教程
  10. Promise