Vue到React迁移笔记(二)指引

455 阅读7分钟

Context

用于跨层的组件通信,类似Vue的provider+inject,挂载于根React结点可作为全局状态管理就像Vuex的实现。

API

  • React.createContext(defalultValue):创建一个一个Context对象。
    • 当一个订阅了这个Context的组件被渲染,会寻找离他最近的一个provider中读取当前的context值。
    • 只有当前组件所处树种没有找到匹配的provider,defaultValue参数会生效。这有助于单元测试。
  • Context.Provider():每个Context.provider对象会返回一个Provider React组件,其允许子集订阅者订阅他的变化。
    • <MyContext.Provider value={/* 某个值 */}>
    • 接收一个value属性,这个值被传递给订阅者。当value发生变化,订阅者组件会重新渲染且不受钩子函数阻止的影响。
    • 使用Object.is的判断是否改变逻辑
  • Context.Consumer:在这个组件中,可以使用一个参数为value的参数来返回要返回的JSX。
    • 返回的React节点对应的value参数等同于组件树上最近provider提供的值或者找不到时的默认值。
  • 在Context中保存几个方法用于改变值,像Vuex中的mountion一样为组件修改它的值提供可以调用的函数。

官网的例子

  • 使用
    • 创建一个Context对象
    • 将此Context在你需要的地方improt,并将其注入对应的组件(与Vue的eventBus相同/Vuex中你无需自己进行这一步)
    • 在你的JSX渲染中使用你注入的Context
    • 使用时,你引入的context对象会寻找provider,没有则会使用创建时的默认值
  • 这是使用了Context的组件
import {ThemeContext} from './theme-context';
class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;    
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;
  • 以下内容使用了两次上述组件,被provider包裹的一个和使用了默认值的一个。
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// 一个使用 ThemedButton 的中间组件
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }
  render() {
    // 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,    
    // 而外部的组件使用默认的 theme 值    
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}
ReactDOM.render(<App />, document.root);

性能陷阱

  • Context会使用id来决定何时更新渲染
  • 当provider的父组件重新渲染,value值可能会被重新赋值(例如value为一个固定字符串)
  • 为了防止这种情况可以将value值提升到发生重新渲染父组件的state上

高阶组件

  • 是以参数为组件,返回新组件的函数
  • 用于提取组件之间的逻辑关系的共同部分,来提取出这一部分逻辑对应的公共代码。
  • 定义一个函数,参数为逻辑关系的一方(通常为子组件),返回逻辑关系的另一方的React组件class。其中部分包含了这种逻辑关系的共同部分,以及根据其他参数确定的其他特别的逻辑或取值。
  • 使用这个函数去生成逻辑关系的另一方,自此就实现了对共同逻辑的提取。
  • tips:
    • 你不应该在HOC中更改传入的组件!
    • HOC不应该改变使用HOC之前的约定逻辑。所以你应该对props做过滤,避免一些在实现共用的过程中生成的冗余props内容。但是除了你在HOC过程中用到的或者确定的冗余props外,其他的内容都应被完整的传递给子组件。
    • 你也可以对对高阶组件再进行一次抽象提取,来形成一个返回高阶函数的高阶函数,这样组件的双方都是可定义的而非static。
    • 约定:为了便于调试,HOC生成的组件名字应带有对应HOC名前缀或者被其包裹。
    • 陷阱-render的绝对刷新:你不应该使用HOC来操作render部分,这会导致render每次调用都会用HOC计算全新的内容,导致diff认为他是全新的树,除了性能问题外,这更会会导致组件重新挂载时丢失该组件和所有子组件状态的丢失。
    • 陷阱-静态方法的丢失:HOC应用在组件时,增强函数像闭包一样保存了最初组件状态,当你为组件添加其他的静态方法时,HOC生成的组件并无法获取添加的静态方法,你需要再HOC中手动拷贝或使用hoist-non-react-statics库拷贝,或者不要把方法挂载在组件上而是单独提取并按需引入这个方法。

JSX

JSX实际是React.createElement(component, props, ...children)的语法糖

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>
以上代码会被bable编译为下面的形式
React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

tips

  • 因为JSX本质是React的语法糖,所以你必须在使用JSX时引入React以及你在JSX中用到的组件
  • 你可以在JSX的Tag位置使用点语法,来从一个对象中取出对应的组件。
    • 你仍然需要保证以大写字母开头
    • 你可以使用点语法但不可以使用表达式计算值,比如<arr[1] />是无效的
  • 大小写:JSX的tag为小写时,编译时会把这个值作为一个HTML原生组件名,将其作为静态字符传入createElement,当首字母为大写,则传入的是变量名而非字符串。
    • 为了做到运行时选择tag类型,或者必须要使用小写开头的组件,建议在render之前定义新的变量来对这些计算值或小写值进行一次映射。
  • props的默认值为true,这与HTML标签属性行为一致,但不建议简写因为容易造成与JS中ES对象简写默认值为他本身的写法混淆。
  • 你可以使用{...props}来在JSX中渐变的一次性传入多个props。
  • 你可以在JSX中使用标签包裹其他部分,这部分被包裹的内容会作为props.children传入这个组件

错误边界

  • 部分UI的JS错误不应该导致整个应用的崩溃。React16引入了新的概念—错误边界。
  • 这种组件可以捕获&打印发生在子组件树任何位置的JS错误并渲染出备用的UI。
    • 事件处理、异步代码、服务器渲染、本组件自身错误并不算在内
  • 当一个组件定义了static的getDerivedStateFromErrorcomponentDidCatch的任意一个生命周期钩子,那么他就会变成一个错误边界。
    • 抛出错误后可以使用getDerivedStateFromError()渲染备用UI或componentDidCatch()打印错误信息
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {   
    // 更新 state 使下一次渲染能够显示降级后的 UI   
    return { hasError: true };
    }
  componentDidCatch(error, errorInfo) {    
    // 你同样可以将错误日志上报给服务器    
    logErrorToMyService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {      
      // 你可以自定义降级后的 UI 并渲染      
      return <h1>Something went wrong.</h1>; 
    }
      return this.props.children; 
  }
}

Uncaught Errors

从React16开始,任何未被错误边界捕获的错误会导致整个React组件树被卸载。

事件处理器

  • 错误边界无法捕获事件处理器内部错误
  • 事件处理器不会在渲染期间触发,即使他们抛出异常,React仍然知道需要展示什么。
  • 捕获事件处理器的错误,仍然使用普通的tyt\catch

Others/API

firwardRef——Ref转发

  • 解决如何让父组件获得子组件ref的问题
  • 使用
    • 在父组件使用React.createRef创建一个React Ref变量
    • 将这个变量作为props,name为ref传递给子组件
    • 使用React.forwardRef返回子组件,传入一个参数是(props,ref)的函数,返回render。
    • 将参数ref绑定在JSX中的DOM上。
    • 当子组件挂在完成后,这个变量的.current指向绑定的dom节点