zoukankan      html  css  js  c++  java
  • React源码解析——ReactAPI

    一、API背景

     api的具体转化关系

    可以通过到https://babeljs.io/repl/网站去将我们创建的Jsx进行实时的转译

    const React = {
      Children: {
        map,
        forEach,
        count,
        toArray,
        only,
      },
    
      createRef,
      Component,
      PureComponent,
    
      createContext,
      forwardRef,
    
      Fragment: REACT_FRAGMENT_TYPE,
      StrictMode: REACT_STRICT_MODE_TYPE,
      unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
      unstable_Profiler: REACT_PROFILER_TYPE,
    
      createElement: __DEV__ ? createElementWithValidation : createElement,
      cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
      createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
      isValidElement: isValidElement,
    
      version: ReactVersion,
    
      __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
    };
    

      请先无视__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

       __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED这个属性引用得是ReactSharedInternals,其中包括了ReactCurrentOwnerReactDebugCurrentFrame(仅dev),ReactCurrentOwner是fiber算法用到的,这样是把renderer完全独立,所以以后即使换个render算法也没有问题,ReactDebugCurrentFrame则是用来调试render过程的,

    Children

    这个对象提供了一堆帮你处理props.children的方法,因为children是一个类似数组但是不是数组的数据结构,如果你要对其进行处理可以用React.Children外挂的方法。

    createRef

    新的ref用法,React即将抛弃<div ref="myDiv" />这种string ref的用法,将来你只能使用两种方式来使用ref

    class App extends React.Component{
    
      constructor() {
        this.ref = React.createRef()
      }
    
      render() {
        return <div ref={this.ref} />
        // or
        return <div ref={(node) => this.funRef = node} />
      }
    
    }
    

      

    Component & PureComponent

    这两个类基本相同,唯一的区别是PureComponent的原型上多了一个标识

    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
      return (
        !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
      );
    }
    

      

    这是检查组件是否需要更新的一个判断,ctor就是你声明的继承自Component or PureComponent的类,他会判断你是否继承自PureComponent,如果是的话就shallowEqual比较stateprops

    顺便说一下:React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有shouldComponentUpdate方法,二就是这里的PureComponent判断

    createContext

    createContext是官方定稿的context方案,在这之前我们一直在用的老的context API都是React不推荐的API,现在新的API释出,官方也已经确定在17大版本会把老API去除。

    新API的使用方法:

    const { Provider, Consumer } = React.createContext('defaultValue')
    
    const ProviderComp = (props) => (
      <Provider value={'realValue'}>
        {props.children}
      </Provider>
    )
    
    const ConsumerComp = () => (
      <Consumer>
        {(value) => <p>{value}</p>}
      </Consumber>
    )
    

      

    后面讲context会专门比较新老的API的差异,提前说一句,老API的性能不是一般的差

    forwardRef

    forwardRef是用来解决HOC组件传递ref的问题的,所谓HOC就是Higher Order Component,比如使用redux的时候,我们用connect来给组件绑定需要的state,这其中其实就是给我们的组件在外部包了一层组件,然后通过...props的方式把外部的props传入到实际组件。forwardRef的使用方法如下:

    const TargetComponent = React.forwardRef((props, ref) => (
      <TargetComponent ref={ref} />
    ))
    

      

    这也是为什么要提供createRef作为新的ref使用方法的原因,如果用string ref就没法当作参数传递了。

    这里只是简单说一下使用方法,后面讲ref的时候会详细分析。

    类型

    Fragment: REACT_FRAGMENT_TYPE,
    StrictMode: REACT_STRICT_MODE_TYPE,
    unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
    unstable_Profiler: REACT_PROFILER_TYPE,
    

      

    这四个都是React提供的组件,但他们呢其实都只是占位符,都是一个Symbol,在React实际检测到他们的时候会做一些特殊的处理,比如StrictModeAsyncMode会让他们的子节点对应的Fiber的mode都变成和他们一样的mode

    createElement & cloneElement & createFactory & isValidElement

    createElement可谓是React中最重要的API了,他是用来创建ReactElement的,但是很多同学却从没见过也没用过,这是为啥呢?因为你用了JSX,JSX并不是标准的js,所以要经过编译才能变成可运行的js,而编译之后,createElement就出现了:

    // jsx
    <div id="app">content</div>
    
    // js
    React.createElement('div', { id: 'app' }, 'content')
    

      

    cloneElement就很明显了,是用来克隆一个ReactElement

    createFactory是用来创建专门用来创建某一类ReactElement的工厂的,

    export function createFactory(type) {
      const factory = createElement.bind(null, type);
      factory.type = type;
      return factory;
    }
    

      

    他其实就是绑定了第一个参数的createElement,一般我们用JSX进行编程的时候不会用到这个API

    二、ReactElement

    ReactElement通过createElement创建,调用该方法需要传入三个参数:

    • type
    • config
    • children

    type指代这个ReactElement的类型

    • 字符串比如divp代表原生DOM,称为HostComponent
    • Class类型是我们继承自Component或者PureComponent的组件,称为ClassComponent
    • 方法就是functional Component
    • 原生提供的FragmentAsyncMode等是Symbol,会被特殊处理
    • TODO: 是否有其他的

    从源码可以看出虽然创建的时候都是通过config传入的,但是keyref不会跟其他config中的变量一起被处理,而是单独作为变量出现在ReactElement上。

    在最后创建ReactElement我们看到了这么一个变量$$typeof,这是个啥呢,在这里可以看出来他是一个常量:REACT_ELEMENT_TYPE,但有一个特例:ReactDOM.createPortal的时候是REACT_PORTAL_TYPE,不过他不是通过createElement创建的,所以他应该也不属于ReactElement

    export function createElement(type, config, children) {
      // 处理参数
    
      return ReactElement(
        type,
        key,
        ref,
        self,
        source,
        ReactCurrentOwner.current,
        props,
      );
    }
    
    const ReactElement = function(type, key, ref, self, source, owner, props) {
      const element = {
        // This tag allows us to uniquely identify this as a React Element
        $$typeof: REACT_ELEMENT_TYPE,
    
        // Built-in properties that belong on the element
        type: type,
        key: key,
        ref: ref,
        props: props,
    
        // Record the component responsible for creating this element.
        _owner: owner,
      };
    
      return element
    }
    

      

    ReactElement只是一个用来承载信息的容器,他会告诉后续的操作这个节点的以下信息:

    1. type类型,用于判断如何创建节点
    2. keyref这些特殊信息
    3. props新的属性内容
    4. $$typeof用于确定是否属于ReactElement

    这些信息对于后期构建应用的树结构是非常重要的,而React通过提供这种类型的数据,来脱离平台的限制

    二、React Children

    最开始React.Children这个 API 是不想讲的,一方面平时不怎么用,另一方面跟数组处理功能差不多,不深究实现是比较容易理解的。但是后来实际去看了一下源码之后发现,他的实现方式还是非常有趣的,尤其是mapforEach,我们就按照map的流程来看一下,forEach其实差不多,只是没有返回新的节点。

    先来看一下流程图:

    开始源码

    function mapChildren(children, func, context) {
      if (children == null) {
        return children
      }
      const result = []
      mapIntoWithKeyPrefixInternal(children, result, null, func, context)
      return result
    }
    
    function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
      let escapedPrefix = ''
      if (prefix != null) {
        escapedPrefix = escapeUserProvidedKey(prefix) + '/'
      }
      const traverseContext = getPooledTraverseContext(
        array,
        escapedPrefix,
        func,
        context,
      )
      traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)
      releaseTraverseContext(traverseContext)
    }
    
    

      

    mapforEach的最大区别就是有没有return result

    getPooledTraverseContext就是从pool里面找一个对象,releaseTraverseContext会把当前的context对象清空然后放回到pool中。

    const POOL_SIZE = 10
    const traverseContextPool = []
    function getPooledTraverseContext() {
      // args
      if (traverseContextPool.length) {
        const traverseContext = traverseContextPool.pop()
        // set attrs
        return traverseContext
      } else {
        return {
          /* attrs */
        }
      }
    }
    
    function releaseTraverseContext(traverseContext) {
      // clear attrs
      if (traverseContextPool.length < POOL_SIZE) {
        traverseContextPool.push(traverseContext)
      }
    }
    

      那么按照这个流程来看,是不是pool永远都只有一个值呢,毕竟推出之后操作完了就推入了,这么循环着。答案肯定是否的,这就要讲到React.Children.map的一个特性了,那就是对每个节点的map返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。

    function traverseAllChildren(children, callback, traverseContext) {
      if (children == null) {
        return 0
      }
    
      return traverseAllChildrenImpl(children, '', callback, traverseContext)
    }
    
    function traverseAllChildrenImpl(
      children,
      nameSoFar,
      callback,
      traverseContext,
    ) {
      const type = typeof children
    
      if (type === 'undefined' || type === 'boolean') {
        children = null
      }
    
      let invokeCallback = false
    
      if (children === null) {
        invokeCallback = true
      } else {
        switch (type) {
          case 'string':
          case 'number':
            invokeCallback = true
            break
          case 'object':
            switch (children.$$typeof) {
              case REACT_ELEMENT_TYPE:
              case REACT_PORTAL_TYPE:
                invokeCallback = true
            }
        }
      }
    
      if (invokeCallback) {
        callback(
          traverseContext,
          children,
          nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
        )
        return 1
      }
    
      let child
      let nextName
      let subtreeCount = 0 // Count of children found in the current subtree.
      const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR
    
      if (Array.isArray(children)) {
        for (let i = 0; i < children.length; i++) {
          child = children[i]
          nextName = nextNamePrefix + getComponentKey(child, i)
          subtreeCount += traverseAllChildrenImpl(
            child,
            nextName,
            callback,
            traverseContext,
          )
        }
      } else {
        const iteratorFn = getIteratorFn(children)
        if (typeof iteratorFn === 'function') {
          // iterator,和array差不多
        } else if (type === 'object') {
          // 提醒不正确的children类型
        }
      }
    
      return subtreeCount
    }
    

      这里就是一层递归了,对于可循环的children,都会重复调用traverseAllChildrenImpl,直到是一个节点的情况,然后调用callback,也就是mapSingleChildIntoContext

    function mapSingleChildIntoContext(bookKeeping, child, childKey) {
      const { result, keyPrefix, func, context } = bookKeeping
    
      let mappedChild = func.call(context, child, bookKeeping.count++)
      if (Array.isArray(mappedChild)) {
        mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c)
      } else if (mappedChild != null) {
        if (isValidElement(mappedChild)) {
          mappedChild = cloneAndReplaceKey(
            mappedChild,
            keyPrefix +
              (mappedChild.key && (!child || child.key !== mappedChild.key)
                ? escapeUserProvidedKey(mappedChild.key) + '/'
                : '') +
              childKey,
          )
        }
        result.push(mappedChild)
      }
    }
    

      

    mapSingleChildIntoContext这个方法其实就是调用React.Children.map(children, callback)这里的callback,就是我们传入的第二个参数,并得到map之后的结果。注意重点来了,如果map之后的节点还是一个数组,那么再次进入mapIntoWithKeyPrefixInternal,那么这个时候我们就会再次从pool里面去context了,而pool的意义大概也就是在这里了,如果循环嵌套多了,可以减少很多对象创建和gc的损耗。

    而如果不是数组并且是一个合规的ReactElement,就触达顶点了,替换一下key就推入result了。

    React 这么实现主要是两个目的:

    1. 拆分map出来的数组
    2. 因为对Children的处理一般在render里面,所以会比较频繁,所以设置一个pool减少声明和gc的开销

    这就是Children.map的实现,虽然不算什么特别神奇的代码,但是阅读一下还是挺有意思的。

    react性能优化之ConcurrentMode 和 flushSync

      使用 ConcurrentMode 组件包裹的子组件的渲染过程的优先级会被降低,react 会先渲染优先级高的,然后将js线程空闲出来先干其他的事,如动画的渲染,完了之后再渲染优先级低的,当我们想提高子组件渲染的优先级的时候,可以使用flushSync方法来包裹需要进行的操作。

  • 相关阅读:
    java io系列23之 BufferedReader(字符缓冲输入流)
    java io系列22之 FileReader和FileWriter
    java io系列21之 InputStreamReader和OutputStreamWriter
    java io系列20之 PipedReader和PipedWriter
    java io系列19之 CharArrayWriter(字符数组输出流)
    java io系列18之 CharArrayReader(字符数组输入流)
    java io系列17之 System.out.println("hello world")原理
    java io系列16之 PrintStream(打印输出流)详解
    java io系列15之 DataOutputStream(数据输出流)的认知、源码和示例
    java io系列14之 DataInputStream(数据输入流)的认知、源码和示例
  • 原文地址:https://www.cnblogs.com/fuGuy/p/11407514.html
Copyright © 2011-2022 走看看