Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

  • 工程能力

  • 应用基础

    • 兼容性

    • 前端安全

    • 国际化

    • 性能优化

    • 换肤

      • React Context及换肤功能实现
        • 前言
        • Context 概念
        • Context 基本使用
          • Provider & Consumer
          • Class 组件使用 Consumer
          • Class 组件使用 contextType
          • 消费多个 Context
          • 在嵌套组件中更新 Context
        • Context 注意事项
          • 1.当传递对象给 value 时,检测变化的方式会导致一些问题
          • 2.什么情况下不该用 Context
        • 换肤
      • 前端换肤开发总结
    • 无障碍

  • 专业领域

  • 业务场景

  • 大前端
  • 应用基础
  • 换肤
gahing
2019/07/01
目录

React Context及换肤功能实现

# 前言

通过讲解 React Context 的用法,引出 React 换肤功能的实现

# Context 概念

在组件树中共享数据,避免逐层传递。

我们经常遇到这样的场景,数据需要传到子组件的子组件更甚至更下层组件,用props逐层传递的代码如下:

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}
function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 Button 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <Button theme={props.theme} />
    </div>
  );
}
function Button(props){
  // Button 组件根据传递过来的 theme 决定 背景色
  return (
    <button style={{backgroundColor:props.theme==='dark'?'black':'white'}}>
      test
    </button>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

使用 context,可以避免中间组件传递props

# Context 基本使用

# Provider & Consumer

// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  // 使用一个 Provider 来将当前的 theme 传递给以 Toolbar 开始的组件树
  // 本例使用 "dark" 值覆盖默认的 "light"值
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    )
  }
}
function Toolbar() {
  // 无需再传递 theme值
  return (
    <div>
      <Button />
    </div>
  );
}
function Button(){
  // 在 Context.Consumer 中通过 RenderProps 的方式使用
  return (
    <ThemeContext.Consumer>
      {theme =>(
        <button style={{backgroundColor:theme==='dark'?'black':'white'}}>
          test
        </button>
      )}
    </ThemeContext.Consumer>
  )
}
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

Button 往上组件树寻找最近的 ThemeContext.Provider 提供的value值,如果没有对应的 Provider,使用 createContext 时的默认值

同时也说明了一个问题,数据是单向的自上而下,若 ThemeContext.Provider 定义在子组件, ThemeContext.Consumer 在父组件,子组件传的值传不到父组件中。这里就不举例了

# Class 组件使用 Consumer

在 Class 组件中也可以用 Consumer 的形式

class Button extends React.Component {
  render() {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}>
            test
          </button>
        )}
      </ThemeContext.Consumer>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

但是在 render 里这么写看着有点乱,如果 theme的值能像 props 那样使用就好了,

# Class 组件使用 contextType

这使用就要利用 Class.contextType 来获取 this.context 值,举个例子

class Button extends React.Component {
  static contextType = ThemeContext;
  render() {
    let theme = this.context;
    return (
      <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }}>
        test
      </button>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11

static contextType = ThemeContext; 也可以写在外面: Button.contextType = ThemeContext;

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

参考:Class.contextType (opens new window)

# 消费多个 Context

可以看出来,如果是使用 contextType 的做法,只能消费一种 Context 且最近的那个 ,多 Context 还是得通过 Consumer 实现

举例:

const ThemeContext = React.createContext('light');
const UserContext = React.createContext({
  name: 'Guest'
});
class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <ThemeContext.Provider value="blue">
          <UserContext.Provider value={{ name: 'gahing' }}>
            <Toolbar />
          </UserContext.Provider>
        </ThemeContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <Button />
    </div>
  );
}

class Button extends React.Component {
  static contextType = ThemeContext;
  render() {
    let theme = this.context;
    console.log(theme)
    return (
      <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'red' }}>
        <UserContext.Consumer>
          {(user) => (
            <span>{user.name}</span>
          )}
        </UserContext.Consumer>
      </button>
    )
  }
}
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

可以看到,theme 拿到的值为最近的 Provider 提供的 blue ,button里面的内容是 gahing 。

同时,要想使用 UserContext 值,需要使用 Context.Consumer

# 在嵌套组件中更新 Context

还是以最开始的 ThemeContext 为例,此时我们需要加个功能,点击 button 后 backgroundColor 会进行切换,

最简单的想法是就是 Provider 包组件的时候传一个 toggleTheme prop,然后一层层传上去,最后 button 点击的时候执行 toggleTheme 方法,

就又回到了最开始说 props 逐层传递的弊端,那应该怎么做呢?

把 toggleTheme 和 theme 都作为一个对象属性放在 React.createContext 中的默认值参数

export const ThemeContext = React.createContext({
  theme: 'dark',
  toggleTheme: () => {},
});
1
2
3
4

具体例子

const ThemeContext = React.createContext({
  theme: 'dark',
  toggleTheme: () => {},
});
class App extends React.Component {
  constructor(props) {
    super(props);
    this.toggleTheme = () => {
      this.setState((state) => ({
        theme:
          state.theme === 'dark'
            ? 'light'
            : 'dark',
      }));
    };
    this.state = {
      theme: 'light',
    };
  }
  render() {
    return (
      <ThemeContext.Provider value={{
        theme:this.state.theme,
        toggleTheme: this.toggleTheme,
      }}>
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <Button />
    </div>
  );
}

class Button extends React.Component {
  static contextType = ThemeContext;
  render() {
    let {theme, toggleTheme} = this.context;
    return (
      <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }} onClick={toggleTheme}>
        test
      </button>
    )
  }
}
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

效果即默认白色按钮,点击后切换成黑色,再点又变成白色...

# Context 注意事项

写例子的时候用的是 tsx,不了解 typescript 的可以直接把 any 之类的删去

# 1.当传递对象给 value 时,检测变化的方式会导致一些问题

上面的例子中, ThemeContext value的值是这样的

<ThemeContext.Provider value={{
  theme:this.state.theme,
  toggleTheme: this.toggleTheme,
}}>
1
2
3
4

当 provider 的父组件(App)进行重渲染(执行render方法)时,由于 provider 的value属性总是一个新的对象,导致 consumers 组件会触发意外的渲染

把上面的例子稍微改造下就知道了

const ThemeContext = React.createContext({
  theme: 'dark',
  toggleTheme: () => {},
});
class App extends React.Component<any,any> {
  private toggleTheme:any;
  constructor(props: any) {
    super(props);
    this.toggleTheme = () => {
      this.setState((state: any) => ({
        themeContext: {
          theme: state.themeContext.theme === 'dark'
            ? 'light'
            : 'dark',
          toggleTheme: state.themeContext.toggleTheme
        }
      }));
    };
    this.state = {
      themeContext: {
        theme: 'light',
        toggleTheme: this.toggleTheme,
      },
      count: 1,
    };
  }
  componentDidMount(){
    setTimeout(() => {
      this.setState({
        count:2
      })
    }, 5000);
  }
  render() {
    console.log('render App')
    return (
      <ThemeContext.Provider value={{
        theme: this.state.themeContext.theme,
        toggleTheme: this.toggleTheme,
      }}>
        <Toolbar />
        <span>{this.state.count}</span>
      </ThemeContext.Provider>
    );
  }
}
// 使用memo,当props没有变动时不触发render
const Toolbar = React.memo(()=> {
  console.log('render Toolbar')
  return (
    <div>
      <Button />
    </div>
  );
})
// 使用 PureComponent,本来应该是 props没有变动时不触发render,但本例中还是触发了render
class Button extends React.PureComponent {
  static contextType = ThemeContext;
  render() {
    let {theme, toggleTheme} = this.context;
    console.log('render Button')
    return (
      <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }} onClick={toggleTheme}>
        test
      </button>
    )
  }
}
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

输出结果为

render App
render Toolbar
render Button
# 自动过5s后输出
render App
render Button
1
2
3
4
5
6

可以发现,App组件重渲染的时候,Button 这个 consumers 组件也发生了重渲染

Button 换成 Consumer 的实现

class Button extends React.PureComponent {
  render() {
    console.log('render Button')
    return (
      <ThemeContext.Consumer>
        {({ theme, toggleTheme }) => {
          console.log('render Consumer')
          return (
            <button style={{ backgroundColor: theme === 'dark' ? 'black' : 'white' }} onClick={toggleTheme}>
              test
            </button>
          )
        }}
      </ThemeContext.Consumer>

    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

输出结果为

render App
render Toolbar
render Button
render Consumer
# 自动过5s后输出
render App
render Consumer
1
2
3
4
5
6
7

此时没有输出 render Button 是因为 Button 并不是 Consumer 组件,只有底下的子组件(Consumer包住的部分) 才是,才会进行重渲染

改造 value,使得 consumers 组件不会重渲染

// App render 修改为
<ThemeContext.Provider value={this.state.themeContext}>
1
2

输出结果为

render App
render Toolbar
render Button
render Consumer
# 自动过5s后输出
render App
# 点击按钮后输出
render App
render Consumer
1
2
3
4
5
6
7
8
9

这就说明,App组件进行重渲染,只要 provider 提供的 value 值不变,其下的 consumers 组件就不会意外的重渲染

除了将 value 状态提升到父节点的 state 里,也可以利用 memoization 来实现

import memoize from "memoize-one";
class App extends React.Component<any, any> {
  private toggleTheme: any;
  constructor(props: any) {
    super(props);
    this.toggleTheme = () => {
      this.setState((state: any) => ({
        themeContext: {
          theme: state.themeContext.theme === 'dark'
            ? 'light'
            : 'dark',
        }
      }));
    };
    this.state = {
      themeContext: {
        theme: 'light',
      },
      count: 1,
    };
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        count: 2
      })
    }, 5000);
  }
  cacheThemeContext = memoize((theme)=>({
    theme,
    toggleTheme: this.toggleTheme
  }))
  render() {
    console.log('render App')
    return (
      <ThemeContext.Provider value={this.cacheThemeContext(this.state.themeContext.theme)}>
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
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

# 2.什么情况下不该用 Context

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据

如果你只是想避免层层传递一些属性,组件组合(component composition) (opens new window)有时候是一个比 context 更好的解决方案

参考: 使用 Context 之前的考虑 (opens new window)

大致意思就是把最底下需要用到 props 的组件提到最上层来,将组件包成一个prop往下传递,

这里有个疑问,那不还是得每个组件写一次 prop 而且这些高层组件变得更复杂了。。

当然有的说法是减少了传递的props数量,对高层组件更容易把控等等。。

所以,使用 组件组合 还是 Context 个人觉得没有详细的界限

# 换肤

需求很简单,换个主题色。

React组件库主题设计 (opens new window) 的做法,主题色定义在js中,和上文一样,通过 Context 去设置或切换主题色

同样的,主题色定义在 css 中,context 值保存 className,切换 className 实现主题切换

(示例就不提供了,可以看上面的参考文献)

编辑 (opens new window)
#React#换肤
上次更新: 2024/09/01, 23:56:56
面试官问:如何实现 H5 秒开?
前端换肤开发总结

← 面试官问:如何实现 H5 秒开? 前端换肤开发总结→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式