为什么你应该放弃老的React Context API改用新的Context API

本文对比了React中新旧Context API的性能,通过实验发现新版API显著提升了效率,尤其是在大量DOM更新时,新API仅需0.4毫秒,而旧版需7毫秒。新API减少了不必要的DOM重绘,提高了应用性能。
React16.3发布了新的Context API,并且已经确认了将在下一个版本废弃老的Context API。所以大家更新到新的Context API是无可厚非的事情。而这篇文章会从原理的角度为大家分析为什么要用新的API--不仅仅是因为React官方要更新,毕竟更新了你也可以用16版本的React来使用老的API--而是因为新的API性能比老API 高出太多

用法

我们先来看一下两个版本的Context API如何使用

// old version
class Parent extends Component{
  getChildContext() {
    return {type: 123}
  }
}

Parent.childContextType = {
  type: PropTypes.number
}

const Child = (props, context) => (
  <p>{context.type}</p>
)

Child.contextTypes = {
  type: PropTypes.number
}

通过在父组件上声明getChildContext方法为其子孙组件提供context,我们称其ProviderComponent。注意必须要声明Parent.childContextType才会生效,而子组件如果需要使用context,需要显示得声明Child.contextTypes

// new version
const { Provider, Consumer } = React.createContext('defaultValue')

const Parent = (props) => (
  <Provider value={'realValue'}>
    {props.children}
  </Provider>
)

const Child = () => {
  <Consumer>
    {
      (value) => <p>{value}</p>
    }
  </Consumer>
}

新版本的API,React提供了createContext方法,这个方法会返回两个组件ProviderConsumberProvider用来提供context的内容,通过向Provider传递value这个prop,而在需要用到对应context的地方,用相同来源的Consumer来获取contextConsumer有特定的用法,就是他的children必须是一个方法,并且context的值使用参数传递给这个方法。

性能对比

正好前几天React devtool发布了Profiler功能,就用这个新功能来查看一下两个API的新能有什么差距吧,先看一下例子

不知道Profiler的看这里

// old api demo
import React from 'react'
import PropTypes from 'prop-types'

export default class App extends React.Component {
  state = {
    type: 1,
  }

  getChildContext() {
    return {
      type: this.state.type
    }
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        type: this.state.type + 1
      })
    }, 500)
  }

  render() {
    return this.props.children
  }
}

App.childContextTypes = {
  type: PropTypes.number
}

export const Comp = (props, context) => {
  const arr = []
  for (let i=0; i<100; i++) {
    arr.push(<p key={i}>{i}</p>)
  }

  return (
    <div>
      <p>{context.type}</p>
      {arr}
    </div>
  )
}

Comp.contextTypes = {
  type: PropTypes.number
}
// new api demo
import React, { Component, createContext } from 'react'

const { Provider, Consumer } = createContext(1)

export default class App extends Component {

  state = {
    type: 1
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        type: this.state.type + 1
      })
    }, 500)
  }

  render () {
    return (
      <Provider value={this.state.type}>
        {this.props.children}
      </Provider>
    )
  }

}

export const Comp = () => {
  const arr = []
  for (let i=0; i<100; i++) {
    arr.push(<p key={i}>{i}</p>)
  }

  return (
    <div>
      <Consumer>
        {(type) => <p>{type}</p>}
      </Consumer>
      {arr}
    </div>
  )
}
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

import App, {Comp} from './context/OldApi'

// import App, { Comp } from './context/NewApi'

ReactDOM.render(
  <App><Comp /></App>,
  document.getElementById('root')
)

代码基本相同,主要变动就是一个interval,每500毫秒给type加1,然后我们来分别看一下Profiler的截图

不知道Profiler的看这里

老API

老API

新API

新API

可见这两个性能差距是非常大的,老的API需要7点几毫秒,而新的API只需要0.4毫秒,而且新的API只有两个节点重新渲染了,而老的API所有节点都重新渲染了(下面还有很多节点没截图进去,虽然每个可能只有0.1毫秒或者甚至不到,但是积少成多,导致他们的父组件Comp渲染时间很长)

进一步举例

在这里可能有些同学会想,新老API的用法不一样,因为老API的context是作为Comp这个functional Component的参数传入的,所以肯定会影响该组件的所有子元素,所以我在这个基础上修改了例子,把数组从Comp组件中移除,放到一个新的组件Comp2

// Comp2
export class Comp2 extends React.Component {
  render() {
    const arr = []
    for (let i=0; i<100; i++) {
      arr.push(<p key={i}>{i}</p>)
    }

    return arr
  }
}

// new old api Comp
export const Comp = (props, context) => {
  return (
    <div>
      <p>{context.type}</p>
    </div>
  )
}

// new new api Comp
export const Comp = () => {
  return (
    <div>
      <Consumer>
        {(type) => <p>{type}</p>}
      </Consumer>
    </div>
  )
}

现在受context影响的渲染内容新老API都是一样的,只有<p>{type}</p>,我们再来看一下情况

老API

老API

新API

新API

忽视比demo1时间长的问题,应该是我电脑运行时间长性能下降的问题,只需要横向对比新老API就可以了

从这里可以看出来,结果跟Demo1没什么区别,老API中我们的arr仍然都被重新渲染了,导致整体的渲染时间被拉长很多。

事实上,这可能还不是最让你震惊的地方,我们再改一下例子,我们在App中不再修改type,而是新增一个statenum,然后对其进行递增

// App
export default class App extends React.Component {
  state = {
    type: 1,
    num: 1
  }

  getChildContext() {
    return {
      type: this.state.type
    }
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        num: this.state.num + 1
      })
    }, 500)
  }

  render() {
    return (
      <div>
        <p>inside update {this.state.num}</p>
        {this.props.children}
      </div>
    )
  }
}
老API

老API

新API

新API

可以看到老API依然没有什么改观,他依然重新渲染所有子节点。

再进一步我给Comp2增加componentDidUpdate生命周期钩子

componentDidUpdate() {
  console.log('update')
}

在使用老API的时候,每次App更新都会打印

而新API则不会

总结

从上面测试的结果大家应该可以看出来结果了,这里简单的讲一下原因,因为要具体分析会很长并且要涉及到源码的很多细节,所以有空再写一片续,来详细得讲解源码,大家有兴趣的可以关注我。

要分析原理要了解React对于每次更新的处理流程,React是一个树结构,要进行更新只能通过某个节点执行setState、forceUpdate等方法,在某一个节点执行了这些方法之后,React会向上搜索直到找到root节点,然后把root节点放到更新队列中,等待更新。

所以React的更新都是从root往下执行的,他会尝试重新构建一个新的树,在这个过程中能复用之前的节点就会复用,而我们现在看到的情况,就是因为复用算法根据不同的情况而得到的不同的结果

我们来看一小段源码

if (
  !hasLegacyContextChanged() &&
  (updateExpirationTime === NoWork ||
    updateExpirationTime > renderExpirationTime)
) {
  // ...
  return bailoutOnAlreadyFinishedWork(
    current,
    workInProgress,
    renderExpirationTime,
  );
}

如果能满足这个判断条件并且进入bailoutOnAlreadyFinishedWork,那么有极高的可能这个节点以及他的子树都不需要更新,React会直接跳过,我们使用新的context API的时候就是这种情况,但是使用老的context API是永远不可能跳过这个判断的

老的context API使用过程中,一旦有一个节点提供了context,那么他的所有子节点都会被视为有side effect的,因为React本身并不判断子节点是否有使用context,以及提供的context是否有变化,所以一旦检测到有节点提供了context,那么他的子节点在执行hasLegacyContextChanged的时候,永远都是true的,而没有进入bailoutOnAlreadyFinishedWork,就会变成重新reconcile子节点,虽然最终可能不需要更新DOM节点,但是重新计算生成Fiber对象的开销还是又得,一两个还好,数量多了时间也是会被拉长的。

以上就是使用老的context API比新的API要慢很多的原因,大家可以先不深究得理解一下,在我之后的源码分析环节会有更详细的讲解。

<think>我们正在讨论的是Android开发中关于文件目录的问题。用户提到使用`context.getDataDir()`创建的文件夹看不见,需要解释原因。 根据引用[4]中的信息: - `context.getDataDir()`在API 24(Android 7.0)及以上返回的是`/data/data/包名`(注意引用[4]中描述有误,它写成了`/storage/emulated/0/Android/data/包名/cache`,但根据标准文档,`getDataDir()`返回的是内部存储的私有目录,即`/data/data/包名`) - 而外部存储的私有目录是`/storage/emulated/0/Android/data/包名`,由`getExternalFilesDir()`等方法返回。 用户问题:为什么使用`context.getDataDir()`创建的文件夹看不见? 原因分析: 1. `/data/data/包名`这个目录位于设备的内部存储(internal storage)中,是应用程序的私有目录。 2. 该目录对于非root设备,普通用户(以及没有root权限的应用)是无法通过文件管理器直接查看的。即使通过ADB,如果没有root权限,也只能在应用自己的权限下访问(即只有应用自身和root用户可见)。 3. 而用户通常使用文件管理器查看的是外部存储(external storage),即`/storage/emulated/0/`下的内容,其中应用的私有外部目录是`/storage/emulated/0/Android/data/包名`,这个目录在文件管理器中可见(但需要用户授权访问外部存储,并且从Android 11开始,其他应用无法直接访问该目录)。 因此,在非root设备上,用户无法在文件管理器中直接看到`/data/data/包名`下的文件,这就是为什么使用`context.getDataDir()`创建的文件夹看不见。 如果想要创建用户可见的文件夹,应该使用外部存储的私有目录(使用`getExternalFilesDir()`)或者公共目录(需要存储权限)。 下面按照用户要求的格式回答: ### 为什么使用 `context.getDataDir()` 创建的文件夹看不见? 在 Android 系统中,应用程序的私有数据存储分为两个区域: 1. **内部存储 (Internal Storage)** - 路径:`/data/data/应用包名`(由 `context.getDataDir()` 返回) - 特性: - 完全私有(仅应用自身和系统可访问) - 无需任何权限即可读写 - **用户无法直接通过文件管理器查看**(非 root 设备) - 适用场景:存储敏感数据或应用私有文件 2. **外部存储私有目录 (External Storage - App-Specific Directory)** - 路径:`/storage/emulated/0/Android/data/应用包名`(由 `context.getExternalFilesDir()` 返回) - 特性: - 应用卸载时自动清除 - **用户可通过文件管理器查看**(需授予存储权限) - 从 Android 11 开始,其他应用无法直接访问此目录[^2] #### 根本原因 `context.getDataDir()` 返回的是内部存储路径 (`/data/data/包名`),该目录: - 受 Linux 文件系统权限保护(`rwx------` 模式) - 仅应用自身 UID 和 root 用户有访问权限 - 普通用户无权限浏览此目录 ```mermaid graph LR A[文件存储位置] --> B[内部存储 /data/data/包名] A --> C[外部存储 /storage/emulated/0/Android/data/包名] B --> D[用户不可见] C --> E[用户可见] ``` #### 解决方案 若需要创建用户可见的文件夹,请改用以下方法: ```java // 获取外部存储的私有目录(用户可见) File visibleDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS); ``` - 此目录在文件管理器中路径为:`Android/data/应用包名/files/Documents` - 注意:从 Android 11 开始,其他应用无法访问此目录,但用户可通过系统文件管理器查看 --- ### 技术验证 可通过以下代码检查目录属性: ```java File dataDir = context.getDataDir(); Log.d("Storage", "内部存储路径: " + dataDir.getAbsolutePath()); Log.d("Storage", "目录可读性: " + (dataDir.canRead() ? "可读" : "不可读")); Log.d("Storage", "目录可写性: " + (dataDir.canWrite() ? "可写" : "不可写")); ``` 输出示例: ``` 内部存储路径: /data/data/com.example.app 目录可读性: 可读 目录可写性: 可写 ``` 尽管应用自身可读写,但该路径在非 root 设备上对用户不可见。 --- §§相关问题§§ 1. 如何在 Android 设备上查看应用的内部存储文件? 2. Android 11 及以上版本如何管理外部存储访问权限? 3. 应用私有目录和公共目录在数据安全上有何差异? 4. 如何将文件从内部存储迁移到外部存储? 5. 哪些类型的文件适合存储在内部存储中? </think>### 为什么使用 `context.getDataDir()` 创建的文件夹看不见? 在 Android 系统中,使用 `context.getDataDir()` 创建的文件夹不可见的原因主要是**系统级访问限制**和**存储位置特性**: #### 1. 内部存储的访问限制 `context.getDataDir()` 返回的是应用的**内部私有存储路径**(`/data/data/包名`),该目录具有以下特性: - **专属权限**:仅当前应用和 root 用户可访问 - **沙盒隔离**:其他应用(包括文件管理器)无法直接访问 - **系统保护**:Android 内核通过 Linux 权限系统(如 `rwx------`)严格限制访问[^1][^2] #### 2. 路径位置的特殊性 - 内部存储路径位于系统分区:`/data/data/包名` - 外部可见路径位于用户分区:`/storage/emulated/0/...` - 两者物理存储位置不同,普通文件管理器只能显示用户分区内容[^3] #### 3. 调试验证方法 ```java // 创建测试文件夹 File hiddenDir = new File(context.getDataDir(), "test_folder"); hiddenDir.mkdirs(); // 验证存在性(应用内可见) Log.d("DIR_TEST", "路径存在: " + hiddenDir.exists()); // 输出 true Log.d("DIR_TEST", "绝对路径: " + hiddenDir.getAbsolutePath()); // 输出示例: /data/data/com.example.app/test_folder ``` #### 4. 用户可见的替代方案 如需创建用户可见的文件夹,改用外部存储 API: ```java // 获取用户可见的外部私有目录 File visibleDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS); // 路径示例: /storage/emulated/0/Android/data/包名/files/Documents ``` ```mermaid graph TD A[存储类型] --> B[内部存储 /data/data/包名] A --> C[外部存储 /storage/emulated/0] B --> D[系统分区] B --> E[仅应用和root可访问] C --> F[用户分区] C --> G[文件管理器可见] ``` --- ### 根本原因总结 内部存储路径 (`/data/data/包名`) 的不可见性是由 Android 的安全沙盒机制决定的: 1. Linux 文件权限限制访问 2. 系统分区与用户分区隔离 3. 应用沙盒保护用户数据安全[^2][^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值