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方法来包裹需要进行的操作。

  • 相关阅读:
    2.17NOIP模拟赛(by hzwer) T2 小奇的序列
    2.17NOIP模拟赛(by hzwer) T1 小奇挖矿
    题解【洛谷P3662】[USACO17FEB]Why Did the Cow Cross the Road II S
    题解【CF886B】Vlad and Cafes
    题解【CJOJ1070/UVA】嵌套矩形
    题解 【CF381A】 Sereja and Dima
    何时使用UI层的智能表单技术
    开机加电到系统打开究竟发生了什么?(1)
    asp.net MVC 常见安全问题及解决方案
    HDU 4422 The Little Girl who Picks Mushrooms【水题】
  • 原文地址:https://www.cnblogs.com/fuGuy/p/11407514.html
Copyright © 2011-2022 走看看