My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

复杂状态管理

发表于 2021-11-22

React 的开发其实就是复杂应用程序状态的管理和开发

复杂状态管理的两个原则

原则一:保证状态最小化

在定义一个新的状态之前,都要再三拷问自己:
这个状态是必须的吗?是否能通过计算得到呢?
在得到肯定的回答后,再去定义新的状态,
就能避免大部分多余的状态定义问题,也就能在简化状态管理的同时,保证状态的一致性

比如根据关键字搜索的场景,搜索结果可以根据关键和原始数据计算得到,可以使用useMemo保存过滤结果,
而不是再为过滤结果声明一个state

原则二:避免中间状态,确保唯一数据源

比如在需要根据URL查询数据的场景,查询条件在url,同时页面有配置条件的交互
那么直接将 URL 作为唯一的数据来源,状态的读取和修改都是对 URL 直接进行操作,
而不是通过一个中间的状态保存条件
(URL 变化时,同步查询关键字到 State;State 变化时,同步查询关键字到输入框; 用户在输入框输入的时候,同步关键字到 URL 和 State),
这样就简化了状态的管理,保证了状态的一致性

My Little World

redux的基本使用

发表于 2021-11-17

Redux 的三个基本概念

Redux 引入的概念其实并不多,主要就是三个:State、Action 和 Reducer。

其中 State 即 Store,一般就是一个纯 JavaScript Object。
Action 也是一个 Object,用于描述发生的动作。
而 Reducer 则是一个函数,接收 Action 和 State 并作为参数,通过计算得到新的 Store。

如何在 React 中使用 Redux

如何建立 Redux 和 React 的联系

主要是两点:

  1. React 组件能够在依赖的 Store 的数据发生变化时,重新 Render;
  2. 在 React 组件中,能够在某些时机去 dispatch 一个 action,从而触发 Store 的更新

答案:
借助react-redux 这样一个工具库,工具库的作用就是建立一个桥梁,让 React 和 Redux 实现互通。

在 react-redux 的实现中,为了确保需要绑定的组件能够访问到全局唯一的 Redux Store,
利用了 React 的 Context 机制去存放 Store 的信息。
通常我们会将这个 Context 作为整个 React 应用程序的根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)

Hooks 的本质就是提供了让 React 组件能够绑定到某个可变的数据源的能力。
在这里,当 Hooks 用到 Redux 时可变的对象就是 Store,而 useSelector 则让一个组件能够在 Store 的某些数据发生变化时重新 render.

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


import React from 'react'
import { useSelector, useDispatch } from 'react-redux'

export function Counter() {
// 从 state 中获取当前的计数值
const count = useSelector(state => state.value)

// 获得当前 store 的 dispatch 方法
const dispatch = useDispatch()

// 在按钮的 click 时间中去分发 action 来修改 store
return (
<div>
<button
onClick={() => dispatch({ type: 'counter/incremented' })}
>+</button>
<span>{count}</span>
<button
onClick={() => dispatch({ type: 'counter/decremented' })}
>-</button>
</div>
)
}

使用 Redux 处理异步逻辑

借助中间件机制

middleware 可以让你提供一个拦截器在 reducer 处理 action 之前被调用。
在这个拦截器中,你可以自由处理获得的 action。
无论是把这个 action 直接传递到 reducer,或者构建新的 action 发送到 reducer,都是可以的

Middleware 正是在 Action 真正到达 Reducer 之前提供的一个额外处理 Action 的机会

Redux 提供了 redux-thunk 这样一个中间件,它如果发现接受到的 action 是一个函数,那么就不会传递给 Reducer,
而是执行这个函数,并把 dispatch 作为参数传给这个函数,从而在这个函数中你可以自由决定何时,如何发送 Action。

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
// 在创建 Redux Store 时指定了 redux-thunk 这个中间件
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './reducer'

const composedEnhancer = applyMiddleware(thunkMiddleware)
const store = createStore(rootReducer, composedEnhancer)

//dispatch action 时就可以 dispatch 一个函数用于来发送请求

function fetchData() {
return dispatch => {
dispatch({ type: 'FETCH_DATA_BEGIN' });
fetch('/some-url').then(res => {
dispatch({ type: 'FETCH_DATA_SUCCESS', data: res });
}).catch(err => {
dispatch({ type: 'FETCH_DATA_FAILURE', error: err });
})
}
}

// dispatch action 时就可以 dispatch 一个函数用于来发送请求
import fetchData from './fetchData';

function DataList() {
const dispatch = useDispatch();
// dispatch 了一个函数由 redux-thunk 中间件去执行
dispatch(fetchData());
}

通过这种方式,我们就实现了异步请求逻辑的重用。那么这一套结合 redux-thunk 中间件的机制,我们就称之为异步 Action.

dispatch时可以传入其他参数

1
2
3
4
5
6
7
8
9
10
11
12

function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/incremented': return {value: state.value + action.payload}
case 'counter/decremented': return {value: state.value - action.payload}
default: return state
}
}
const incrementAction = { type: 'counter/incremented', payload: 10 };
store.dispatch(incrementAction); // 计数器加 10
const decrementAction = { type: 'counter/decremented', payload: 10 };
store.dispatch(decrementAction); // 计数器减 10
My Little World

hooks 的典型应用场景

发表于 2021-11-16

自定义hooks

声明一个名字以 use 开头的函数, 并在函数中用到其他hooks。
Hooks 和普通函数在语义上的区别,就在于函数中有没有用到其它 Hooks(自定义或者内置,能够让组件刷新,或者去产生副作用),没有用到就是普通函数。

典型的四个使用场景

  1. 抽取业务逻辑;
  2. 封装通用逻辑;
  3. 监听浏览器状态;
  4. 拆分复杂组件.

抽取业务逻辑

抽取具体业务逻辑到hooks中,暴露接口在组件中调用
一方面能让这个逻辑得到重用,另外一方面也能让代码更加语义化,并且易于理解和维护

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
// 实现计数器业务逻辑的拆分和重用
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 };
}

// 应用
import React from 'react';

function Counter() {
// 调用自定义 Hook
const { count, increment, decrement, reset } = useCounter();

// 渲染 UI
return (
<div>
<button onClick={decrement}> - </button>
<p>{count}</p>
<button onClick={increment}> + </button>
<button onClick={reset}> reset </button>
</div>
);
}

封装通用逻辑:useAsync

在每个需要异步请求的组件中,其实都需要重复相同的逻辑。
事实上,在处理这类请求的时候,模式都是类似的,通常都会遵循下面步骤:

  1. 创建 data,loading,error 这 3 个 state;
  2. 请求发出后,设置 loading state 为 true;
  3. 请求成功后,将返回的数据放到某个 state 中,并将 loading state 设为 false;
  4. 请求失败后,设置 error state 为 true,并将 loading state 设为 false。

最后,基于 data、loading、error 这 3 个 state 的数据,
UI 就可以正确地显示数据,或者 loading、error 这些反馈给用户了。
所以,通过创建一个自定义 Hook,可以很好地将这样的逻辑提取出来,成为一个可重用的模块。

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


import { useState } from 'react';
// asyncFunction 真正发出请求的函数
const useAsync = (asyncFunction) => {
// 设置三个异步逻辑相关的 state
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 定义一个 callback 用于执行异步逻辑 相当于声明一个请求的函数,在需要请求数据的情况下调用
const execute = useCallback(() => {
// 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
setLoading(true);
setData(null);
setError(null);
return asyncFunction()
.then((response) => {
// 请求成功时,将数据写进 state,设置 loading 为 false
setData(response);
setLoading(false);
})
.catch((error) => {
// 请求失败时,设置 loading 为 false,并设置错误状态
setError(error);
setLoading(false);
});
}, [asyncFunction]);

return { execute, loading, data, error };
};


// 应用

import React from "react";
import useAsync from './useAsync';

export default function UserList() {
// 通过 useAsync 这个函数,只需要提供异步逻辑的实现
const {
execute: fetchUsers, // fetchUsers可以在需要请求的地方去使用
data: users, // 返回的数据可以直接拿来渲染ui
loading,
error,
} = useAsync(async () => {
const res = await fetch("https://reqres.in/api/users/");
const json = await res.json();
return json.data;
});

return (
// 根据状态渲染 UI...
);
}

利用了 Hooks 能够管理 React 组件状态的能力,将一个组件中的某一部分状态独立出来,从而实现了通用逻辑的重用。

监听浏览器状态:useScroll

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

import { useState, useEffect } from 'react';

// 获取横向,纵向滚动条位置
const getPosition = () => {
return {
x: document.body.scrollLeft,
y: document.body.scrollTop,
};
};
const useScroll = () => {
// 定一个 position 这个 state 保存滚动条位置
const [position, setPosition] = useState(getPosition());
useEffect(() => {
const handler = () => {
setPosition(getPosition(document));
};
// 监听 scroll 事件,更新滚动条位置
document.addEventListener("scroll", handler);
return () => {
// 组件销毁时,取消事件监听
document.removeEventListener("scroll", handler);
};
}, []);
return position;
};

// 应用


import React, { useCallback } from 'react';
import useScroll from './useScroll';

function ScrollTop() {
const { y } = useScroll();

const goTop = useCallback(() => {
document.body.scrollTop = 0;
}, []);

const style = {
position: "fixed",
right: "10px",
bottom: "10px",
};
// 当滚动条位置纵向超过 300 时,显示返回顶部按钮
if (y > 300) {
return (
<button onClick={goTop} style={style}>
Back to Top
</button>
);
}
// 否则不 render 任何 UI
return null;
}

拆分复杂组件

拆分逻辑的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。

把 Hooks 就看成普通的函数,能隔离的尽量去做隔离

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115


import React, { useEffect, useCallback, useMemo, useState } from "react";
import { Select, Table } from "antd";
import _ from "lodash";
import useAsync from "./useAsync";

const endpoint = "https://myserver.com/api/";
const useArticles = () => {
// 使用上面创建的 useAsync 获取文章列表
const { execute, data, loading, error } = useAsync(
useCallback(async () => {
const res = await fetch(`${endpoint}/posts`);
return await res.json();
}, []),
);
// 执行异步调用
useEffect(() => execute(), [execute]);
// 返回语义化的数据结构
return {
articles: data,
articlesLoading: loading,
articlesError: error,
};
};
const useCategories = () => {
// 使用上面创建的 useAsync 获取分类列表
const { execute, data, loading, error } = useAsync(
useCallback(async () => {
const res = await fetch(`${endpoint}/categories`);
return await res.json();
}, []),
);
// 执行异步调用
useEffect(() => execute(), [execute]);

// 返回语义化的数据结构
return {
categories: data,
categoriesLoading: loading,
categoriesError: error,
};
};
const useCombinedArticles = (articles, categories) => {
// 将文章数据和分类数据组合到一起
return useMemo(() => {
// 如果没有文章或者分类数据则返回 null
if (!articles || !categories) return null;
return articles.map((article) => {
return {
...article,
category: categories.find(
(c) => String(c.id) === String(article.categoryId),
),
};
});
}, [articles, categories]);
};
const useFilteredArticles = (articles, selectedCategory) => {
// 实现按照分类过滤
return useMemo(() => {
if (!articles) return null;
if (!selectedCategory) return articles;
return articles.filter((article) => {
console.log("filter: ", article.categoryId, selectedCategory);
return String(article?.category?.name) === String(selectedCategory);
});
}, [articles, selectedCategory]);
};

const columns = [
{ dataIndex: "title", title: "Title" },
{ dataIndex: ["category", "name"], title: "Category" },
];

export default function BlogList() {
const [selectedCategory, setSelectedCategory] = useState(null);
// 获取文章列表
const { articles, articlesError } = useArticles();
// 获取分类列表
const { categories, categoriesError } = useCategories();
// 组合数据
const combined = useCombinedArticles(articles, categories);
// 实现过滤
const result = useFilteredArticles(combined, selectedCategory);

// 分类下拉框选项用于过滤
const options = useMemo(() => {
const arr = _.uniqBy(categories, (c) => c.name).map((c) => ({
value: c.name,
label: c.name,
}));
arr.unshift({ value: null, label: "All" });
return arr;
}, [categories]);

// 如果出错,简单返回 Failed
if (articlesError || categoriesError) return "Failed";

// 如果没有结果,说明正在加载
if (!result) return "Loading...";

return (
<div>
<Select
value={selectedCategory}
onChange={(value) => setSelectedCategory(value)}
options={options}
style={{ width: "200px" }}
placeholder="Select a category"
/>
<Table dataSource={result} columns={columns} />
</div>
);
}
My Little World

hooks 与 生命周期

发表于 2021-11-15

单次执行函数

利用 useRef 这个 Hook,我们可以实现一个 useSingleton 这样的一次性执行某段代码的自定义 Hook

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


import { useRef } from 'react';

// 创建一个自定义 Hook 用于执行一次性代码
function useSingleton(callback) {
// 用一个 called ref 标记 callback 是否执行过
const called = useRef(false);
// 如果已经执行过,则直接返回
if (called.current) return;
// 第一次调用时直接执行
callBack();
// 设置标记为已执行过
called.current = true;
}



import useSingleton from './useSingleton';

const MyComp = () => {
// 使用自定义 Hook
useSingleton(() => {
console.log('这段代码只执行一次');
});

return (
<div>My Component</div>
);
};

useEffect与class生命周期方法联系

1
2
3
4
5
6
7
8
9

useEffect(() => {
// componentDidMount + componentDidUpdate
console.log('这里基本等价于 componentDidMount + componentDidUpdate');
return () => {
// componentWillUnmount
console.log('这里基本等价于 componentWillUnmount');
}
}, [deps])

这个写法并没有完全等价于传统的这几个生命周期方法。主要有两个原因。

一方面,useEffect(callback) 这个 Hook 接收的 callback,只有在依赖项变化时才被执行。
而传统的 componentDidUpdate 则一定会执行。
这样来看,Hook 的机制其实更具有语义化,
因为过去在 componentDidUpdate 中,我们通常都需要手动判断某个状态是否发生变化,然后再执行特定的逻辑。

另一方面,
callback 返回的函数(一般用于清理工作)在下一次依赖项发生变化以及组件销毁之前执行,
而传统的 componentWillUnmount 只在组件销毁时才会执行。

My Little World

react 自带的hooks

发表于 2021-11-12

useCallback:缓存回调函数

在 React 函数组件中,每一次 UI 的变化,都是通过重新执行整个函数来完成的

如果组件中依赖的处理函数没有被useCallback做缓存处理,那么每次重新执行时都会被重新创建
那么依赖处理函数的组件就会因为处理函数更新进行重现渲染

为避免频繁的重新渲染组件,useCallback的作用就是将函数缓存起来,
只有当函数逻辑中依赖的状态发生变化才会去重新生成,然后引起组件更新,
避免了依赖没有变化时的无效更新

useCallback 可以减少不必要的渲染,主要体现在将回调函数作为属性传给某个组件。
如果每次都不一样就会造成组件的重新渲染。
但是如果确定子组件多次渲染也没有太大问题,特别是原生的组件,比如 button,
那么不用 useCallback 也问题不大。所以这和子组件的实现相关,和函数是否轻量无关。

useMemo:缓存计算的结果

同处理函数类似,如果组件中用到的数据A通过其他状态值计算得到,
在依赖值没有发生变化情况下,其实就不用重新计算,
即
如果某个数据是通过其它数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算
因此当组件因为其他原因刷新,进而重新执行函数,引起的重新计算是没有必要的

可以实现这个功能的hooks就是useMemo,useMemo就是只有在依赖的状态值发生变化时才会去重新执行计算的过程
避免不必要的重新计算过程
同时,对于依赖数据A的组件来说,没有重新计算产生的新值,可以在一定程度上避免子组件重复渲染。

如果将函数也看做一个状态值/变量的话,其实,useMemo可以实现useCallback的功能

1
2
3
4
5
6
const myEventHandler = useMemo(() => {
// 返回一个函数作为缓存结果
return () => {
// 在这里进行事件处理
}
}, [dep1, dep2]);

二者的本质就是
建立了一个绑定某个结果到依赖数据的关系。只有当依赖变了,这个结果才需要被重新得到。

useRef:在多次渲染之间共享数据

可以把 useRef 看作是在函数组件之外创建的一个容器空间
在这个容器上,我们可以通过唯一的 current 属设置一个值,从而在函数组件的多次渲染之间共享这个值

使用 useRef 保存的数据一般是和 UI 的渲染无关的,因此当 ref 的值发生变化时,是不会触发组件的重新渲染的,
这也是 useRef 区别于 useState 的地方。
比如保存定时器句柄,在回调函数中关掉定时器,然后将ref.current 设置为null, 这个句柄没有在组件中被用到,所以不会引起重新渲染。
但如果用useState去保存句柄,将定时器关掉时,同时将state设置为null,即使没有被组件用到,也会引起重新组件渲染。

useRef 还有一个重要的功能,就是保存某个 DOM 节点的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14

function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

可以看到 ref 这个属性提供了获得 DOM 节点的能力,并利用 useRef 保存了这个节点的应用

useContext:定义全局状态

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

const MyContext = React.createContext(initialValue);

const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// 创建一个 Theme 的 Context

const ThemeContext = React.createContext(themes.light);
function App() {
// 整个应用使用 ThemeContext.Provider 作为根组件
return (
// 使用 themes.dark 作为当前 Context
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}

// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}

// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{
background: theme.background,
color: theme.foreground
}}>
I am styled by theme context!
</button>
);
}

Context 提供了一个方便在多个组件之间共享数据的机制。
不过需要注意的是,它的灵活性也是一柄双刃剑。
你或许已经发现,Context 相当于提供了一个定义 React 世界中全局变量的机制,
而全局变量则意味着两点:

  1. 会让调试变得困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
  2. 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。

需要再三强调的是,Context 更多的是提供了一个强大的机制, 让 React 应用具备定义全局的响应式数据的能力。

My Little World

如何保存组件状态和使用生命周期

发表于 2021-11-11

那么什么样的值应该保存在 state 中呢?

在一个函数组件的多次渲染之间,这个 state 是共享的。
这是日常开发中需要经常思考的问题。通常来说,我们要遵循的一个原则就是:state 中永远不要保存可以通过计算得到的值。
比如说:

  1. 从 props 传递过来的值。有时候 props 传递过来的值无法直接使用,而是要通过一定的计算后再在 UI 上展示,比如说排序。
    那么我们要做的就是每次用的时候,都重新排序一下,或者利用某些 cache 机制,而不是将结果直接放到 state 里。
  2. 从 URL 中读到的值。比如有时需要读取 URL 中的参数,把它作为组件的一部分状态。那么我们可以在每次需要用的时候从 URL 中读取,而不是读出来直接放到 state 里。
  3. 从 cookie、localStorage 中读取的值。通常来说,也是每次要用的时候直接去读取,而不是读出来后放到 state 里。

useEffect执行

useEffect 让我们能够在下面四种时机去执行一个回调函数产生副作用:

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

定义依赖项注意点

那么在定义依赖项时,我们需要注意以下三点:

  1. 依赖项中定义的变量一定是会在回调函数中用到的,否则声明依赖项其实是没有意义的。
  2. 依赖项一般是一个常量数组,而不是一个变量。因为一般在创建 callback 的时候,你其实非常清楚其中要用到哪些依赖项了。
  3. React 会使用浅比较来对比依赖项是否发生了变化,所以要特别注意数组或者对象类型。
    如果你是每次创建一个新对象,即使和之前的值是等价的,也会被认为是依赖项发生了变化。这是一个刚开始使用 Hooks 时很容易导致 Bug 的地方。

Hooks 的使用规则

只能在函数组件的顶级作用域使用

第一,所有 Hook 必须要被执行到。第二,必须按顺序执行。

所谓顶层作用域,就是 Hooks 不能在循环、条件判断或者嵌套函数内执行,而必须是在顶层。
同时 Hooks 在组件的多次渲染之间,必须按顺序被执行。
因为在 React 组件内部,其实是维护了一个对应组件的固定 Hooks 执行列表的,以便在多次渲染之间保持 Hooks 的状态,并做对比。
也不能在return语句后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

function MyComp() {
const [count, setCount] = useState(0);
if (count > 10) {
// 错误:不能将 Hook 用在条件判断里
useEffect(() => {
// ...
}, [count])
}

// 这里可能提前返回组件渲染结果,后面就不能再用 Hooks 了
if (count === 0) {
return 'No content';
}

// 错误:不能将 Hook 放在可能的 return 之后
const [loading, setLoading] = useState(false);

//...
return <div>{count}</div>
}

Hooks 只能在函数组件或者其它 Hooks 中使用

如果一定要在 Class 组件中使用,那应该如何做呢?
其实有一个通用的机制,那就是利用高阶组件的模式,将 Hooks 封装成高阶组件,从而让类组件使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
a.
import React from 'react';
import { useWindowSize } from '../hooks/useWindowSize';

export const withWindowSize = (Comp) => {
return props => {
const windowSize = useWindowSize();
return <Comp windowSize={windowSize} {...props} />;
};
};

b.
import React from 'react';
import { withWindowSize } from './withWindowSize';

class MyComp {
render() {
const { windowSize } = this.props;
// ...
}
}

// 通过 withWindowSize 高阶组件给 MyComp 添加 windowSize 属性
export default withWindowSize(MyComp);

小结
hooks使用时应包括这么三点:

  1. 在 useEffect 的回调函数中使用的变量,都必须在依赖项中声明;
  2. Hooks 不能出现在条件语句或者循环中,也不能出现在 return 之后;
  3. Hooks 只能在函数组件或者自定义 Hooks 中使用。
My Little World

hook 产生原因

发表于 2021-11-10

数据与视图间关系

在过去,我们需要处理当 Model 变化时,DOM 节点应该如何变化的细节问题。
而现在,我们只需要通过 JSX,根据 Model 的数据用声明的方式去描述 UI 的最终展现就可以了,
因为 React 会帮助你处理所有 DOM 变化的细节。而且,当 Model 中的状态发生变化时,UI 会自动变化,即所谓的数据绑定。

所以呢,我们可以把 UI 的展现看成一个函数的执行过程。
其中,Model 是输入参数,函数的执行结果是 DOM 树,也就是 View。
而 React 要保证的,就是每当 Model 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。

class组件不合适的原因

一方面,React 组件之间是不会互相继承的。比如说,你不会创建一个 Button 组件,然后再创建一个 DropdownButton 来继承 Button。
所以说,React 中其实是没有利用到 Class 的继承特性的。

另一方面,因为所有 UI 都是由状态驱动的,因此很少会在外部去调用一个类实例(即组件)的方法。
要知道,组件的所有方法都是在内部调用,或者作为生命周期方法被自动调用的。

所以你看,这两个 Class 最重要的特性其实都没有用到。

函数组件的缺陷/hooks产生的背景

只是当时有一个局限是,函数组件无法存在内部状态,必须是纯函数,而且也无法提供完整的生命周期机制。这就极大限制了函数组件的大规模使用。
那么我们自然就知道了,Class 作为 React 组件的载体,也许并不是最适合,
反而函数是更适合去描述 State => View 这样的一个映射,但是函数组件又没有 State ,也没有生命周期方法。
以此来看,我们应该如何去改进呢?

hooks的产生

简单想一下,函数和对象不同,并没有一个实例的对象能够在多次执行之间保存状态,那势必需要一个函数之外的空间来保存这个状态,而且要能够检测其变化,从而能够触发函数组件的重新渲染。
再进一步想,那我们是不是就是需要这样一个机制,能够把一个外部的数据绑定到函数的执行。当数据变化时,函数能够自动重新执行。
这样的话,任何会影响 UI 展现的外部数据,都可以通过这个机制绑定到 React 的函数组件。
在 React 中,这个机制就是 Hooks。
所以我们现在也能够理解这个机制为什么叫 Hooks 了。
顾名思义,Hook 就是“钩子”的意思。

在 React 中,Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

对于函数组件,这个结果是最终的 DOM 树;对于 useCallback、useMemo 这样与缓存相关的组件,则是在依赖项发生变化时去更新缓存。

hooks 好处一 逻辑复用

Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:逻辑的复用

class高阶组件缺陷

更为糟糕的是,高阶组件几乎是 Class 组件中实现代码逻辑复用的唯一方式,其缺点其实比较显然:
代码难理解,不直观,很多人甚至宁愿重复代码,也不愿用高阶组件;
会增加很多额外的组件节点。每一个高阶组件都会多一层节点,这就会给调试带来很大的负担。

使用hooks封装好处

通过 Hooks 的方式进行封装,从而将依赖变成一个可绑定的数据源。
这样当窗口大小发生变化时,使用这个 Hook 的组件就都会重新渲染。而且代码也更加简洁和直观,不会产生额外的组件节点。

hooks好处二 有助于关注分离

在 Class 组件中,代码是从技术角度组织在一起的,例如在 componentDidMount 中都去做一些初始化的事情。
而在函数组件中,代码是从业务角度组织在一起的,相关代码能够出现在集中的地方,从而更容易理解和维护。

My Little World

《js 语言精髓与编程实践》

发表于 2021-10-18

整本书看下来,稍微有些晦涩,很多理论概念被换了一种说法阐释,开阔思路,故整理笔记。

笔记按照篇章整理,没有固定逻辑。

第二章

1.语法关键字对语义逻辑的绑定结果,是对作用域的限定;
变量对位置性质的绑定结果,则是对变量生命周期的限定。

2.函数的6种声明标识符的方法(var,const,let,fucntion,class,import),他们声明的标识符在语法分析阶段就可以被识别。

3.关于值传递与引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var str = 'abcded';
var obj = new String(str);

function newToString() {
return 'hello world'
}

function func(val){
val.toString = newToString;
}

func(str);
console.log(str); // abcded

func(obj);
console.log(String(obj)) // hello world

func函数传入字符串时,是值传递,根据结果可知,不能直接修改其属性方法
传入obj对象时,是引用传递,传入后被修改了属性值,故打印出了hello world.

所以,到底是值传递还是引用,会根据传递的数据类型决定。

4.等值检测(==)运算规则

值类型与引用类型比较,将引用转换成值类型数据相同的数据,进行数据等值比较

两个值类型比较,将二者转换成相同数据类型,在进行数据等值比较

两个引用类型比较,仅比较引用地址

跟数字,布尔,字符串比较时
有任何一个是数字时,将另外一个转数字
有任何一个是布尔值时,转换成数字后比较
有任何一个是对象(或函数),调用valueOf方法来将其转换成值数据进行比较
按特定规则返回比较结果,如,undefined和null会返回true

等值检测一些特例

1
2
3
4
5
6
7
8
9
10
1. NaN不等于自身

NaN == (===/ != /!==) NaN // true

2. 符号可以转为true,但不等值于true

Boolean(Symbol()) ,!Symbol(), Symbol() ==(===) true // true false false

3. 即使字面量相同的引用类型,也是不严格相等的
{} === {}, /./ === /./, function() {} === function() {} // false false false

5.序列检测规则

两个值类型比较
直接比较数据在序列中的大小

值类型与引用类型进行比较
将引用类型转换成与值类型数据相同的的数据,再进行“序列大小”比较

两个引用类型进行比较
无意义,总是返回false

6.赋值
字符串赋值原理是复制地址,所以对于字符串的操作有了以下三种限制
a.不能直接修改字符串中的字符
b.字符串连接运算必然导致写复制,这将产生新的字符串
c.不能改变字符串的长度

7.函数隐式调用的几种情况
a.es6之后的模板处理函数
b.将函数用作属性读取器时,属性存取操作将隐世调用该函数,==>getter,setter
c.使用bind方法将原函数绑定为目标函数时,调用目标函数,就是隐世调用原函数
d.当使用Proxy() 创建原函数的代理对象时,调用代理对象也是隐式调用原函数
e.new运算符运算
f.当函数用作对象的符号属性,触发相应的行为时,就会隐式调用原函数

8.模块导入

1
2
3
4
5
import * as mynames from 'module'; // mynames.x是对象属性读取

import {x} from 'module'; // x是来自原模块的引用,得到的是引用值

let { x : x2 } = mynames // x2 是本地声明的变量可修改
  1. new 调用普通函数
    如果该普通函数返回对象,则返回该对象
    如果该函数返回值类型数据,则返回值忽略,返回this 引用,也就是普通函数本身

  2. arr[1,2,3] 会返回arr[3],中括号里面的1,2,3会形成逗号运算

  3. delete
    不能删除
    用var/let/const声明的变量与常量
    直接继承自原型的成员
    本质上用于删除对象自有属性表属性

  4. 关于对象
    所谓原型,就是构造器用于生成实例的模板

空白对象: 它的原型链上的所有自有属性表都为空
原型链:对象所有父类和祖先类的原型所形成的,可上溯访问的链表

函数和构造器并没有明显的界限,唯一区别只在于原型prototype是不是一个有意义的值

类是静态的声明,意味着类继承的构建过程也是静态的,是在语法分析期就决定了的

当在函数f中使用super.xxx时,无论该函数被用来作为那个实际对象的方法,
super都绑定在Object.getPrototypeOf(obj)上,f为obj的属性方法

在new 类的实例时,super()执行的目的就是回溯整个原型链,确保基类最先创建实例
没有在类中声明constructor()方法时,会默认添加并调用super()

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
function foo() {
var showInArrow = () => {
console.log(this.name);
}
showInArrow();
var obj = {
showInArrow,
name: 'aObject',
showThis: function() {
console.log(this.name)
}
}
with(obj) {
var showThis2 = () => { console.log(this.name)}
showThis();
showThis2();
showInArrow();
}
}
foo.call({name: 'outSide'})
==>
outSide
aObject
outSide
outSide

继承方式的选择上,

大型系统必须采用类继承的思路,继承关系的确定性和支持静态语法检测等特性
可以帮助开发者最终简化构建大型系统的开发和业务逻辑实现,并提供足够的系统稳定性

小型系统或者体系的局部使用原型继承的思路,既可以有优美的实现和高校的性质,
也可以更深入理解js中混合不同语言特性的精髓

1
2
3
4
5
6
function MyFunction() {}

MyFunction.prototype = new Function();
var myFunc = new MyFunction();

myFunc(); //触发异常无法执行

上面例子符合对象继承语义,但不能继承它的‘可执行’效果
内置对象的特殊效果不被对象系统继承
一方面这些效果被引擎绑定在特殊的构造器上,而不是他们的原型上
另一方面,系统只负责维护内部原型链,以确保instanceof运算能正确检测这种关系
而不负责这些特定效果的实现和传递

如果一个属性使用的是存取描述符,那么无论读写性质是什么,都不会新建属性描述符
子类中如果是继承来的这样的属性,那么在子类中对该属性的读写也会忠实地调用继承来的读写器

第四章

  1. 信息是对状态集合的解释,该集合的解释成本即是编程所应付的复杂性

编程的目的是使一个系统对外呈现可解释信息

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var x = 'outer', y = 'outer';
    function foo() {
    console.log(1, [x,y]) // [undefined, undefined]
    if(true) {
    function x() {}
    } else {
    function y() {}
    }
    console.log(2, [x,y]) // [f, undefined]
    }
    foo()

第五章

  1. 关于函数参数
    默认参数都是有名字的形式参数,但是从第一个参数开始,后续所有参数不会载计入形参个数
    剩余参数不计入形参个数
    模板参数参与计数,但是无论一个模板参数被解构为多少标识符,都按一个计算
    1
    2
    3
    4
    5
    6
    7
    8
    const ff = (a,b=1,c=2) => {console.log(34)}
    ff.length ==>1

    const ff2 = (a=3,b,c) => {console.log(34)}
    ff2.length ==>0

    const ff3 = (a, [b,c]) => {console.log(34)}
    ff3.length ==>2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo(filename) {
var [filename2, ...args] = arguments;

// filename 会影响arguments
filename = 'new file name';
console.log(arguments[0]); // new file name
console.log(filename2); // test.txt

// arguments 也会影响filename
arguments[0] = filename2;
console.log(filename);// test.txt

// 使用filenam2时,没有影响
filename2 = 'update again';
console.log(arguments[0]); // test.txt
console.log(filename);// test.txt
}
foo('test.txt');

arguments 获取的参数,不被赋予初始默认值

1
2
3
4
5
function foo(a=1,b,c=2,d) {
console.log(...arguments); // undefined,100,200,300
console.log(a,b,c,d); // 1,100,200,300
}
foo(undefined,100,200,300);

惰性求值

1
2
3
4
5
6
function foo(msg) {
console.log(msg)
}
var i = 100;
foo(i+=20, i*=2, 'value:'+i); // 120
foo(i); // 240

1
2
3
4
5
6
7
var f = function func2() {
console.log(typeof func2);
}

f() // function

console.log(typeof func2); // undefined

16.
类可以赋予对象成员,但是不能进行函数调用,可以用new 来调用生成实例

17.
绑定函数特殊性质

  1. 内部原型被置为与targetFunc的原型一致
  2. 没有自有的prototype属性
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
class MyFunc {
static foo() {
console.log('prototype method in myFunc');
}
}

class MyFuncEx extends MyFunc {
static foo() {
console.log('own method in MyFuncEX');
}
callMe() {
console.log('call me in MyFuncEx');
}
}

var f = MyFuncEx.bind();
MyFuncEx.foo(); // own method in MyFuncEX
f.foo(); // prototype method in myFunc

console.log('prototype' in Object.getPrototypeOf(MyFunc)) // false

// MyFunc.bind()生成的函数没有prototype
// class MyFuncEX2 extends MyFunc.bind() {} // 会报错

// MyFuncEx原型有继承自MyFunc的prototype,可以声明,但实例不会被继承
class MyFuncEx3 extends MyFuncEx.bind() {}

// 继续继承MyFuncEx原型的prototype属性,实例可继承
class MyFuncEx4 extends MyFuncEx {}
(new MyFuncEx4).callMe() //call me in MyFuncEx

console.log('callMe' in Object.getPrototypeOf(MyFuncEx4.prototype)); // true
console.log('callMe' in Object.getPrototypeOf(MyFuncEx3.prototype)); // false
console.log('callMe' in new MyFuncEx3); // false

18.
函数与对象的区别在于,前者内部结构中初始化了[[call]] 和 [[contruct]] 这两个内部方法
绑定函数通过这两个内部方法实现绑定逻辑
重写这两个方法并使其分别指向一段特有的调用或构建逻辑(以处理暂存在内部槽中的thisArg和arg1…n参数)
代理类对象Proxy则侧重重写了对象的全部13个内部方法
借助代理类也可以实现与绑定函数完全相同的功能
需要在handlers上添加自己的陷阱,以处理[[call]] 和 [[construc]]行为

19.
用属性来替代方法,并在递归中维护this引用

1
2
3
4
5
6
7
8
9
var obj = {
get fact() {
const fact = x=> x && x*fact(x-1) || this.power || 1;
return fact
}
}

obj.power = 100;
obj.fact(9) // 36288000

20.

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
var obj = {
[Symbol.iterator] : function*() {
var obj = {
[Symbol.iterator] : function*() {
for(var i =0;i< 10; i++) yield i;
}
}

console.log(...obj) // 0 1 2 3 4 5 6 7 8 9
}
}

console.log(...obj) // 0 1 2 3 4 5 6 7 8 9

var obj = {
start: 3,
[Symbol.iterator] : function*(start = 5, end = 10) {
var {start, end} = {start, end, ...this};
for(var i=start;i<end; i++) yield i;
}
}

console.log(...obj) // 3 4 5 6 7 8 9
obj.end = 6;
console.log(...obj) // 3 4 5

delete obj.end
delete obj.start
console.log(...obj) // 5 6 7 8 9

21.

1
2
3
4
5
var msg = (function myFunc(num) {
return myFunc = typeof myFunc;
})(10) + ", and upvalue's type is: " + typeof myFunc;

console.log(msg); // function, and upvalue's type is: undefined

第六章

  1. with语句传入的值如果是基础类型数据,会转换成相应类型对象再构建闭包
  2. null 作为对象总是转换为确定的三种值类型0,’null’和false
  3. 对于值类型来说,包装类上的toString()和valueOf方法其实只会对显示方法调用有效
    而并不影响原始值的运算
  4. 类型转换分为两个阶段,其一是转换为原始值,其二是转换为尝试运算的值类型
    这两个阶段,一个受上述”隐式转换逻辑”控制,另一个受具体的运算操作控制
    会尽可能的转换成预期的类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    x = {
    toString: () => '10',
    valueOf: () => -1
    }

    parseInt(x) ===> 10 因为规定参数是字符串类型

    Math.abs(x) ===> 1

    1 + x ===> 0

    "1" + x ===> 1

    delete x.valueOf

    1+x ===> '1-1'
1
2
3
var x = new Boolean(false)
console.log(x.valueOf()) ===> false
console.log(!!x) ===> false x此时是对象
  1. 一旦对象中声明过Symbol.toPrimitive属性,那么valueOf()与toString() 在值运算的隐式转换中就无效了
  2. switch() 语句对表达式求值,并用该值与case分支中的值进行比较运算时,会采用===操作符进行运算,优先进行类型检测而不会发生类型转换过程

  3. symbol只能转换成bool类型的true,尝试转换成其他值都会报错

  4. 补前缀并转大写

    1
    2
    3
    var x = 1234567

    x.toString(16).padStart(8, '0').toUpperCase() // 0012D687
  5. 数组当对象去结构时,只会保留有值的key

1
2
3
4
5
var arr = [1,2,'345',,12];
var { 0: x, 1:y, length} = arr;
console.log(x,y,length); // 1,2,5
var x = {length: 100, ...arr};
console.log(x.length + ' => ' + Object.keys(x)); // 100 => 0,1,2,4,length
  1. 计算一个字符串中不同字符个数
1
console.log(new Set('abcadf134oaafshjafgoi').size) ==> 14
  1. 是否可重写的限制主要是两个,可引用,可写

    1
    [100][0]++ // 100
  2. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 检测成员是否是重写的
    var isRewrited = function(obj,key) {
    return obj.hasOwnProperty(key) && (key in Object.getPrototypeOf(obj));
    }

    // 检测成员是否是继承来的
    var isInherited = function(obj,key) {
    return (key in obj) && !obj.hasOwnProperty(key);
    }

    // 在对象及其原型链上查找属性的属主对象
    var getPropertyOwner = function f(obj,key) {
    return !obj ? null \
    : obj.hasOwnProperty(key) ? obj
    : f(Object.getPrototypeOf(obj),key)
    }
  3. 给Object.prototype添加成员,与添加全局变量名“效果相当”

1
2
3
4
Object.prototype.x = 100;
console.log(x) // 100
Object.getPrototypeOf(Object.getPrototypeOf(global)) === Object.prototype // true
global.constructor === Object
  1. try-catch-finally中的代码会在try最后return时,先被挂起,执行完finally再return
    如果return的值是个值类型则对返回值没有影响
    但如果是对象,return只保留引用,在finally中对return值进行了修改,那么将会影响返回值
My Little World

TS 学习

发表于 2020-10-06

数组

元素为同一类型

1
2
3
let list:number[] = [1,2,3]
或者
let list Array<number> = [1,2,3]

声明不同类型的元素就要用元祖

元祖 Tuple

已知数组个数和每个元素类型下进行声明的变量类型
如,我想定义一个变量是一个数组,第一项是字符串类型,第二项是数字

1
let x:[string,number]

后续对于相应位置的元素的使用基于该位置上元素的类型
新添加的元素类型可以为之前元素的任一类型,不能是其他类型

1
2
3
4
5
6
7
8
9
10
11
//结合解构
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);

type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}

枚举 enum

对js数据类型补充,可以通过key值互相使用

1
2
3
4
5
6
7
8
enum Color {Red, Green, Blue} //默认从0开始给元素编号
let c: Color = Color.Red; //访问元素返回值
console.log(c) ===>0

enum Color {Red = 1, Green, Blue=4,Yellow} //未赋值的依据赋值的前一个,加1
let c: Color = Color.Green;
console.log(c) ===>2 color.Yellow ===>5
let colorName: string = Color[2]; ===>'Green' Color[6]===>undefined //没赋值的为undefined

Any

任何类型
用于不确定变量类型,仅知道部分元素类型的数组或者避免对这些变量进行校验
区别对象Object类型,Object类型只允许赋值,不允许调用值上面的方法,any可以

void

没有任何类型,通常用于函数没有返回值

1
2
3
function warnUser(): void {
console.log("This is my warning message");
}

Null 和 Undefined

是所有类型的子类型,即该类型变量可以赋值给其他类型
但在指定了–strictNullChecks标记,null和undefined只能赋值给void和它们各自

Never

表示永不存在的值的类型
会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
变量也可能是 never类型,当它们被永不为真的类型保护所约束时

never类型是任何类型的子类型,也可以赋值给任何类型
然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外
即使 any也不可以赋值给never

1
2
3
4
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

Object

除基本类型(number,string,boolean,symbol,null,undefined)之外的类型

1
2
3
4
5
6
7
8
9
declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

类型断言

防止ts编译时根据推断的变量类型进行判断
让ts根据设置的类型进行编译判断

1
2
3
4
5
6
7
8
9
10
11
12
const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'

interface Foo {
bar: number;
bas: string;
}

const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';

接口

一个描述对象用来描述需要的数据类型

描述参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface LabelledValue {
label: string;
color?: string;//在可选属性名字定义的后面加一个?符号,这个属性可传可不传
//可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误
readonly y: number;//属性名前用 readonly来指定只读属性
[propName: string]: any;//索引签名,表示LabelledValue可以有任意数量的属性,只要不是上面已经定义过的 ;结合as 可以避免额外属性检查
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。

描述函数类型

1
2
3
4
5
6
7
8
9
interface SearchFunc {
(source: string, subString: string): boolean; //输入参数名,类型和返回值类型
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) { //参数名可以和SearchFunc定义的不同,因为只会检查对应位置类型
let result = source.search(subString);
return result > -1;
}

索引签名

描述了对象索引的类型,还有相应的索引返回值类型

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

TypeScript支持两种索引签名:字符串和数字。
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。
也就是说用 100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致。

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

联合类型

联合类型表示一个值可以是几种类型之一。
我们用竖线( |)分隔每个类型,
所以 number | string | boolean表示一个值可以是 number, string,或 boolean。

type 和 interface 区别
函数写法
implements 和 extends

My Little World

hook API学习

发表于 2020-10-06
1
import React,{useState,useEffect}from 'react';

useState

用于生成state数据和其对应的替换函数

1
2
3
4
5
6
7
const [state,setState] = useState(initialState)
state 即为声明的变量
setState 即更新函数
initialState即state初始值,不传入时为undefined
使用:
const [count,setCount] = useState(0)
const [obj,setObj] = useState({a:1})

useEffect

render执行结束之后执行effect

配置多个effect时从上到下依次执行

如果effect函数返回了一个函数,这个函数将会在组件卸载时执行,负责清除副作用

如果想让effect仅在某些情况下执行,可以传入第二个参数,
第二参数内为effect依赖的值,当值变化时,才会执行,否则跳过不执行

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

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
function App() {
const [data,setData] = useState(3);
useEffect(()=>{
console.log('useEffect');
let id = setTimeout(()=>{
setData({a:1})
// clearTimeout(id)
},5000)
return ()=>{ //卸载时执行
clearTimeout(id)
}
},[]) //传入第二个参数,控制是否每次render完都执行

useEffect(()=>{
console.log('会从上到下执行')
})
return (
<div>
{
(()=>{
console.log('render')
return null
})()
}
<p>{JSON.stringify(data)}</p>
</div>
)
}

useContext

1
2
const MyContext = React.createContext();
const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定

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
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(); //创建context 对象,可传入value初始值

function App() {
let [thems,setThems] = useState(themes.dark)
useEffect(()=>{
console.log('useEffect');
let id = setTimeout(()=>{
setThems(themes.light)
},5000)
return ()=>{
clearTimeout(id)
}
},[])
return (
<ThemeContext.Provider value={thems}>
<Toolbar />
<Child2 />
</ThemeContext.Provider>
)
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
//获取最近的ThemeContext.Provider 标签上value的绑定值
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
//子组件不依赖constext的数据时,使用React.memo可以避免在context有变化时进行的更新
const Child2 = React.memo((props)=>{
console.log('fire')
return <div>xxxxxx</div>
})

useRef

1
const refContainer = useRef(initialValue);

返回对象ref可以用于绑定dom对象
ref.current属性被初始化为传入的参数,绑到DOM对象后,指向绑定的dom
useRef会在每次渲染时返回同一个ref对象,在整个组件的生命周期内是唯一的

ref.current 可以存储那些不需要引起页面重新渲染的数据
如果刻意地想要从某些异步回调中读取最新的state,可以用一个ref来保存,读取,修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>
</>
);
}
export default App;

当 ref 对象内容发生变化时,useRef 并不会通知你。
变更 .current 属性不会引发组件重新渲染。
如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MeasureExample() {
const [height, setHeight] = useState(0);
console.log('fire')
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
},[]);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref
每当 ref 被附加到一个另一个节点,React 就会调用 callback

当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。
使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果

useCallback

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

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
function Child({event,data}){
console.log('child-render')
useEffect(()=>{
console.log('child-effect')
event()
},[event])
return(
<div>
<p>child</p>
<button onClick = {event}>调用父event</button>
</div>
)
}
const set = new Set()
function Test(){
const [count,setCount] = useState(0)
const [data,setData] = useState({})
const handle = useCallback(async ()=>{
function temp(){
setTimeout(()=>{
return 'xxxx'
},1000)
}
const res = await temp()
setData(res)
console.log('paent-useCallback',data)
},[count]) //count变化才会生成新的handle,才会引起child组件的useEffect的执行
set.add(handle)
console.log('parent-render====>',data)
return (
<div>
<button
onClick={e=>{
setCount(count + 1)
}}
>
count++
</button>
<p>set size:{set.size}</p>
<p>count:{count}</p>
<Child event={handle} />
</div>
)
}

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useMemo

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

返回一个 memoized 值
会在render 前执行
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

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
function Test(){
const [count,setCount] = useState(0)
const handle1 = useMemo(()=>{
console.log('handle1',count)
return count
},[])
console.log('render-parent')
return (
<div>
<p>
demo:{count}
<button onClick={()=>setCount(count+1)}>++COUNT</button>
</p>
<Child handle = {handle1} />
</div>
)
}
function Child({handle}){
console.log('render-child')
return (
<div>
<p>child</p>
<p>prop-data:{handle}</p>
</div>
)
}

打印结果:
handle1 0
render-parent
render-child

useReducer

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。
它接收一个形如 (state, action) => newState 的 reducer,
并返回当前的 state 以及与其配套的 dispatch 方法。

适用场景
state 逻辑较复杂且包含多个子值;
下一个 state 依赖于之前的 state
给那些会触发深更新的组件做性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const initialState = {count: 0};

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

初始化有两种方式
1.传入第二个参数指定初始值

1
2
3
4
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);

2.惰性初始化
将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

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
function init(initialCount) {
return {count: initialCount};
}

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}

function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行

参考资料
参考资料

1…456…25
YooHannah

YooHannah

246 日志
1 分类
21 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces