Skip to main content

· 2 min read

什么是 storybook​

storybook是一个组件开发工具,它提供了完整的组件开发的生态环境,包括插件、用例、文档等等。

storybook 支持 react、vue、angular、web component 等主流前端 UI 框架。

怎么用 storybook​

storybook 需要安装到已经设置了框架的项目中。

新建 react 应用

npx create-react-app react-app

初始化 storybook 配置

npx storybook init

启动 storybook

npm run storybook

image.png

给组件写 story,一个 story 表示组件的一种状态。

story 的文件名约定为.stories.js。

import React from 'react';

import { Button } from './Button';

export default {
title: 'Button', // 侧边栏导航名称
component: Button, // 组件地址
};

export const Primary = () => <Button primary>Button</Button>;

storybook 呈现的用例展示如下

image.png

storybook 附带了内置的工具栏:

  • canvas:用于缩放组件、设置背景、设置尺寸及方向
  • docs:用于查看组件文档,从组件代码自动推断

image.png

storybook 附带了内置的插件:

  • controls:支持动态修改组件参数
  • actions:验证交互是否通过回调产生正确的输出

参考资料​

· 3 min read

背景​

在一个大中台项目中,怎么高效高性能的将多个子项目互相嵌入?

熟知的微前端解决方案有 micro-app、qiankun、webpack5 module federation 等等。

那 webpack5 module federation 是什么、如何使用、它的原理是什么呢?

本文将一一揭晓。

module federation 是什么​

module federation 译为模块联邦,意思是模块可以在多个应用之间互相使用,应用 a 可以导出模块,也可以使用应用 b 导出的远程模块。

module federation 就是微前端的一种,而这种微前端和 micro-app、qiankun 等微前端框架确有本质的区别。

颗粒度的定义:

  • module federation:多个互相独立的模块聚合;
  • qiankun:多个互相独立的应用聚合。

技术实现:

  • module federation:打包的 chunk 的聚合;
  • qiankun:打包的 main.js 的聚合。

如何使用​

在 webpack5 项目中新增 module federation plugin 配置。

配置项如下:

  • name:应用名称,当作为 remote 引用时,路径为 name/expose;
  • library:声明全局变量的方式,name 为 umd 的 name;
  • filename:构建输出的文件名;
  • remotes:远程引用的应用名及其别名的映射,使用时以 key 值作为 name;
  • exposes:被远程引用时可暴露的资源路径及其别名;
  • shared:与其他应用之间可以共享的第三方依赖;
    • requiredVersion: 依赖的版本;
    • singleton: 仅允许共享范围内的单个版本;
    • eager: 允许在初始块中使用这个共享模块。

项目 1 的配置:

new ModuleFederationPlugin({
name: 'host',
remotes: {
remote1: 'remote1@[remote1Url]/remoteEntry.js',
libs: 'libs@[libsUrl]/remoteEntry.js',
},
})

项目 2 的配置:

new ModuleFederationPlugin({
name: 'remote1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
},
remotes: {
libs: 'libs@[libsUrl]/remoteEntry.js',
},
})

通过 module federation 实现了项目 1 的 Button 组件在项目 2 的聚合。

示例 demo 见:https://github.com/jiaozitang/mf-demo。

原理​

module federation 远程模块 remotes 和共享模块 shared 都是异步 chunk,通过 import 动态引入,实现按需加载。

小结​

module federation 的应用场景有限,因为它仅仅是模块的聚合,不具备应用之间的隔离性。

通常适用于多个应用依赖不同组件库的组件的场景。

这些组件库将组件通过 module federation 暴露,应用通过 module federation 引入远程组件。

参考资料​

· 14 min read

一、高级类型​

交叉类型​

交叉类型就是通过 & 符号,将多个类型合并为一个类型。

interface I1 {
name: string;
}

interface I2 {
age: number;
}

type T3 = I1 & I2

const a: T3 = {
name: 'tj',
age: 11,
}

联合类型​

联合类型就是通过 | 符号,表示一个值可以是几种类型之一。

const a: string | number = 1

字符串字面量类型​

字符串字面量类型就是使用一个字符串类型作为变量的类型。

const a: 'number' = 'number'

数字字面量类型​

数字字面量类型就是使用一个数字作为变量的类型。

const a: 1 = 1

布尔字面量类型​

数字字面量类型就是使用一个布尔值作为变量的类型。

const a: true = true

字符串模板类型​

字符串模板类型就是通过 ES6 的模板字符串语法,对类型进行约束。

type https = `https://${string}`
const a:https = `https://jd.com`

二、操作符​

keyof​

keyof 用于获取某种类型的所有键,其返回值是联合类型。

// const a: 'name' | 'age' = 'name'
const a: keyof {
name: string,
age: number
} = 'name'

typeof​

typeof 用于获取对象或者函数的结构类型。

const a2 = {
name: 'tj',
}

type T1 = typeof a2 // {name: string}

function fn1(x: number): number {
return x * 10
}

type T2 = typeof fn1 // (x: number) => number

in​

in 用于遍历联合类型。

const obj = {
name: 'tj',
age: 11
}

type T5 = {
[P in keyof typeof obj]: any
}

/*
{
name: any,
age: any
}
*/

T[K]​

T[K] 用于访问索引,得到索引对应的值的联合类型。

interface I3 {
name: string,
age: number
}

type T6 = I3[keyof I3] // string | number

三、运算符​

非空断言运算符​

非空断言运算符 ! 用于强调元素是非 null 非 undefined,告诉 Typescript 该属性会被明确的赋值。

function Demo(): JSX.Element () {
const divRef = useRef<HTMLDivElement>()
useEffect(() => {
divRef.current!.scrollIntoView() // 断言divRef.current 是非 null 非 undefined
}, [])
return <div ref={divRef}>divDemo</div>
}

可选链运算符​

可选链运算符 ?. 用于判断左侧表达式的值是否是 null 或 undefined,如果是将停止表达式运行。

const a = b?.a

空值合并运算符​

空值合并运算符 ?? 用于判断左侧表达式的值是否是 null 或 undefined,如果不是返回右侧的值。

const a = b ?? 10

数字分隔符​

数字分隔符 _ 用于对长数字进行分割,便于数字的阅读,编译结果将会自动去除 _。

const num: number = 1_111_111_111

四、类型别名​

类型别名用来给一个类型起个新名字。

type Message = string | string[]
let greet = (message: Message) => {
// ...
}

五、类型断言​

类型断言就是告诉浏览器我非常确定的类型。

// 尖括号 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

七、类型守卫​

类型守卫就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。

interface A {
name: string;
age: number;
}
interface B {
sex: string;
home: string;
}
function doSomething(person: A | B): void {
if (person.name) {
// Error
// ...
}
}
// 使用 in 类型守卫
function doSomething(person: A | B): void {
if ('name' in person) {
// OK
// ...
}
}
// 使用 typeof 类型守卫
function A(a: string | number): string | number {
if (typeof a === 'string') {
// OK
return a + ''
}
if (typeof a === 'number') {
// OK
return a + 2
}
return ''
}
// instanceof类型守卫
class Foo {}
class Bar {}

function test(input: Foo | Bar) {
if (input instanceof Foo) {
// 这里 input 的类型「收紧」为 Foo
} else {
// 这里 input 的类型「收紧」为 Bar
}
}

八、泛型​

1. 泛型介绍​

泛型就是通过给类型传参,得到一个更加通用的类型,就像给函数传参一样。

如下我们得到一个通用的泛型类型 T1,通过传参,可以得到 T2 类型 string[]、T3 类型 number[]:

type T1<T> = T[]

type T2 = T1<string> // string[]

type T3 = T1<number> // number[]

如上 T 是变量,我们可以用任意其他变量名代替他。

type T4<A> = A[]

type T5 = T4<string> // string[]

type T6 = T4<number> // number[]

2. 命名规范​

在 Typescript 泛型变量的命名规范中,默认了 4 种常见的泛型变量名,为提高可读性,不建议改为其他的变量名来定义。

  • T:表示第一个参数
  • K: 表示对象的键类型
  • V:表示对象的值类型
  • E:表示元素类型

3. 泛型接口​

泛型接口和上述示例类似,为接口类型传参:

interface I1<T, U> {
name: T;
age: U;
}

type I2 = I1<string, number>

4. 泛型约束(extends 操作符)​

有时候,我们需要对泛型参数进行约束,限制每个变量的类型。Typescript 通过 extends 实现类型约束。

泛型约束语法如下:

泛型名 extends 类型

通过 T extends Length 约束了 T 的类型,必须是包含 length 属性,且 length 的类型必须是 number。

interface Length {
length: number
}

function fn1<T extends Length>(arg: T): number{
return arg.length
}

通过 extends 约束了 K 必须是 T 的 key。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

let tsInfo = {
name: "Typescript",
supersetOf: "Javascript",
difficulty: Difficulty.Intermediate
}

let difficulty: Difficulty =
getProperty(tsInfo, 'difficulty'); // OK

let supersetOf: string =
getProperty(tsInfo, 'superset_of'); // Error

5. 泛型参数默认值​

泛型参数默认值,和函数参数默认值一样,在没有传参时,给定默认值。

interface I4<T = string> {
name: T;
}

const S1: I4 = { name: '123' } // 默认 name: string类型
const S2: I4<number> = { name: 123 }

6. 泛型条件​

条件类型和 Js 的条件判断意思一样,都是指如果满足条件,就 xx,否则 xx。

条件类型表达式:

T extends U ? X : Y

如果 T 能够赋值给 U,那么类型是 X,否则类型是 Y。

type T1<T> = T extends string ? 'string' : 'number'
type T2 = T1<string> // 'string'
type T3 = T1<number> // 'number

7. 泛型推断(infer 操作符)​

泛型推断的关键字是 infer,语法是 infer 类型。

一般是和泛型条件结合使用,结合实际例子理解:

如果泛型参数 T 能赋值给类型 {t: infer Test},那么类型是推断类型 Test,否则类型是 string。

type Foo<T> = T extends {t: infer Test} ? Test : string
  • 泛型参数 number 没有 t 属性,所以类型是 string
type One = Foo<number> // string
  • 泛型参数的 t 属性是 boolean,所以类型是推断类型 boolean
type Two = Foo<{ t: boolean }> // boolean

泛型参数的 t 属性是 () => void,所以类型是推断类型 () => void

type Three = Foo<{ a: number, t: () => void }> // () => void

8. 泛型工具类型​

映射类型​

映射类型,它是一种泛型类型,可用于把原有的对象类型映射成新的对象类型。

常见的映射类型语法:

{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }

举例说明,通过映射类型将所有属性变为可选:

type Partial<T> = {
[P in keyof T]?: T[P]
}

Partial​

Typescript 已将一些常用的映射类型进行封装,如 Partial 就是用于将泛型的全部属性变为可选。

type Partial<T> = {
[P in keyof T]?: T[P]
}

type T1 = Partial<{
name: string,
}>

const a: T1 = {} // 没有name属性也不会报错

Required​

Required 将泛型的所有属性变为必选。

type Required<T> = {
[P in keyof T]-?: T[P]
}

type T2 = Required<{
name?: string,
}>

const b: T2 = {} // ts报错,类型 "{}" 中缺少属性 "name",但类型 "Required<{ name?: string | undefined; }>" 中需要该属性。ts(2741)

语法-?,是把?可选属性减去的意思

Readonly​

Readonly 将泛型的所有属性变为只读。

type T3 = Readonly<{
name: string,
}>

const c: T3 = {
name: 'tj',
}

c.name = 'tj1' // ts 报错,无法分配到 "name" ,因为它是只读属性。ts(2540)

Pick​

Pick 从类型中选择一下属性,生成一个新类型。

type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}

type T4 = Pick<
{
name: string,
age: number,
},
'name'
>

/*

这是一个新类型,T4={name: string}

*/

const d: T4 = {
name: 'tj',
}

Record​

Record 将 key 和 value 转化为 T 类型。

type Record<K extends keyof any, T> = {
[key in K]: T
}

const e: Record<string, string> = {
name: 'tj',
}

const f: Record<string, number> = {
age: 11,
}

keyof any 对应的类型为 number | string | symbol,是可以做对象键的类型集合。

ReturnType​

ReturnType 获取 T 类型对应的返回值类型。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

Exclude​

Exclude 将某个类型中属于另一个的类型移除掉。

type Exclude<T, U> = T extends U ? never : T;

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract​

Extract 从 T 中提取出 U。

type Extract<T, U> = T extends U ? T : never;

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void

Omit​

Omit 使用 T 类型中除了 K 类型的所有属性,来构造一个新的类型。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

interface Todo {
title: string;
completed: boolean;
description: string;
}

type TodoPreview = Omit<Todo, "description">;

/*
{
title: string;
completed: boolean;
}
*/

NonNullable​

NonNullable 用来过滤类型中的 null 及 undefined 类型。

type NonNullable<T> = T extends null | undefined ? never : T;

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

Parameters​

Parameters 用于获得函数的参数类型组成的元组类型。

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;

type A = Parameters<() =>void>; // []
type B = Parameters<typeofArray.isArray>; // [any]
type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
type D = Parameters<typeofMath.max>; // number[]

参考资料​

· 10 min read

本文将从什么是函数式编程、函数式编程的特点、如何使用函数式编程等 3 个方面带你入门 Javascript 函数式编程。

一、什么是函数式编程​

先来一个官方的解释:

函数式编程,是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

1. 举例说明​

有一个数据转换的需求,需要将 ['tom', 'bob', 'alice'] 转换为 [{name: 'Tom'}, {name: 'Bob'}, {name: 'Alice'}]。

解析一下功能点:

  • 字符串数组转 key 为 name 数组对象;
  • name 字符串转换为首字符大写。

命令式编程​

命令式编程实现该需求,实现思路如下:

  • 声明临时变量 arr2 存放新数组;
  • 声明临时变量 first、rest 存放字符串,并进行转首字母大写的操作;
  • 通过 array.map 得到新数组,赋值给 arr2。

实现代码如下:

const arr = ['tom', 'bob', 'alice']
const arr2 = arr.map(i => {
const first = i.substring(0, 1)
const rest = i.substring(1, i.length)
return {
name: first.toUpperCase() + rest.toLowerCase()
}
})

函数式编程​

函数式编程实现该需求,实现思路如下:

  • 字符串转首字母大写的函数;
  • 字符串生成对象的函数;
  • 字符串转指定格式对象的函数;
  • 数组转数组对象的函数。

实现代码如下:

const { curry, compose, map } = require('ramda')

// 首字符大写
const capitalize = (x) => x[0].toUpperCase() + x.slice(1).toLowerCase()

// 字符串生成对象
const genObj = curry((key, x) => {
let obj = {}
obj[key] = x
return obj
})

// 字符串转对象,ex:'tom' => { name: 'Tom' }
const convert2Obj = compose(genObj('name'), capitalize)

// 数组转数组对象,ex:['tom'] => [{ name: 'Tom' }]
const convertName = map(convert2Obj)

console.log(convertName(['tom', 'bob', 'alice']))

这儿使用到了 ramda 包中的 curry 和 compose 函数。

从上面例子可知,函数式编程就是通过函数的组合变换去解决问题的一种编程方式。

在函数式编程中,函数是头等对象,意思是说一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。

二、函数式编程的特点​

在上述例子中,函数式编程看起来比命令式编程更加复杂,难以理解,那我们为什么要学习函数式编程,它能够给我们带来什么呢?

学习函数式编程,是为了让我们在编程过程中有更多编程范式的选择,可以根据不同的场景选择不同的编程范式。

那为什么 React、Redux 这些流行框架都选择并推荐函数式编程呢?因为函数式编程相较于命令式编程、面向对象编程而言,更易维护,可读性高,方便扩展。

1. 声明式编程​

函数式编程大多时候都是在声明我需要做什么,而非怎么去做。这种编程风格称为 声明式编程。

举例说明:在使用 React 时,只要描述UI,接下来状态变化后 UI 如何更新,是 React 在运行时帮你处理的,而不用开发者去写渲染逻辑和优化 diff 算法。

声明式编程具有以下优点:

  • 简化开发者的工作
  • 减少重复工作
  • 留下改进的空间
  • 提供全局协调能力

更详细的声明式编程介绍:从年会看声明式编程(Declarative Programming)。

2. 纯函数​

函数式编程使用纯函数组合变换计算,纯函数指的是相同的输入,永远会得到相同的输出,纯函数有以下特征:

2.1 不依赖外部状态​

纯函数不会依赖全局变量、this 等外部状态。

// 非纯函数
let counter = 0

function increment() {
// 引用了外部变量
return counter++
}

// 纯函数
const increment = (counter) => counter + 1

2.2 数据不可变​

纯函数不修改全局变量,不修改入参,不修改对象,当需要修改一个对象时,应该创建一个新的对象用来修改,而不是修改已有的对象。

// 非纯函数
let obj = {}

function genObj() {
obj.a = 'a'
}

// 纯函数
const genObj = (a) => {
return {
[a]: a
}
}

2.3 没有副作用​

纯函数没有副作用,副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用可能包含,但不限于:更改文件系统、往数据库插入记录、发送一个 http 请求、可变数据、打印/log、获取用户输入、DOM 查询、访问系统状态等。

2.4 小结​

纯函数不会指向不明的 this、不会引用全局变量、不会直接修改数据,可以大大的提升代码的易维护性。

三、如何使用函数式编程​

函数式编程中,函数是一等公民,那么怎么把一个复杂函数转换成多个单元函数,然后怎么把多个单元函数组合起来按顺序依次执行呢,这时候就需要用到柯里化(curry)和函数组合(compose)了。

1. 柯里化(curry)​

柯里化的就是将一个多元函数,转换成一个依次调用的单元函数。

f(a,b,c) → f(a)(b)(c)

如下是一个柯里化的加法函数:

var add = function(x) {
return function(y) {
return x + y;
};
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

如下是使用 ramda 封装的 curry 生成的单元函数:

const { curry } = require('ramda');
const replace = curry((a, b, str) => str.replace(a, b));
const replaceSpaceWith = replace(/\s*/);
const replaceSpaceWithComma = replaceSpaceWith(',');
const replaceSpaceWithDash = replaceSpaceWith('-');

2. 函数组合(compose)​

函数组合就是将多个函数组合成一个函数。

如下是使用 ramda 封装的 compose 生成的函数组合:

const compose = require('ramda')

const f = x => x + 1;
const g = x => x * 2;
const fg = compose(f, g);
fg(1) //3

函数组合让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。

小结​

通过上文,相信你已经对函数式编程有了一些了解,下方有一些参考资料,推荐你进行更深入的学习。

学习函数式编程只是为了让你在开发过程中多一个编程范式的选择,大多时候我们无法将所有函数写成纯函数的形式,但是我们仍可以学习函数式编程的不依赖外部状态、不改写数据的做法,通过减少共享的数据来减少开发过程的 bug 数。

希望本文能对你有所帮助,感谢阅读~

别忘了点个赞鼓励一下我哦,笔芯❤️

参考资料​

· 12 min read

本文从 Hooks 究竟是什么,为什么要使用 Hooks,React 提供了哪些常用 Hooks,以及如何自定义 Hooks 4 个方面带你深入理解 React Hooks。

一、前言​

Hooks 用于在不编写 class 的情况下,使用 state 以及其他 React 特性。那么 Hooks 究竟是什么,为什么要使用 Hooks,React 提供了哪些常用 Hooks,以及如何自定义 Hooks 呢,下文将为您一一揭晓。

本文的学习资料主要来自 React 官方文档-Hook和王沛老师的专栏《React Hooks 核心原理与实战》。

二、什么是 Hooks​

Hooks 译为钩子,Hooks 就是在函数组件内,负责钩进外部功能的函数。

React 提供了一些常用钩子,React 也支持自定义钩子,这些钩子都是用于为函数引入外部功能。

当我们在组件中,需要引入外部功能时,就可以使用 React 提供的钩子,或者自定义钩子。

比如在组件内引入可管理 state 的功能,就可以使用 useState 函数,下文会详细介绍 useState 的用法。

三、为什么要用 Hooks​

使用 Hooks 有 2 大原因:

  • 简化逻辑复用;
  • 让复杂组件更易理解。

1. 简化逻辑复用​

在 Hooks 出现之前,React 必须借用高阶组件、render props 等复杂的设计模式才能实现逻辑的复用,但是高阶组件会产生冗余的组件节点,让调试更加复杂。

Hooks 让我们可以在无需修改组件结构的情况下复用状态逻辑,下文会详细介绍自定义 Hooks 的用法。

2. 让复杂组件更易理解​

在 class 组件中,同一个业务逻辑的代码分散在组件的不同生命周期函数中,而 Hooks 能够让针对同一个业务逻辑的代码聚合在一块,让业务逻辑清晰地隔离开,让代码更加容易理解和维护。

四、常用的 Hooks​

1. useState​

useState 是允许你在 React 函数组件中添加 state 的 Hook。

使用示例如下:

import React, { useState } from 'react';

function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
// ...

以上代码声明了一个初始值为 0 的 state 变量 count,通过调用 setCount 来更新当前的 count。

2. useEffect​

useEffect 可以让你在函数组件中执行副作用操作。

副作用是指一段和当前执行结果无关的代码,常用的副作用操作如数据获取、设置订阅、手动更改 React 组件中的 DOM。

useEffect 可以接收两个参数,代码如下:

useEffect(callback, dependencies)

第一个参数是要执行的函数 callback,第二个参数是可选的依赖项数组 dependencies。

其中依赖项是可选的,如果不指定,那么 callback 就会在每次函数组件执行完后都执行;如果指定了,那么只有依赖项中的值发生变化的时候,它才会执行。

使用示例如下:

function Example() {
const [count, setCount] = useState(0);

// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;

return () => {
// 可用于做清除,相当于 class 组件的 componentWillUnmount
}

}, [count]); // 指定依赖项为 count,在 count 更新时执行该副作用
// ...

以上代码通过 useEffect 实现了当依赖项 count 更新时,执行副作用函数,并通过返回回调函数清除上一次的执行结果。

另外,useEffect 提供了四种执行副作用的时机:

  • 每次 render 后执行:不提供第二个依赖项参数。比如 useEffect(() => {});
  • 仅第一次 render 后执行:提供一个空数组作为依赖项。比如 useEffect(() => {}, []);
  • 第一次以及依赖项发生变化后执行:提供依赖项数组。比如 useEffect(() => {}, [deps]);
  • 组件 unmount 后执行:返回一个回调函数。比如 useEffect() => { return () => {} }, [])。

3. useCallback​

useCallback 定义的回调函数只会在依赖项改变时重新声明这个回调函数,这样就保证了组件不会创建重复的回调函数。而接收这个回调函数作为属性的组件,也不会频繁地需要重新渲染。

使用示例如下:

const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])

以上代码在依赖项 a、b 发生变化时,才会重新声明回调函数。

4. useMemo​

useMemo 定义的创建函数只会在某个依赖项改变时才重新计算,有助于每次渲染时不会重复的高开销的计算,而接收这个计算值作为属性的组件,也不会频繁地需要重新渲染。

使用示例如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

以上代码在依赖项 a、b 发生变化时,才会重新计算。

5. useRef​

useRef 返回一个 ref 对象,这个 ref 对象在组件的整个生命周期内持续存在。

他有 2 个用处:

  • 保存 DOM 节点的引用;
  • 在多次渲染之间共享数据。

保存 DOM 节点的引入使用示例如下:

function TextInputWithFocusButton() {
const inputEl = useRef(null)
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus()
}
return (
<>
<input ref={inputEl} type='text' />
<button onClick={onButtonClick}>Focus the input</button>
</>
)
}

以上代码通过 useRef 创建了 ref 对象,保存了 DOM 节点的引用,可以对 ref.current 做 DOM 操作。

在多次渲染之间共享数据示例如下:

import React, { useState, useCallback, useRef } from 'react'

export default function Timer() {
// 定义 time state 用于保存计时的累积时间
const [time, setTime] = useState(0)

// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null)

// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1)
}, 100)
}, [])

// 暂停计时的事件处理函数
const handlePause = useCallback(() => {
// 使用 clearInterval 来停止计时
window.clearInterval(timer.current)
timer.current = null
}, [])

return (
<div>
{time / 10} seconds.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
)
}

以上代码通过 useRef 创建了一个变量名为 timer 的 ref 对象,该对象可以在跨组件渲染时调用,在开始计时时新建计时器,在暂停计时时清空计时器。

6. useContext​

useContext 用于接收一个 context 对象并返回该 context 的值,可以实现跨层级的数据共享。

示例如下:

// 创建一个 context 对象
const MyContext = React.createContext(initialValue)
function App() {
return (
// 通过 Context.Provider 传递 context 的值
<MyContext.Provider value='1'>
<Container />
</MyContext.Provider>
)
}

function Container() {
return <Test />
}

function Test() {
// 获取 Context 的值
const theme = useContext(MyContext) // 1
return <div></div>
}

以上代码通过 useContext 取得了 App 组件中定义的 Context,做到了跨层次组件的数据共享。

7. useReducer​

useReducer 用来引入 Reducer 功能。

示例如下:

const [state, dispatch] = useReducer(reducer, initialState)

它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的 dispatch 函数。

五、自定义 Hooks​

通过自定义 Hooks,可以将组件逻辑提取到可重用的函数中。

1. 如何创建自定义 Hooks?​

自定义 Hooks 就是函数,它有 2 个特征区分于普通函数:

  • 名称以 “use” 开头;
  • 函数内部调用其他的 Hook。

示例如下:

import { useState, useCallback } from 'react'

function useCounter() {
// 定义 count 这个 state 用于保存当前数值
const [count, setCount] = useState(0)
// 实现加 1 的操作
const increment = useCallback(() => setCount(count + 1), [count])
// 实现减 1 的操作
const decrement = useCallback(() => setCount(count - 1), [count])
// 重置计数器
const reset = useCallback(() => setCount(0), [])

// 将业务逻辑的操作 export 出去供调用者使用
return { count, increment, decrement, reset }
}

// 组件1
function MyComponent1() {
const { count, increment, decrement, reset } = useCounter()
}

// 组件2
function MyComponent2() {
const { count, increment, decrement, reset } = useCounter()
}

以上代码通过自定义 Hooks useCounter,轻松的在 MyComponent1 组件和 MyComponent2 组件之间复用业务逻辑。

2. 自定义 Hooks 库 - react-use​

React 官方提供了 react-use 库,其中封装了大量可直接使用的自定义 Hooks,帮助我们简化组件内部逻辑,提高代码可读性、可维护性。

其中我们常用的自定义 Hooks 有:

  • useLocation 和 useSearchParam:跟踪页面导航栏位置状态;
  • useScroll:跟踪 HTML 元素的滚动位置;
  • useScrolling:跟踪 HTML 元素是否正在滚动;
  • useAsync, useAsyncFn, and useAsyncRetry:解析一个 async 函数;
  • useTitle:设置页面的标题。

可至 react-use 官网学习使用。

六、小结​

本文从 Hooks 究竟是什么,为什么要使用 Hooks,React 提供了哪些常用 Hooks,以及如何自定义 Hooks 4 个方面介绍了 React Hooks,相信大家对 React Hooks 已经有了更加深入的理解。

希望能对你有所帮助,感谢阅读~

别忘了点个赞鼓励一下我哦,笔芯 ❤️

参考资料​

· 14 min read

s08502502142022

本文从晋升体系、学习方法、做事方法 4 个方向介绍了学习大厂晋升指南的心得,其中晋升体系、职级详解提升认知,学习方法、做事方法提高效率,相信会对你有所帮助。

一、前言​

在比较长的一段时间,我的学习方向非常随心所欲,想学什么就开始读官方文档,找源码,学习效率不高。

我也开始反思,我这样的学习方法是不是太过于浪费好不容易在下班后挤出的宝贵时间。

我开始找提高学习效率的教程,于是找到了《大厂晋升指南》。在专栏中,我找到了好的学习方法和做事方法,下文将分享我的一些学习心得。

薅羊毛 tips: 如果你有点进去你会发现这个专栏是收费的!不过不怕,可以薅羊毛,下载 app 可以免费阅读 5 天,差不多也能学完这个专栏。

二、晋升体系​

在第一章节晋升体系中,了解职级体系、晋升流程、晋升原则、晋升逻辑、职级档次,不再无的放矢的寻找晋升的方法。

1. 职级体系​

互联网公司晋升体系包含介跨越式职级体系和阶梯式职级体系两种,跨越式相邻级别差异大、晋升机会少,阶梯式相邻级别差异小、晋升机会多。

了解了职级体系后,判断自己所在公司属于哪个类别,清晰自己的目标。

s11141202112022

2. 晋升流程​

晋升流程包括主管提名->部门内初筛->评委团考察->部门调控->高层确认->主管/HR 沟通。

  • 主管提名:绩效满足、年限满足、能力满足;
  • 部门内初筛:横向对比多个团队提名的人时,能力满足;
  • 评委团考察:答辩材料过关、答辩现场表现过关;
  • 部门调控:横向对比多个团队答辩的人时,答辩过关;
  • 高层确认:等待即可;
  • 主管/HR 沟通:等待即可。

因此,技术能力和答辩的技巧是同等重要,不仅在日常要储备技术能力,也要加强表达技巧,能够把自己优秀的点非常好的表现出来。

3. 晋升原则​

晋升有 3 大原则,满足以下原则会更容易晋升:

  • 主动原则:主动做事(主动找 leader 沟通工作、主动找别人沟通了解更多信息);
  • 成长原则:一边做事一边挖掘成长点提升自己(而不是一直原地踏步用熟悉的技术);
  • 价值原则:学习为公司产出价值的技能(而不是只学习自己感兴趣的技能)。

因此,在日常工作中,应该更加主动的沟通工作,在工作中发掘可成长的点,如引用新技术,并且持续的学习对工作有帮助的技能。

4. 晋升逻辑​

怎么判断有没有达到晋升要求呢?应该满足以下两点:

  • 能够做好当前级别的事(做到精通的水平,精通=优化和创造新的经验);
  • 寻找机会提前做下一级别的事,并取得工作成果。

因此,在日常工作中,不能仅仅埋头干活,还应该思考优化,去创造新的经验,在完全精通当前工作时,主动沟通,寻找机会提前做下一级别的事。

因为晋升和表白一样,表白是两个人时机到了互有感觉就能成功,而晋升是你已经掌握了下一级别的能力就能晋升到下一级别。两者都是充分准备后,水到渠成的事情。

5. COMD 能力模型​

通过 COMD 能力模型把抽象的能力要求具体化,帮助我们清晰成长的目标包含的能力范围。

COMD 能力模型包含 4 种复杂度 + 3 个维度,核心思想是通过事情的复杂度来判断能力的高低。

4 种复杂度:

  • 规模复杂度:规模越大,规模复杂度越高;
  • 时间复杂度:时间跨度越大,时间复杂度越高;
  • 环境复杂度:环境不确定性越高,环境复杂度越高;
  • 创新复杂度:创新程度越高,创新复杂度越高。

3 个维度:

  • 技术;
  • 业务;
  • 管理。

COMD 模型应用在 P6 的例子:

s11383702112022

掌握 COMD 模型后,更加清晰各个级别的能力要求,可以按照列出的能力要求进行针对性提升。

6. 职级档次​

以阿里为例列举了 P5-P10 的职级档次对应的角色:

s11264202112022

一般来说,高级别的能力要求默认包含了低级别的要求。

三、职级详解​

从技术、业务、管理 3 个方向对 P5-P9 进行职级详解:

各个职级具体的提升攻略见《大厂晋升指南-职级详解》。

1. P5​

  • 技术:工作岗位中实际用到的基础技术的积累;
  • 业务:熟悉各项业务的处理逻辑;
  • 管理:了解公司的管理制度和项目流程。

2. P6​

  • 技术:掌握团队用到的技术“套路”;
  • 业务:掌握所有功能并深度理解处理逻辑;
  • 管理:推进项目中的子任务。

3. P7​

  • 技术:精通团队相关技术;
  • 业务:关注业务整体;
  • 管理:指挥 10 人以内的小团队。

4. P8​

  • 技术:精通领域相关技术;
  • 业务:熟悉多个业务或精通端到端业务;
  • 管理:核心是抓重点。

5. P9​

  • 技术:跨领域整合能力;
  • 业务:从理解规划到亲自导演;
  • 管理:授权但不要放羊。

s11272702112022

四、学习方法​

1. 学习方向​

找到正确的学习方向,学习方向应该围绕以下 2 个方面:

  • 工作强相关(与工作弱相关的学习优先级可降低);
  • 能产出价值(没有产出的学习优先级可降低)。

2. 链式学习法​

链式学习法,可以提升技术深度,让知识形成锁链,环环相扣。

分为以下步骤:

  • 明确一项技术的深度可以分为哪些层;
  • 明确要学到哪一层;
  • 明确每一层应该怎么学。

3. 比较学习法​

比较学习法,通过横向对比不同技术,让技术选型更加有理有据。

分为以下步骤:

  • 整理领域关键技术点;
  • 整理不同技术的差异点;
  • 整理差异点背后的原理和对应用场景的影响。

4. 间隔学习法​

该方法来自大厂晋升指南 - 19 | 链式 & 比较 & 环式学习法:怎么多维度提升技术能力?评论区

长期记忆的形成,需要有个巩固的过程,可能是数小时,可能是数天,在这期间,记忆痕迹得到加深,所学的新知识与旧知识建立连接,带来稳固的长期记忆,因此不要频繁的进行集中式学习,而是有间隔的进行。

拿学习专栏来说,不要反复地去学习同一章节,而是有间隔地进行,等遗忘一些后进行练习,能够形成长久的记忆。

5. Teach 学习法​

通过教别人来提升自己,我的方式是写博客。

学习完技术后,将学到的东西通过博客的方式,传授给其他人。

在写博客的过程中可以巩固、梳理知识,并且输出学习成果,加强学习的深度。

s11450402112022

五、做事方法​

1. 3C 方案设计法​

每次做事的时候至少设计 3 个方案,择优执行。

能够帮助我们系统的梳理一个领域,提升整体流程和工作效率。

2. PDCA 执行法​

PDCA 执行法把事情的执行过程分成四个环节,保证具体事项高效高质地落地:

  • 计划:确定具体任务、阶段目标、时间节点和具体责任人;
  • 执行:落地各项具体活动;
  • 检查:检查实际执行结果;
  • 行动:明确下一步需要采取的措施。

3. 5Why 分析法​

找准问题源头才能治标又治本,通过追问 5 个为什么来分析问题的根本原因,从而得到彻底的解决方案。

4. 金字塔汇报法​

金字塔汇报法来源于金字塔原理,金字塔原理的核心思想是任何事情都可以归纳出一个中心思想,中心思想可由三到七个论点支持,每个论点可以由三到七个论据支撑。

金字塔汇报法:

  • 总体结论:先抛出关键性结论;
  • 具体分析:分析结论的因果;
  • 关键事项:介绍做过的关键事项的情况;
  • 总结改进:总结经验教训和后续改进措施。

关键事项汇报技巧:

  • 全局大图:展示整体情况;
  • 演进路径:展示个体的发展路径和当前所处阶段;
  • 时间轴:展示事情发生过程;

s21093702112022

5. 5W1H1D 分析法​

5W1H1D 分析法用于帮助我们深入理解业务,理解业务功能=分析功能需求+分析质量需求+总结上线效果。

5W:需求产生的背景和功能上线后的运行环境。

1H:如何去实现,整个流程是怎么运行的。

1D:上线之后的业务效果。

5W 的具体描述如下:

  • 何时:需要用到该功能的时间;
  • 何地:需要用到该功能的地点,也可以指场所,如地铁、开车等;
  • 何人:该功能面向的人群;
  • 何事:这个功能具体是什么,一般描述在需求文档中;
  • 何因:为什么需要这个功能,只有真正了解客户提出需求的驱动力,才能真正解决客户的问题。

通过 5W1H8C1D 分析法快速入门,上线前分析和理解业务功能,上线后熟悉运行数据。

六、小结​

本文从晋升体系、职级详解、学习方法、做事方法 4 个方向介绍了学习《大厂晋升指南》的心得,其中晋升体系、职级详解提升认知,学习方法、做事方法提高效率,想了解更多可以至原文-《大厂晋升指南》进行学习。

希望能对你有所帮助,感谢阅读~

别忘了点个赞鼓励一下我哦,笔芯 ❤️

参考资料​

· 7 min read

原文:https://www.michaelagreiler.com/code-review-checklist-2/

原文作者:Dr. McKayla

一、前言​

Code Review 可以帮助我们提高代码质量、减少项目问题,那么您知道 Code Review 可以从哪些地方开始审查吗?

下文将列出一个详细的代码审查清单,该清单是作者 Dr. McKayla 在 Microsoft 与数百名工程师一起工作,分析了数千次代码审查后,总结得出的最全代码审核清单。

它分为 7 个独立的部分,每个部分都会引导我们完成几个问题。

二、代码审查清单​

1. 功能检查​

  • 此代码更改是否完成了它应该做的事情?
  • 这个解决方案可以简化吗?
  • 您是否会以在代码的可维护性、可读性、性能和安全性等方面有更好的方式解决问题?
  • 代码库中是否有类似的功能?如果有,为什么不复用此功能?
  • 这段代码是否遵循面向对象的分析和设计原则,如单一职责原则、开闭原则、Liskov 替换原则、接口隔离、依赖注入?

2. bug 检查​

  • 您能想到代码未按预期运行的任何用例吗?
  • 您能想到任何可能破坏代码的输入或外部事件吗?

3. 依赖项检查​

  • 如果此更改需要在代码之外进行更新,例如更新文档、配置、自述文件,是否已完成?
  • 这种变化是否会对系统的其他部分产生任何影响,是否已经兼容?
  • 如果代码处理用户输入,它是否解决了跨站点脚本、SQL 注入等安全漏洞,它是否进行输入清理和验证?

4. 可用性和可访问性​

  • 从可用性的角度来看,提议的解决方案是否设计良好?
  • API 是否有据可查?
  • UI 是否可访问?
  • API/UI 使用起来是否直观?

5. 表现​

  • 您是否认为此代码更改会对系统性能产生负面影响?
  • 您是否看到任何提高代码性能的潜力?

6. 测试​

  • 代码是否可测试?
  • 它是否有足够的自动化测试(单元/集成/系统测试)?
  • 现有的测试是否合理地涵盖了代码更改?
  • 是否有一些测试用例、输入或边缘用例需要额外测试?

7. 可读性​

  • 代码容易理解吗?
  • 哪些部分让您感到困惑,为什么?
  • 可以通过更小的方法来提高代码的可读性吗?
  • 代码的可读性可以通过不同的函数/方法或变量名来提高吗?
  • 代码是否位于正确的文件/文件夹/包中?
  • 更多注释会使代码更易于理解吗?
  • 是否可以通过使代码本身更具可读性来删除一些注释?
  • 您认为某些方法应该被重组以拥有更直观的控制流程吗?

三、自审代码​

代码审查清单不仅适用于代码审查人员。我们应该并首先成为自己的审查者,遵循代码审查最佳实践。

因此,在发送代码进行审核之前,请确保:

  • 代码编译并通过静态分析,没有警告
  • 代码通过所有测试(单元、集成和系统测试)
  • 您已经仔细检查了拼写错误并进行了清理(评论、待办事项等)
  • 您概述了此更改的内容,包括更改的原因和更改的内容
  • 除此之外,作为代码作者,您应该通过与审阅者相同的代码审查清单。

四、关注重要问题​

作为代码审查员,您的任务是首先寻找最重要的问题。结构或逻辑问题比代码格式等小问题更有价值。

一份出色的清单将您的注意力引导到重要和最有价值的问题上。

五、自动化编码风格及约定​

清晰的编码风格指南是在代码库中强制执行一致性的唯一方法。而且,一致性使代码审查更快,允许人们轻松更改项目,并使您的代码库保持可读性和可维护性。

上文的审查清单没有介绍编码风格相关的内容,是因为我们建议使用自动化工具来强制遵守编码风格。通过安装及配置 prettier、eslint、tslint、stylelint 等格式化工具,节省编码风格的代码审查时间。

六、注意表达方式​

最后,代码审查反馈的质量不仅取决于您在说什么,还取决于您怎么说。 建议将您的反馈表述为建议而不是要求。 例如,不要写“变量名应该是 removeObject ”,而是说“调用变量 removeObject 怎么样?”。

七、总结​

代码审查清单以及围绕代码审查的明确规则和指南至关重要。代码审查清单可以使您的代码审查实践对您的团队更加有益,并显着加快代码审查速度。

本文详细介绍了 Code Review 的 7 种审查类型,帮助您在 Code Review 聚焦到重要和最有价值的问题上,提升 Code Review 质量。

· 7 min read
Jiaozi

一、背景​

需求就是生产力,常规项目的文档说明,大多放在 README.md 下进行记录和说明,而对于为外部赋能的项目来说,一个对外开放的文档系统是必不可少的。

如下是可供参考一个标准的在线文档系统界面 - taro 官网:

image.png

二、技术选型​

首先理清搭建一个在线的文档系统的要求:

  1. 内置 markdown 文件转网页
  2. 对 SEO 友好
  • 可通过 React 扩展网页
  • 界面美观
  • 文档清晰,上手简单,方便部署
  • 拓展功能丰富,如可搜索、文档版本化

在上述需求背景下,找出了以下可供参考的技术栈:

下表格数据来源:文档网站生成工具选型

开源工具对比HexoVuePressDocuteDocsifyDocusaurus
文档生成方式预先渲染 HTML预先渲染 HTML运行时解析运行时解析预先渲染 HTML
对 SEO 友好程度友好友好不友好不友好友好
语法-VueVueVueReact
官网地址hexovuepressdocutedocsifydocusaurus
适用场景个人博客需要 SEO 支持的技术文档公司或团队内部的文档系统公司或团队内部的文档系统需要 SEO 支持的技术文档
特点与主题解耦,更换主题成本低采用 vue,对 vue 开发友好Docute(60kB)使用 Vue,Vue Router 和 VuexDocsify(20kB)使用的是 vanilla JavaScript 采用 React,对 React 开发友好

选择使用了上手简单、对 SEO 友好、功能丰富的 Docusaurus 来搭建文档系统。

三、快速搭建​

1. 开始​

1.1 新建项目​

安装  Node,并创建新的 Docusaurus 站点

npx create-docusaurus@latest my-website classic

1.2 启动项目​

本地启动项目

yarn start

一个本地的文档系统已经搭建完成:

image.png

2. 目录结构​

熟悉 Docusaurus 文档系统的目录结构,清晰后续自定义配置及文档存放位置。

.
├── blog // 包含博客的 Markdown 文件
│ └── 2022-01-13-first-blog-post.markdown
├── docs // 包含文档的 Markdown 文件
│ ├── README.markdown
│ ├── api.markdown
│ └── changelog.markdown
├── src // 非文档文件
│ ├── components
│ │ ├── HomepageFeatures.js
│ │ └── HomepageFeatures.module.css
│ ├── css
│ │ └── custom.css
│ └── pages // 转换成网站页面
│ ├── index.js
│ ├── index.module.css
│ └── markdown-page.markdown
├── static // 静态资源
│ └── img
├── docusaurus.config.js // 配置文件
└── sidebars.js // 指定侧边栏文档顺序

3. 自定义内容​

熟悉了目录结构后,开始自定义配置,将初始化的文档项目,改成我们自己的内容。

3.1 配置站点元数据​

包括:

  • title:标题
  • url:文档系统域名
  • baseUrl:域名下的一级地址
  • favicon:网站图标

修改 docusaurus.config.js:

const config = {
title: 'distribute-sdk',
url: 'http://tls-pre.jd.com',
baseUrl: '/distribute-sdk-docs/',
favicon: 'img/favicon.ico',
};

3.2 配置导航栏​

包括导航栏、logo、主站名称、coding 地址。

修改 docusaurus.config.js:

const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: 'Distribute SDK',
logo: {
alt: 'Distribute SDK Logo',
src: 'https://img11.360buyimg.com/ling/jfs/t1/103667/23/20676/2779/61d59cd2Ef2665258/239330f23ecbae81.png',
href: 'docs/',
},
items: [
{
type: 'doc',
docId: 'README',
position: 'left',
label: '文档',
},
{ to: '/blog', label: 'Blog', position: 'left' },
{
href: 'xx', // git remote 地址
label: 'Coding',
position: 'right',
},
],
},
}),
};

3.3 新增文档​

在 blog 路径下新建 markdown 文件,会以标题为顺序,自动生成一级目录,展示 blog 下的 markdown 文件转的静态网页。

s11364401222022 s11362001222022

在 docs 路径下新建 markdown 文件,以 markdown 文件内声明的 sidebar_position 大小排序,自动生成一级目录。展示 docs 下的 markdown 文件转的静态网页。

s11381701222022

s11383101222022

如下是文档网站效果:

Untitled7.gif

四、丰富功能​

1. 自动部署​

通过公司内部 talos 系统内新建项目,并在 coding 配置 webhook,实现自动部署。

中间遇到了 talos 上编译时,node 版本低于 Docusaurus 要求的 v14 问题,故只能将编译流程放在本地进行。

s11441601222022

外网可通过 Github Pages、Gitee Pages 实现自动部署。

2. 自动更新 changelog​

lerna version 提供自动更新 changelog 功能,本文档系统也是为 lerna 搭建的项目服务。

规范了如下发布 lerna 版本流程,可实现更新版本时自动更新文档系统内的 changelog 页面:

  • lerna version --conventional-commits 确定版本号并自动生成 changelog;

  • npm run changelog 将自动生成的 changelog 部署至文档系统(写一个脚本复制文件至指定位置即可);

  • lerna publish from-git 发布版本。

五、总结​

本文讲述了快速搭建文档系统的完整过程,总结为以下 3 点:

  • 技术选型,根据需求场景选择合适的手段实现功能;
  • 通过官方文档快速搭建网站;
  • 根据需求丰富更多功能。

往期精彩​

参考资料​

· 6 min read

一、背景​

在开发的工作中,我们都引用过大量的社区优秀的开源项目,但怎么才能更好的了解这些开源项目呢,答案是 Join it。

参与开源项目,能够帮助我们拓宽对研发项目的认识,更好的理解开源项目的原理,以及提高个人影响力、竞争力。

二、选择项目​

人对于不熟悉的东西总是觉得高深莫测的,没有参与开源项目的经验的时候,会对参与开源项目不知道从何下手。

其实不然,在我们开发日常需求时就可以参与到开源项目中来,开发时用到的技术栈,就是我们最值得入手的开源项目了。

像我自己日常有开发微信小程序、京东小程序等跨平台的需求,这类型需求主要技术栈是 Taro,Taro 本身就是 github star 近 30 k 的优秀开源项目了,那么我就可以从 Taro 着手,参与到开源项目的建设工作中。

image.png

三、快速开始​

首先要了解、遵守开源项目的贡献规范,一般可以在官网找到贡献规范文档,如 Taro 贡献指南。

1. 确定贡献形式​

贡献形式多种多样,下面我以 “提交代码” 类型快速开始贡献流程。

s17110101222022

2. 找到感兴趣的 issue​

Taro 官网:issue 中会列出所有被标记为 Help Wanted 的 Issues,并且会被分为 Easy、 Medium、 Hard 三种难易程度。

通过 issue 标签筛选,选择自己感兴趣的 issue,可以先从 "Easy" 的开始:

s17454801222022

在 issue 中 Assignees 至自己:

s17463701222022

3. fork & clone​

fork Taro 源码至自己仓库:

s17280901222022

clone 个人仓库的 Taro 源码至本地:

git clone https://github.com/jiaozitang/taro

4. 本地开发环境​

在 Taro 源码项目中安装依赖并编译:

# 安装依赖
$ yarn

# 编译
$ yarn build

查看该 issue 涉及哪些 package,为这些 package 设置 yarn link,并在本地编译,使得调试项目能够 link 到开发中的源码:

Taro package 说明见文档:Taro 仓库概览

# yarn link
$ cd packages/taro-components
$ yarn link

# 本地编译
$ yarn dev

新建 Taro 项目用于调试 Taro 源码:

# 使用 npm 安装 CLI
$ npm install -g @tarojs/cli

# 初始化项目
$ taro init myApp

# yarn link
$ yarn link "@tarojs/components"

5. 开始开发​

5.1 功能开发​

通过以下步骤解决上述 “textarea 组件 onLineChange 时间调用无效” issue:

源码路径:packages/taro-components/src/components/textarea/textarea.tsx

onLineChange 事件:

  • 新增 onLineChange 事件
  • 监听输入事件,输入时通过行高计算行数
  • 行数改变时触发 onLineChange

auto-height 属性:

  • 新增 auto-height 属性
  • 新增 auto-height 样式

具体代码见:https://github.com/NervJS/taro/pull/10681/files

5.2 更新测试用例​

在测试用例中添加对 onLineChange 事件、aotu-height 属性的测试。

源码路径:packages/taro-components/__tests__/textarea.spec.js

5.3 更新文档​

在 README 中更新对 onLineChange、auto-height 的描述。

源码地址:packages/taro-components/src/components/textarea/index.md

5.4 自测​

在本地测试项目 myApp 中,自测 onLineChange 事件、auto-height 属性功能是否正常,自测通过后,在 Taro 源码项目中跑自动化测试。

# 自动化测试
$ npm run test

6. 提交 pull request​

测试通过后,在 github 中提交 pull requrst。

s18260601222022

7. code review 流程​

提交 pull request 后等待社区 code review,并及时跟进 code review 反馈进行修改。

s09142901242022

四、总结​

本文讲述了参与大型开源项目-Taro 的流程,其中以为 Taro 解决 issue 的视角,介绍了从认领 issue、解决 issue、贡献代码的完整过程。

在这个过程中,我们可以了解到如何参与开源项目并帮助开源项目解决问题,在开发工作中遇到开源项目的问题时,就可以愉快的参与进来了,不用因为一个小问题耽搁项目进度。

星星之火,可以燎原,在越来越多的开发者的参与下,开源社区的发展未来可期。

参考资料​

· 30 min read

为什么是铂金呢,因为和王者还有很远的距离。本文仅实现简单版本的 React,参考 React 16.8 的基本功能,包括虚拟 DOM、Fiber、Diff 算法、函数式组件、hooks 等。

一、前言

本文基于 https://pomb.us/build-your-own-react/ 实现简单版 React。

本文学习思路来自 卡颂-b站-React源码,你在第几层。

模拟的版本为 React 16.8。

将实现以下功能:

  1. createElement(虚拟 DOM);
  2. render;
  3. 可中断渲染;
  4. Fibers;
  5. Render and Commit Phases ;
  6. 协调(Diff 算法);
  7. 函数组件;
  8. hooks;

下面上正餐,请继续阅读。

二、准备

1. React Demo​

先来看看一个简单的 React Demo,代码如下:

const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);

本例完整源码见:reactDemo

在浏览器中打开 reactDemo.html,展示如下:

image.png

我们需要实现自己的 React,那么就需要知道上面的代码到底做了什么。

1.1 element​

const element = <div>123</div> 实际上是 JSX 语法。

React 官网 对 JSX 的解释如下:

JSX 是一个 JavaScript 语法扩展。它类似于模板语言,但它具有 JavaScript 的全部能力。JSX 最终会被 babel 编译为 React.createElement() 函数调用。

通过 babel 在线编译 const element = <div>123</div> 。

image.png

可知 const element = <div>123</div> 经过编译后的实际代码如下:

const element = React.createElement("div", {
title: "foo"
}, "hello");

再来看看上文的 React.createElement 实际生成了一个怎么样的对象。

在 demo 中打印试试:

const element = <div title="foo">hello</div>
console.log(element)
const container = document.getElementById('container')
ReactDOM.render(element, container);

可以看到输出的 element 如下:

image.png

简化一下 element:

const element = {
type: 'div',
props: {
title: 'foo',
children: 'hello'
}
}

简单总结一下,React.createElement 实际上是生成了一个 element 对象,该对象拥有以下属性:

  • type: 标签名
  • props
    • title: 标签属性
    • children: 子节点

1.2 render​

ReactDOM.render() 将 element 添加到 id 为 container 的 DOM 节点中,下面我们将简单手写一个方法代替 ReactDOM.render()。

  1. 创建标签名为 element.type 的节点;
const node = document.createElement(element.type)
  1. 设置 node 节点的 title 为 element.props.title;
node["title"] = element.props.title
  1. 创建一个空的文本节点 text;
const text = document.createTextNode("")
  1. 设置文本节点的 nodeValue 为 element.props.children;
text["nodeValue"] = element.props.children
  1. 将文本节点 text 添加进 node 节点;
node.appendChild(text)
  1. 将 node 节点添加进 container 节点
container.appendChild(node)

本例完整源码见:reactDemo2

运行源码,结果如下,和引入 React 的结果一致:

image.png

三、开始

上文通过模拟 React,简单代替了 React.createElement、ReactDOM.render 方法,接下来将真正开始实现 React 的各个功能。

1. createElement(虚拟 DOM)​

上面有了解到 createElement 的作用是创建一个 element 对象,结构如下:

// 虚拟 DOM 结构
const element = {
type: 'div', // 标签名
props: { // 节点属性,包含 children
title: 'foo', // title 属性
children: 'hello' // 子节点,注:实际上这里应该是数组结构,帮助我们存储更多子节点
}
}

根据 element 的结构,设计了 createElement 函数,代码如下:

/**
* 创建虚拟 DOM 结构
* @param {type} 标签名
* @param {props} 属性对象
* @param {children} 子节点
* @return {element} 虚拟 DOM
*/
function createElement (type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
)
}
}
}

这里有考虑到,当 children 是非对象时,应该创建一个 textElement 元素, 代码如下:

/**
* 创建文本节点
* @param {text} 文本值
* @return {element} 虚拟 DOM
*/
function createTextElement (text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
}
}

接下来试一下,代码如下:

const myReact = {
createElement
}
const element = myReact.createElement(
"div",
{ id: "foo" },
myReact.createElement("a", null, "bar"),
myReact.createElement("b")
)
console.log(element)

本例完整源码见:reactDemo3

得到的 element 对象如下:

const element = {
"type": "div",
"props": {
"id": "foo",
"children": [
{
"type": "a",
"props": {
"children": [
{
"type": "TEXT_ELEMENT",
"props": {
"nodeValue": "bar",
"children": [ ]
}
}
]
}
},
{
"type": "b",
"props": {
"children": [ ]
}
}
]
}
}

JSX

实际上我们在使用 react 开发的过程中,并不会这样创建组件:

const element = myReact.createElement(
"div",
{ id: "foo" },
myReact.createElement("a", null, "bar"),
myReact.createElement("b")
)

而是通过 JSX 语法,代码如下:

const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)

在 myReact 中,可以通过添加注释的形式,告诉 babel 转译我们指定的函数,来使用 JSX 语法,代码如下:

/** @jsx myReact.createElement */
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)

本例完整源码见:reactDemo4

2. render​

render 函数帮助我们将 element 添加至真实节点中。

将分为以下步骤实现:

  1. 创建 element.type 类型的 dom 节点,并添加至容器中;
/**
* 将虚拟 DOM 添加至真实 DOM
* @param {element} 虚拟 DOM
* @param {container} 真实 DOM
*/
function render (element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
  1. 将 element.children 都添加至 dom 节点中;
element.props.children.forEach(child => 
render(child, dom)
)
  1. 对文本节点进行特殊处理;
const dom = element.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(element.type)
  1. 将 element 的 props 属性添加至 dom;
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})

以上我们实现了将 JSX 渲染到真实 DOM 的功能,接下来试一下,代码如下:

const myReact = {
createElement,
render
}
/** @jsx myReact.createElement */
const element = (
<div id='foo'>
<a>bar</a>
<b></b>
</div>
)

myReact.render(element, document.getElementById('container'))

本例完整源码见:reactDemo5

结果如图,成功输出:

image.png

3. 可中断渲染(requestIdleCallback)​

再来看看上面写的 render 方法中关于子节点的处理,代码如下:

/**
* 将虚拟 DOM 添加至真实 DOM
* @param {element} 虚拟 DOM
* @param {container} 真实 DOM
*/
function render (element, container) {
// 省略
// 遍历所有子节点,并进行渲染
element.props.children.forEach(child =>
render(child, dom)
)
// 省略
}

这个递归调用是有问题的,一旦开始渲染,就会将所有节点及其子节点全部渲染完成这个进程才会结束。

当 dom tree 很大的情况下,在渲染过程中,页面上是卡住的状态,无法进行用户输入等交互操作。

可分为以下步骤解决上述问题:

  1. 允许中断渲染工作,如果有优先级更高的工作插入,则暂时中断浏览器渲染,待完成该工作后,恢复浏览器渲染;
  2. 将渲染工作进行分解,分解成一个个小单元;

使用 requestIdleCallback 来解决允许中断渲染工作的问题。

window.requestIdleCallback 将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

window.requestIdleCallback 详细介绍可查看文档:文档

代码如下:

// 下一个工作单元
let nextUnitOfWork = null
/**
* workLoop 工作循环函数
* @param {deadline} 截止时间
*/
function workLoop(deadline) {
// 是否应该停止工作循环函数
let shouldYield = false

// 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)

// 如果截止时间快到了,停止工作循环函数
shouldYield = deadline.timeRemaining() < 1
}

// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)

// 执行单元事件,并返回下一个单元事件
function performUnitOfWork(nextUnitOfWork) {
// TODO
}

performUnitOfWork 是用来执行单元事件,并返回下一个单元事件的,具体实现将在下文介绍。

4. Fiber​

上文介绍了通过 requestIdleCallback 让浏览器在空闲时间渲染工作单元,避免渲染过久导致页面卡顿的问题。

注:实际上 requestIdleCallback 功能并不稳定,不建议用于生产环境,本例仅用于模拟 React 的思路,React 本身并不是通过 requestIdleCallback 来实现让浏览器在空闲时间渲染工作单元的。

另一方面,为了让渲染工作可以分离成一个个小单元,React 设计了 fiber。

每一个 element 都是一个 fiber 结构,每一个 fiber 都是一个渲染工作单元。

所以 fiber 既是一种数据结构,也是一个工作单元。

下文将通过简单的示例对 fiber 进行介绍。

假设需要渲染这样一个 element 树:

myReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)

生成的 fiber tree 如图:

橙色代表子节点,黄色代表父节点,蓝色代表兄弟节点。

image.png

每个 fiber 都有一个链接指向它的第一个子节点、下一个兄弟节点和它的父节点。这种数据结构可以让我们更方便的查找下一个工作单元。

上图的箭头也表明了 fiber 的渲染过程,渲染过程详细描述如下:

  1. 从 root 开始,找到第一个子节点 div;
  2. 找到 div 的第一个子节点 h1;
  3. 找到 h1 的第一个子节点 p;
  4. 找 p 的第一个子节点,如无子节点,则找下一个兄弟节点,找到 p 的兄弟节点 a;
  5. 找 a 的第一个子节点,如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点,找到 a 的 父节点的兄弟节点 h2;
  6. 找 h2 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 div 的兄弟节点,也找不到,继续找 div 的父节点的兄弟节点,找到 root;
  7. 第 6 步已经找到了 root 节点,渲染已全部完成。

下面将渲染过程用代码实现。

  1. 将 render 中创建 DOM 节点的部分抽离为 creactDOM 函数;
/**
* createDom 创建 DOM 节点
* @param {fiber} fiber 节点
* @return {dom} dom 节点
*/
function createDom (fiber) {
// 如果是文本类型,创建空的文本节点,如果不是文本类型,按 type 类型创建节点
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)

// isProperty 表示不是 children 的属性
const isProperty = key => key !== "children"

// 遍历 props,为 dom 添加属性
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})

// 返回 dom
return dom
}
  1. 在 render 中设置第一个工作单元为 fiber 根节点;

fiber 根节点仅包含 children 属性,值为参数 fiber。

// 下一个工作单元
let nextUnitOfWork = null
/**
* 将 fiber 添加至真实 DOM
* @param {element} fiber
* @param {container} 真实 DOM
*/
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element]
}
}
}
  1. 通过 requestIdleCallback 在浏览器空闲时,渲染 fiber;
/**
* workLoop 工作循环函数
* @param {deadline} 截止时间
*/
function workLoop(deadline) {
// 是否应该停止工作循环函数
let shouldYield = false

// 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)

// 如果截止时间快到了,停止工作循环函数
shouldYield = deadline.timeRemaining() < 1
}

// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
}
// 通知浏览器,空闲时间应该执行 workLoop
requestIdleCallback(workLoop)
  1. 渲染 fiber 的函数 performUnitOfWork;
/**
* performUnitOfWork 处理工作单元
* @param {fiber} fiber
* @return {nextUnitOfWork} 下一个工作单元
*/
function performUnitOfWork(fiber) {
// TODO 添加 dom 节点
// TODO 新建 filber
// TODO 返回下一个工作单元(fiber)
}

4.1 添加 dom 节点

function performUnitOfWork(fiber) {
// 如果 fiber 没有 dom 节点,为它创建一个 dom 节点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}

// 如果 fiber 有父节点,将 fiber.dom 添加至父节点
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}

4.2 新建 filber

function performUnitOfWork(fiber) {
// ~~省略~~
// 子节点
const elements = fiber.props.children
// 索引
let index = 0
// 上一个兄弟节点
let prevSibling = null
// 遍历子节点
while (index < elements.length) {
const element = elements[index]

// 创建 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}

// 将第一个子节点设置为 fiber 的子节点
if (index === 0) {
fiber.child = newFiber
} else if (element) {
// 第一个之外的子节点设置为该节点的兄弟节点
prevSibling.sibling = newFiber
}

prevSibling = newFiber
index++
}
}

4.3 返回下一个工作单元(fiber)


function performUnitOfWork(fiber) {
// ~~省略~~
// 如果有子节点,返回子节点
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟节点,返回兄弟节点
if (nextFiber.sibling) {
return nextFiber.sibling
}

// 否则继续走 while 循环,直到找到 root。
nextFiber = nextFiber.parent
}
}

以上我们实现了将 fiber 渲染到页面的功能,且渲染过程是可中断的。

现在试一下,代码如下:

const element = (
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
)

myReact.render(element, document.getElementById('container'))

本例完整源码见:reactDemo7

如预期输出 dom,如图:

image.png

5. 渲染提交阶段​

由于渲染过程被我们做了可中断的,那么中断的时候,我们肯定不希望浏览器给用户展示的是渲染了一半的 UI。

对渲染提交阶段优化的处理如下:

  1. 把 performUnitOfWork 中关于把子节点添加至父节点的逻辑删除;
function performUnitOfWork(fiber) {
// 把这段删了
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
  1. 新增一个根节点变量,存储 fiber 根节点;
// 根节点
let wipRoot = null
function render (element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
}
// 下一个工作单元是根节点
nextUnitOfWork = wipRoot
}
  1. 当所有 fiber 都工作完成时,nextUnitOfWork 为 undefined,这时再渲染真实 DOM;
function workLoop (deadline) {
// 省略
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
// 省略
}
  1. 新增 commitRoot 函数,执行渲染真实 DOM 操作,递归将 fiber tree 渲染为真实 DOM;
// 全部工作单元完成后,将 fiber tree 渲染为真实 DOM;
function commitRoot () {
commitWork(wipRoot.child)
// 需要设置为 null,否则 workLoop 在浏览器空闲时不断的执行。
wipRoot = null
}
/**
* performUnitOfWork 处理工作单元
* @param {fiber} fiber
*/
function commitWork (fiber) {
if (!fiber) return
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// 渲染子节点
commitWork(fiber.child)
// 渲染兄弟节点
commitWork(fiber.sibling)
}

本例完整源码见:reactDemo8

源码运行结果如图:

image.png

6. 协调(diff 算法)​

当 element 有更新时,需要将更新前的 fiber tree 和更新后的 fiber tree 进行比较,得到比较结果后,仅对有变化的 fiber 对应的 dom 节点进行更新。

通过协调,减少对真实 DOM 的操作次数。

1. currentRoot​

新增 currentRoot 变量,保存根节点更新前的 fiber tree,为 fiber 新增 alternate 属性,保存 fiber 更新前的 fiber tree;

let currentRoot = null
function render (element, container) {
wipRoot = {
// 省略
alternate: currentRoot
}
}
function commitRoot () {
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}

2. performUnitOfWork​

将 performUnitOfWork 中关于新建 fiber 的逻辑,抽离到 reconcileChildren 函数;

/**
* 协调子节点
* @param {fiber} fiber
* @param {elements} fiber 的 子节点
*/
function reconcileChildren (fiber, elements) {
// 用于统计子节点的索引值
let index = 0
// 上一个兄弟节点
let prevSibling = null

// 遍历子节点
while (index < elements.length) {
const element = elements[index]

// 新建 fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}

// fiber的第一个子节点是它的子节点
if (index === 0) {
fiber.child = newFiber
} else if (element) {
// fiber 的其他子节点,是它第一个子节点的兄弟节点
prevSibling.sibling = newFiber
}

// 把新建的 newFiber 赋值给 prevSibling,这样就方便为 newFiber 添加兄弟节点了
prevSibling = newFiber

// 索引值 + 1
index++
}
}

3. reconcileChildren​

在 reconcileChildren 中对比新旧 fiber;

3.1 当新旧 fiber 类型相同时​

保留 dom,仅更新 props,设置 effectTag 为 UPDATE;

function reconcileChildren (wipFiber, elements) {
// ~~省略~~
// oldFiber 可以在 wipFiber.alternate 中找到
let oldFiber = wipFiber.alternate && wipFiber.alternate.child

while (index < elements.length || oldFiber != null) {
const element = elements[index]
let newFiber = null

// fiber 类型是否相同
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type

// 如果类型相同,仅更新 props
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
// ~~省略~~
}
// ~~省略~~
}

3.2 当新旧 fiber 类型不同,且有新元素时​

创建一个新的 dom 节点,设置 effectTag 为 PLACEMENT;

function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
// ~~省略~~
}

3.3 当新旧 fiber 类型不同,且有旧 fiber 时​

删除旧 fiber,设置 effectTag 为 DELETION;

function reconcileChildren (wipFiber, elements) {
// ~~省略~~
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// ~~省略~~
}

4. deletions​

新建 deletions 数组存储需删除的 fiber 节点,渲染 DOM 时,遍历 deletions 删除旧 fiber;

let deletions = null
function render (element, container) {
// 省略
// render 时,初始化 deletions 数组
deletions = []
}

// 渲染 DOM 时,遍历 deletions 删除旧 fiber
function commitRoot () {
deletions.forEach(commitWork)
}

5. commitWork​

在 commitWork 中对 fiber 的 effectTag 进行判断,并分别处理。

5.1 PLACEMENT​

当 fiber 的 effectTag 为 PLACEMENT 时,表示是新增 fiber,将该节点新增至父节点中。

if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}

5.2 DELETION​

当 fiber 的 effectTag 为 DELETION 时,表示是删除 fiber,将父节点的该节点删除。

else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}

5.3 UPDATE​

当 fiber 的 effectTag 为 UPDATE 时,表示是更新 fiber,更新 props 属性。

else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}

updateDom 函数根据不同的更新类型,对 props 属性进行更新。

const isProperty = key => key !== "children"

// 是否是新属性
const isNew = (prev, next) => key => prev[key] !== next[key]

// 是否是旧属性
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
// 删除旧属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})

// 更新新属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}

另外,为 updateDom 添加事件属性的更新、删除,便于追踪 fiber 事件的更新。

function updateDom(dom, prevProps, nextProps) {
// ~~省略~~
const isEvent = key => key.startsWith("on")
//删除旧的或者有变化的事件
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})

// 注册新事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
// ~~省略~~
}

替换 creactDOM 中设置 props 的逻辑。

function createDom (fiber) {
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type)
// 看这里鸭
updateDom(dom, {}, fiber.props)
return dom
}

新建一个包含输入表单项的例子,尝试更新 element,代码如下:

/** @jsx myReact.createElement */
const container = document.getElementById("container")

const updateValue = e => {
rerender(e.target.value)
}

const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
</div>
)
myReact.render(element, container)
}

rerender("World")

本例完整源码见:reactDemo9

输出结果如图:

12.gif

7. 函数式组件​

先来看一个简单的函数式组件示例:

myReact 还不支持函数式组件,下面代码运行会报错,这里仅用于比照函数式组件的常规使用方式。

/** @jsx myReact.createElement */
const container = document.getElementById("container")

function App (props) {
return (
<h1>hi~ {props.name}</h1>
)
}

const element = (
<App name='foo' />
)

myReact.render(element, container)

函数式组件和 html 标签组件相比,有以下两点不同:

  • 函数组件的 fiber 没有 dom 节点;
  • 函数组件的 children 需要运行函数后得到;

通过下列步骤实现函数组件:

  1. 修改 performUnitOfWork,根据 fiber 类型,执行 fiber 工作单元;
function performUnitOfWork(fiber) {
// 是否是函数类型组件
const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
// 如果是函数组件,执行 updateFunctionComponent 函数
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
// 如果不是函数组件,执行 updateHostComponent 函数
updateHostComponent(fiber)
}
// 省略
}
  1. 定义 updateHostComponent 函数,执行非函数组件;

非函数式组件可直接将 fiber.props.children 作为参数传递。

function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
  1. 定义 updateFunctionComponent 函数,执行函数组件;

函数组件需要运行来获得 fiber.children。

function updateFunctionComponent(fiber) {
// fiber.type 就是函数组件本身,fiber.props 就是函数组件的参数
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
  1. 修改 commitWork 函数,兼容没有 dom 节点的 fiber;

4.1 修改 domParent 的获取逻辑,通过 while 循环不断向上寻找,直到找到有 dom 节点的父 fiber;

function commitWork (fiber) {
// 省略
let domParentFiber = fiber.parent
// 如果 fiber.parent 没有 dom 节点,则继续找 fiber.parent.parent.dom,直到有 dom 节点。
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
// 省略
}

4.2 修改删除节点的逻辑,当删除节点时,需要不断向下寻找,直到找到有 dom 节点的子 fiber;

function commitWork (fiber) {
// 省略
// 如果 fiber 的更新类型是删除,执行 commitDeletion
else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber.dom, domParent)
}
// 省略
}

// 删除节点
function commitDeletion (fiber, domParent) {
// 如果该 fiber 有 dom 节点,直接删除
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
// 如果该 fiber 没有 dom 节点,则继续找它的子节点进行删除
commitDeletion(fiber.child, domParent)
}
}

下面试一下上面的例子,代码如下:

/** @jsx myReact.createElement */
const container = document.getElementById("container")

function App (props) {
return (
<h1>hi~ {props.name}</h1>
)
}

const element = (
<App name='foo' />
)

myReact.render(element, container)

本例完整源码见:reactDemo10

运行结果如图:

image.png

8. hooks​

下面继续为 myReact 添加管理状态的功能,期望是函数组件拥有自己的状态,且可以获取、更新状态。

一个拥有计数功能的函数组件如下:

function Counter() {
const [state, setState] = myReact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />

已知需要一个 useState 方法用来获取、更新状态。

这里再重申一下,渲染函数组件的前提是,执行该函数组件,因此,上述 Counter 想要更新计数,就会在每次更新都执行一次 Counter 函数。

通过以下步骤实现:

  1. 新增全局变量 wipFiber;
// 当前工作单元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// 当前工作单元 fiber 的 hook
wipFiber.hook = []
// 省略
}
  1. 新增 useState 函数;
// initial 表示初始参数,在本例中,initial=1
function useState (initial) {
// 是否有旧钩子,旧钩子存储了上一次更新的 hook
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hook

// 初始化钩子,钩子的状态是旧钩子的状态或者初始状态
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}

// 从旧的钩子队列中获取所有动作,然后将它们一一应用到新的钩子状态
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})

// 设置钩子状态
const setState = action => {
// 将动作添加至钩子队列
hook.queue.push(action)
// 更新渲染
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}

// 把钩子添加至工作单元
wipFiber.hook = hook

// 返回钩子的状态和设置钩子的函数
return [hook.state, setState]
}

下面运行一下计数组件,代码如下:

function Counter() {
const [state, setState] = myReact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />

本例完整源码见:reactDemo11

运行结果如图: 123.gif

本章节简单实现了 myReact 的 hooks 功能。

撒花完结,react 还有很多实现值得我们去学习和研究,希望有下期,和大家一起手写 react 的更多功能。

总结

本文参考 pomb.us 进行学习,实现了包括虚拟 DOM、Fiber、Diff 算法、函数式组件、hooks 等功能的自定义 React。

在实现过程中小编对 React 的基本术语及实现思路有了大概的掌握,pomb.us 是非常适合初学者的学习资料,可以直接通过 pomb.us 进行学习,也推荐跟着本文一步步实现 React 的常见功能。

本文源码: github源码 。

建议跟着一步步敲,进行实操练习。

希望能对你有所帮助,感谢阅读~

别忘了点个赞鼓励一下我哦,笔芯❤️

参考资料