Vue3 和 React18 源码片段1——shared

最近在整合框架时,又一次涉及到了 Vue/React 相关源码,也包括tarjsraxjsuni-app等跨端 DSL,优秀的框架通常都会注重性能和代码执行细节,因此借此文/系列开始进行整理。

代码来源

一、工具函数部分shared

Vue:packages/shared

类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 稳妥的类型判断方式
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)

// Array类型判断,很直接用了Array.isArray,也可以用`toTypeString`判断。*移动端兼容没问题,PC IE9起步,不过可以用Polyfill
export const isArray = Array.isArray

// toTypeString判断。*Map和Set都是安卓5起、IE11起步,可以用Polyfill
export const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'

// 用了instanceof,根据原型判断。注意Date实例toTypeString的结果是`[object Object]`
export const isDate = (val: unknown): val is Date => val instanceof Date

// 简单的判断
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'

// 这里的Object比较宽泛,除了Object、Array外,其他Set、Map等引用类型
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'

// duck type,根据关键方法进行判断,所以包含这些属性方法也能过
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

// 严格的对象比较
export const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]'

数据信息判断

正整数字符串的判断:

1
2
3
4
5
6
// 正整数字符串的判断,主要排除NaN、Infinity、负数等边界
export const isIntegerKey = (key: unknown) =>
isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key

判断属性:

1
2
3
4
5
6
// 判断属性key是否为对象数据val本身属性
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)

数据处理

对象拓展/合并属性:

1
2
// 用于拓展/合并属性。*Object.assign兼容移动端还好、PC IE11起步,可用Polyfill
export const extend = Object.assign

删除数组元素:

1
2
3
4
5
6
export const remove = <T>(arr: T[], el: T) => {
const i = arr.indexOf(el)
if (i > -1) {
arr.splice(i, 1)
}
}

字符串转数字:

1
2
3
4
5
// 处理了NaN的边界
export const toNumber = (val: any): any => {
const n = parseFloat(val)
return isNaN(n) ? val : n
}

新旧值比较:

1
2
3
4
// oldValue === oldValue主要考虑了NaN
// compare whether a value has changed, accounting for NaN.
export const hasChanged = (value: any, oldValue: any): boolean =>
value !== oldValue && (value === value || oldValue === oldValue)

处理HTML转义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* @from escapeHtml.ts
*/

const escapeRE = /["'&<>]/

function escapeHtml(string) {
const str = '' + string
const match = escapeRE.exec(str) // 匹配值将进行转义替换

if (!match) {
return str
}

let html = ''
let escaped
let index
let lastIndex = 0
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) { // 根据 Unicode 编码进行判断
case 34: // "
escaped = '&quot;'
break
case 38: // &
escaped = '&amp;'
break
case 39: // '
escaped = '&#39;'
break
case 60: // <
escaped = '&lt;'
break
case 62: // >
escaped = '&gt;'
break
default:
continue
}

if (lastIndex !== index) {
html += str.substring(lastIndex, index)
}

lastIndex = index + 1
html += escaped
}

return lastIndex !== index ? html + str.substring(lastIndex, index) : html
}

// https://www.w3.org/TR/html52/syntax.html#comments
const commentStripRE = /^-?>|<!--|-->|--!>|<!-$/g

function escapeHtmlComment(src) {
return src.replace(commentStripRE, '') // 删除html注释
}

数据比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* @from looseEqual.ts
*/

import { isArray, isDate, isObject } from './'

function looseCompareArrays(a: any[], b: any[]) {
if (a.length !== b.length) return false
let equal = true
for (let i = 0; equal && i < a.length; i++) {
equal = looseEqual(a[i], b[i])
}
return equal
}

// 比较严格的比较
export function looseEqual(a: any, b: any): boolean {
if (a === b) return true
let aValidType = isDate(a)
let bValidType = isDate(b)
// Date数据类型比较
if (aValidType || bValidType) {
return aValidType && bValidType ? a.getTime() === b.getTime() : false
}
aValidType = isArray(a)
bValidType = isArray(b)
// Array数据类型比较
if (aValidType || bValidType) {
return aValidType && bValidType ? looseCompareArrays(a, b) : false
}
aValidType = isObject(a)
bValidType = isObject(b)
// Object数据类型比较
if (aValidType || bValidType) {
/* istanbul ignore if: this if will probably never be called */
if (!aValidType || !bValidType) {
return false
}
const aKeysCount = Object.keys(a).length
const bKeysCount = Object.keys(b).length
if (aKeysCount !== bKeysCount) {
return false
}
for (const key in a) {
const aHasKey = a.hasOwnProperty(key)
const bHasKey = b.hasOwnProperty(key)
if (
(aHasKey && !bHasKey) ||
(!aHasKey && bHasKey) ||
!looseEqual(a[key], b[key])
) {
return false
}
}
}
// NaN
return String(a) === String(b)
}

export function looseIndexOf(arr: any[], val: any): number {
return arr.findIndex(item => looseEqual(item, val))
}

React:packages/shared

React 的 shared 模块更为松散。

类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @from isArray.js
*/

// Flow声明
declare function isArray(a: mixed): boolean %checks(Array.isArray(a));

// 其实也是用了Array.isArray
const isArrayImpl = Array.isArray;

// 保证类型
// eslint-disable-next-line no-redeclare
function isArray(a: mixed): boolean {
return isArrayImpl(a);
}

export default isArray;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @from ReactSymbols.js
*/

const MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';

// 判断是否为迭代器函数,也是根据关键属性函数进行判断
export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> {
if (maybeIterable === null || typeof maybeIterable !== 'object') {
return null;
}
const maybeIterator =
(MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) ||
maybeIterable[FAUX_ITERATOR_SYMBOL];
if (typeof maybeIterator === 'function') {
return maybeIterator;
}
return null;
}

数据信息判断

属性判断:

1
2
3
4
5
6
7
/**
* @from hasOwnProperty.js
*/

const hasOwnProperty = Object.prototype.hasOwnProperty;

export default hasOwnProperty;

环境信息判断

是否为DOM环境:

1
2
3
4
5
6
7
8
9
/**
* @from ExecutionEnvironment.js
*/

export const canUseDOM: boolean = !!(
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
);

数据处理

对象拓展/合并:

1
2
3
4
5
6
7
8
/**
* @from assign.js
*/

// 跟Vue中extend函数一样
const assign = Object.assign;

export default assign;

数据比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @from objectIs.js
*/

// 算是Object.is的polyfill
function is(x: any, y: any) {
return (
// 后面的比较主要区分+0和-0
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}

// Object.is iOS9/安卓5起支持,IE不兼容
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === 'function' ? Object.is : is;

export default objectIs;

浅比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @from shallowEqual.js
*/

import is from './objectIs';
import hasOwnProperty from './hasOwnProperty';

// 浅比较

/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}

// 根据键数量先排除一波
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}

return true;
}

export default shallowEqual;


二、标识处理

Vue:Patch flag

Vue3的静态标记是diff性能优化的一个手段、主要是在与上次 virtual DOM 比较时,只对比带有Patch Flag的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// @from patchFlags.ts

// 业务内容不是本文关注的,
// 这里使用了掩码来避免比较冲突,对大部分前端同学来说其实比较陌生,
// 可见之前记录的位运算笔记:http://blog.michealwayne.cn/2019/10/29/notes/js%E4%B8%AD%E4%BD%8D%E8%BF%90%E7%AE%97%E7%9A%84%E9%AA%9A%E6%93%8D%E4%BD%9C/
// ShapeFlags.ts也用了相同的方案

export const enum PatchFlags {

TEXT = 1,

CLASS = 1 << 1, // 2

STYLE = 1 << 2, // 4

PROPS = 1 << 3, // 8

FULL_PROPS = 1 << 4, // 16

HYDRATE_EVENTS = 1 << 5, // 32

// ...
}

React:Symbol Flag

React16起,源码中的大多判断处理就依赖与Symbol常量

1
2
3
4
5
6
7
8
9
// @from ReactSymbols.js

// Symbol内存占用小,也有不重复、“私有”的特点
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
export const REACT_PORTAL_TYPE = Symbol.for('react.portal');
export const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');
export const REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode');
export const REACT_PROFILER_TYPE = Symbol.for('react.profiler');
export const REACT_PROVIDER_TYPE = Symbol.for('react.provider');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @from ReactTypes.js

export type ReactProvider<T> = {
$$typeof: Symbol | number, // 用Symbol可以避免一些如注入类的安全风险
type: ReactProviderType<T>,
key: null | string,
ref: null,
props: {
value: T,
children?: ReactNodeList,
...
},
...
};

shared 包中就不难发现,shared承担高频工具函数、标识和通用处理,也是整个框架中至关重要的代码。