学习Vue3 源码中那些实用的基础工具函数

目录
文章目录隐藏
  1. 前言
  2. 环境准备
  3. 工具函数
  4. 总结

vue3 源码中工具函数

前言

本文将用相对通俗易懂的方式来学习 Vue3 源码中的工具函数模块的源码,从而为自己所用。很多小伙伴都害怕读源码,认为很难,其实源码也不是想象的那么难,shared模块中 57 个工具函数,本次我们阅读其中的 30 余个。

环境准备

读开源项目 贡献指南

打开 vue-next, 开源项目一般都能在 README.md 或者 .github/contributing.md 找到贡献指南。

而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来,项目目录结构是怎样的。怎么投入开发,需要哪些知识储备等。

我们可以在 项目目录结构 描述中,找到shared模块。

shared: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).

README.mdcontributing.md 一般都是英文的。可能会难倒一部分人。其实看不懂,完全可以可以借助划词翻译,整页翻译和百度翻译等翻译工具。再把英文加入后续学习计划。

本文就是讲shared模块,对应的文件路径是:vue-next/packages/shared/src/index.ts

也可以用github1s访问,速度更快。github1s packages/shared/src/index.ts

按照项目指南 打包构建代码

为了降低文章难度,我按照贡献指南中方法打包把ts转成了js。如果你需要打包,也可以参考下文打包构建。

你需要确保 Node.js 版本是 10+, 而且 yarn 的版本是 1.x Yarn 1.x

你安装的 Node.js 版本很可能是低于 10。最简单的办法就是去官网重新安装。也可以使用 nvm等管理Node.js版本。

node -v
# v14.16.0
# 全局安装 yarn
# 克隆项目
git clone https://github.com/vuejs/vue-next.git
cd vue-next

# 或者克隆我的项目
git clone https://github.com/lxchuan12/vue-next-analysis.git
cd vue-next-analysis

npm install --global yarn
yarn # install the dependencies of the project
# yarn —ignore-scripts 忽略一些安装,更快速
yarn build

可以得到 vue-next/packages/shared/dist/shared.esm-bundler.js,文件也就是纯js文件。也接下就是解释其中的一些方法。

当然,前面可能比较啰嗦。我可以直接讲 工具函数。但通过我上文的介绍,即使是初学者,都能看懂一些开源项目源码,也许就会有一定的成就感。 另外,面试问到被类似的问题或者笔试题时,你说看 Vue3 源码学到的,面试官绝对对你刮目相看。

如何生成 sourcemap 调试 vue-next 源码

熟悉我的读者知道,我是经常强调生成sourcemap调试看源码,所以顺便提一下如何配置生成sourcemap,如何调试。这部分可以简单略过,动手操作时再仔细看。

其实贡献指南里描述了。

Build with Source Maps Use the --sourcemap or -s flag to build with source maps. Note this will make the build much slower.

所以在 vue-next/package.json 追加 "dev:sourcemap": "node scripts/dev.js --sourcemap"yarn dev:sourcemap执行,即可生成sourcemap,或者直接 build

// package.json
{
    "version": "3.2.1",
    "scripts": {
        "dev:sourcemap": "node scripts/dev.js --sourcemap"
    }
}

会在控制台输出类似vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js的信息。

其中packages/vue/dist/vue.global.js.map 就是sourcemap文件了。

我们在 Vue3 官网找个例子,在 vue-next/examples/index.html。其内容引入packages/vue/dist/vue.global.js

// vue-next/examples/index.html
<script src="../../packages/vue/dist/vue.global.js"></script>
<script>
    const Counter = {
        data() {
            return {
                counter: 0
            }
        }
    }

    Vue.createApp(Counter).mount('#counter')
</script>

然后我们新建一个终端窗口,yarn serve,在浏览器中打开http://localhost:5000/examples/,如下图所示,按F11等进入函数,就可以愉快的调试源码了。

如何生成 sourcemap 调试 vue-next 源码

工具函数

本文主要按照源码 vue-next/packages/shared/src/index.ts 的顺序来写。也省去了一些从外部导入的方法。

我们也可以通过ts文件,查看使用函数的位置。同时在VSCode运行调试 JS 代码,我们比较推荐韩老师写的code runner插件。

babelParserDefaultPlugins babel 解析默认插件

/**
 * List of @babel/parser plugins that are used for template expression
 * transforms and SFC script transforms. By default we enable proposals slated
 * for ES2020. This will need to be updated as the spec moves forward.
 * Full list at https://babeljs.io/docs/en/next/babel-parser#plugins
 */
const babelParserDefaultPlugins = [
    'bigInt',
    'optionalChaining',
    'nullishCoalescingOperator'
];

这里就是几个默认插件。感兴趣看英文注释查看。

EMPTY_OBJ 空对象

const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
    ? Object.freeze({})
    : {};

// 例子:
// Object.freeze 是 冻结对象
// 冻结的对象最外层无法修改。
const EMPTY_OBJ_1 = Object.freeze({});
EMPTY_OBJ_1.name = '码云';
console.log(EMPTY_OBJ_1.name); // undefined

const EMPTY_OBJ_2 = Object.freeze({ props: { mp: '码云笔记' } });
EMPTY_OBJ_2.props.name = '码云';
EMPTY_OBJ_2.props2 = 'props2';
console.log(EMPTY_OBJ_2.props.name); // '码云'
console.log(EMPTY_OBJ_2.props2); // undefined
console.log(EMPTY_OBJ_2);
/**
 * 
 * { 
 *  props: {
     mp: "码云笔记",
     name: "码云"
    }
 * }
 * */

process.env.NODE_ENV 是 node 项目中的一个环境变量,一般定义为:development 和production。根据环境写代码。比如开发环境,有报错等信息,生产环境则不需要这些报错警告。

EMPTY_ARR 空数组

const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];

// 例子:
EMPTY_ARR.push(1) // 报错,也就是为啥生产环境还是用 []
EMPTY_ARR.length = 3;
console.log(EMPTY_ARR.length); // 0

NOOP 空函数

const NOOP = () => { };

// 很多库的源码中都有这样的定义函数,比如 jQuery、underscore、lodash 等
// 使用场景:1. 方便判断, 2. 方便压缩
// 1. 比如:
const instance = {
    render: NOOP
};

// 条件
const dev = true;
if(dev){
    instance.render = function(){
        console.log('render');
    }
}

// 可以用作判断。
if(instance.render === NOOP){
 console.log('i');
}
// 2. 再比如:
// 方便压缩代码
// 如果是 function(){} ,不方便压缩代码

NO 永远返回 false 的函数

/**
 * Always return false.
 */
const NO = () => false;

// 除了压缩代码的好处外。
// 一直返回 false

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

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

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

onRE 是正则。^符号在开头,则表示是什么开头。而在其他地方是指非。

与之相反的是:$符合在结尾,则表示是以什么结尾。

[^a-z]是指不是az的小写字母。

isModelListener 监听器

判断字符串是不是以onUpdate:开头

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

// 例子:
isModelListener('onUpdate:change'); // true
isModelListener('1onUpdate:change'); // false
// startsWith 是 ES6 提供的方法

extend 继承 合并

说合并可能更准确些。

const extend = Object.assign;

// 例子:
const data = { name: '码云' };
const data2 = entend(data, { mp: '码云笔记', name: '是码云啊' });
console.log(data); // { name: "是码云啊", mp: "码云笔记" }
console.log(data2); // { name: "是码云啊", mp: "码云笔记" }
console.log(data === data2); // true

remove 移除数组的一项

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

// 例子:
const arr = [1, 2, 3];
remove(arr, 3);
console.log(arr); // [1, 2]

splice 其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。

引申axios InterceptorManager 拦截器源码 中,拦截器用数组存储的。但实际移除拦截器时,只是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行,同样效果。axios 拦截器这个场景下,不得不说为性能做到了很好的考虑。

看如下 axios 拦截器代码示例:

// 代码有删减
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
    this.handlers[id] = null;
}

// 执行
if (h !== null) {
    fn(h);
}

hasOwn 是不是自己本身所拥有的属性

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

// 例子:

// 特别提醒:__proto__ 是浏览器实现的原型写法,后面还会用到
// 现在已经有提供好几个原型相关的 API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数,并执行函数。

hasOwn({__proto__: { a: 1 }}, 'a') // false
hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性,不是通过原型链向上查找的。

isArray 判断数组

const isArray = Array.isArray;

isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
// 所以 instanceof 这种情况 不准确

isMap 判断是不是 Map 对象

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

// 例子:
const map = new Map();
const o = { p: 'Hello World' };

map.set(o, 'content');
map.get(o); // 'content'
isMap(map); // true

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

isSet 判断是不是 Set 对象

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

// 例子:
const set = new Set();
isSet(set); // true

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

isDate 判断是不是 Date 对象

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

// 例子:
isDate(new Date()); // true

// `instanceof` 操作符左边是右边的实例。但不是很准,但一般够用了。原理是根据原型链向上查找的。

isDate({__proto__ : new Date()); // true
// 实际上是应该是 Object 才对。
// 所以用 instanceof 判断数组也不准确。
// 再比如
({__proto__: [] }) instanceof Array; // true
// 实际上是对象。
// 所以用 数组本身提供的方法 Array.isArray 是比较准确的。

isFunction 判断是不是函数

const isFunction = (val) => typeof val === 'function';
// 判断数组有多种方法,但这个是比较常用也相对兼容性好的。

isString 判断是不是字符串

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

// 例子:
isString('') // true

isSymbol 判断是不是 Symbol

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

// 例子:
let s = Symbol();

typeof s;
// "symbol"
// Symbol 是函数,不需要用 new 调用。

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。

isObject 判断是不是对象

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

// 例子:
isObject(null); // false
isObject({name: '码云'}); // true
// 判断不为 null 的原因是 typeof null 其实 是 object

isPromise 判断是不是 Promise

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

// 判断是不是 Promise 对象
const p1 = new Promise(function(resolve, reject){
  resolve('若川');
});
isPromise(p1); // true

// promise 对于初学者来说可能比较难理解。但是重点内容,JS 异步编程,要着重掌握。
// 现在 web 开发 Promise 和 async await 等非常常用。

objectToString 对象转字符串

const objectToString = Object.prototype.toString;

// 对象转字符串

toTypeString 对象转字符串

const toTypeString = (value) => objectToString.call(value);

// call 是一个函数,第一个参数是 执行函数里面 this 指向。
// 通过这个能获得 类似  "[object String]" 其中 String 是根据类型变化的

toRawType 对象转字符串 截取后几位

const toRawType = (value) => {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};

// 截取到
toRawType('');  'String'

可以 截取到 String Array 等这些类型

是 JS 判断数据类型非常重要的知识点。

JS 判断类型也有 typeof ,但不是很准确,而且能够识别出的不多。

这些算是基础知识

// typeof 返回值目前有以下 8 种 
'undefined'
'object'
'boolean'
'number'
'bigint'
'string'
'symobl'
'function'

isPlainObject 判断是不是纯粹的对象

const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
// 
const isPlainObject = (val) => toTypeString(val) === '[object Object]';

// 前文中 有 isObject 判断是不是对象了。
// isPlainObject 这个函数在很多源码里都有,比如 jQuery 源码和 lodash 源码等,具体实现不一样

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

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

// 例子:
isInegerKey('a'); // false
isInegerKey('0'); // true
isInegerKey('011'); // false
isInegerKey('11'); // true
// 其中 parseInt 第二个参数是进制。
// 字符串能用数组取值的形式取值。
//  还有一个 charAt 函数,但不常用 
'abc'.charAt(0) // 'a'
// charAt 与数组形式不同的是 取不到值会返回空字符串'',数组形式取值取不到则是 undefined

makeMap && isReservedProp

传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。第二个参数是小写选项。

/**
 * Make a map and return a function for checking if a key
 * is in that map.
 * IMPORTANT: all calls of this function must be prefixed with
 * \/\*#\_\_PURE\_\_\*\/
 * So that rollup can tree-shake them if necessary.
 */
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 = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted');

// 保留的属性
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('onVnodeBeforeMount'); // true
isReservedProp('onVnodeMounted'); // true
isReservedProp('onVnodeBeforeUpdate'); // true
isReservedProp('onVnodeUpdated'); // true
isReservedProp('onVnodeBeforeUnmount'); // true
isReservedProp('onVnodeUnmounted'); // true

cacheStringFunction 缓存

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

这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。 《JavaScript 设计模式与开发实践》书中的第四章 JS 单例模式也是类似的实现。

var getSingle = function(fn){ // 获取单例
    var result;
    return function(){
        return result || (result = fn.apply(this, arguments));
    }
};

以下是一些正则

// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
// () 小括号是 分组捕获
const camelizeRE = /-(\w)/g;
/**
 * @private
 */
// 首字母转大写
const camelize = cacheStringFunction((str) => {
    return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
// \B 是指 非 \b 单词边界。
const hyphenateRE = /\B([A-Z])/g;
/**
 * @private
 */

const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());

// 举例:onClick => on-click
const hyphenateResult = hyphenate('onClick');
console.log('hyphenateResult', hyphenateResult); // 'on-click'

/**
 * @private
 */
// 首字母转大写
const capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));
/**
 * @private
 */
// click => onClick
const toHandlerKey = cacheStringFunction((str) => (str ? `on${capitalize(str)}` : ``));

const result = toHandlerKey('click');
console.log(result, 'result'); // 'onClick'

hasChanged 判断是不是有变化

// compare whether a value has changed, accounting for NaN.
const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
// 例子:
// 认为 NaN 是不变的
hasChanged(NaN, NaN); // false
hasChanged(1, 1); // false
hasChanged(1, 2); // false
// 场景
// watch 监测值是不是变化了

invokeArrayFns 执行数组里的函数

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

// 例子:
const arr = [
    function(val){
        console.log(val + '的博客地址是:https://mybj123.com');
    },
    function(val){
        console.log('百度搜索 码云笔记 可以找到' + val);
    },
    function(val){
        console.log('微信搜索 码云笔记 可以找到关注' + val);
    },
]
invokeArrayFns(arr, '我');

为什么这样写,我们一般都是一个函数执行就行。

数组中存放函数,函数其实也算是数据。这种写法方便统一执行多个函数。

def 定义对象属性

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

Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的APIObject.defineProperties(obj, props) (ES5)

Object.defineProperty 涉及到比较重要的知识点。

ES3中,除了一些内置属性(如:Math.PI),对象的所有的属性在任何时候都可以被修改、插入、删除。在ES5中,我们可以设置属性是否可以被改变或是被删除——在这之前,它是内置属性的特权。ES5中引入了属性描述符的概念,我们可以通过它对所定义的属性有更大的控制权。这些属性描述符(特性)包括:

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

另外,数据描述符(其中属性为:enumerableconfigurablevaluewritable)与存取描述符(其中属性为enumerableconfigurableset()get())之间是有互斥关系的。在定义了set()get()之后,描述符会认为存取操作已被 定义了,其中再定义valuewritable引起错误

以下是ES3风格的属性定义方式:

var person = {};
person.legs = 2;

以下是等价的 ES5 通过数据描述符定义属性的方式:

var person = {};
Object.defineProperty(person, 'legs', {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});

其中, 除了 value 的默认值为undefined以外,其他的默认值都为false。这就意味着,如果想要通过这一方式定义一个可写的属性,必须显示将它们设为true。 或者,我们也可以通过ES5的存储描述符来定义:

var person = {};
Object.defineProperty(person, 'legs', {
    set:function(v) {
        return this.value = v;
    },
    get: function(v) {
        return this.value;
    },
    configurable: true,
    enumerable: true
});
person.legs = 2;

这样一来,多了许多可以用来描述属性的代码,如果想要防止别人篡改我们的属性,就必须要用到它们。此外,也不要忘了浏览器向后兼容ES3方面所做的考虑。例如,跟添加Array.prototype属性不一样,我们不能再旧版的浏览器中使用shim这一特性。 另外,我们还可以(通过定义nonmalleable属性),在具体行为中运用这些描述符:

var person = {};
Object.defineProperty(person, 'heads', {value: 1});
person.heads = 0; // 0
person.heads; // 1  (改不了)
delete person.heads; // false
person.heads // 1 (删不掉)

toNumber 转数字

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

toNumber('111'); // 111
toNumber('a111'); // 'a111'
parseFloat('a111'); // NaN
isNaN(NaN); // true

其实 isNaN 本意是判断是不是 NaN 值,但是不准确的。

比如:isNaN('a') 为 true。 所以 ES6 有了 Number.isNaN 这个判断方法,为了弥补这一个API

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

getGlobalThis 全局对象

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

获取全局 this 指向。

初次执行肯定是 _globalThisundefined。所以会执行后面的赋值语句。

如果存在 globalThis 就用 globalThis

如果存在self,就用self。在 Web Worker 中不能访问到 window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。

如果存在window,就用window

如果存在global,就用globalNode环境下,使用global

如果都不存在,使用空对象。可能是微信小程序环境下。

下次执行就直接返回 _globalThis,不需要第二次继续判断了。这种写法值得我们学习。

总结

文中主要通过学习 shared 模块下的几十个工具函数,比如有:isPromisemakeMapcacheStringFunctioninvokeArrayFnsdefgetGlobalThis等等。

同时还分享了vue源码的调试技巧,

源码也不是那么可怕。平常我们工作中也是经常能使用到这些工具函数。通过学习一些简单源码,拓展视野的同时,还能落实到自己工作开发中,收益相对比较高。

原文:点击这里

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 学习Vue3 源码中那些实用的基础工具函数

发表回复