一切天空 發表於 2019-8-25 12:23:00

React源码解析——ReactAPI

<p><span style="font-size: 1.5em">一、API背景</span></p>
<h3>&nbsp;api的具体转化关系</h3>
<p>可以通过到https://babeljs.io/repl/网站去将我们创建的Jsx进行实时的转译</p>
<p><img src="https://img2018.cnblogs.com/blog/988076/201908/988076-20190825122159409-1729536442.png" alt=""></p>
<p><img src="https://img2018.cnblogs.com/blog/988076/201908/988076-20190825123000749-1507555063.png" alt=""></p>
<p><img src="https://img2018.cnblogs.com/blog/988076/201908/988076-20190825133534348-1945314039.png" alt=""></p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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,
};
</pre>
</div>
<p>  请先无视<code>__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED</code></p>
<p>&nbsp;  <span style="background-color: rgba(255, 0, 0, 1)"><strong>__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED</strong></span>这个属性引用得是<code>ReactSharedInternals</code>,其中包括了<code>ReactCurrentOwner</code>和<code>ReactDebugCurrentFrame</code>(仅dev),ReactCurrentOwner是fiber算法用到的,这样是把<code>renderer</code>完全独立,所以以后即使换个render算法也没有问题,ReactDebugCurrentFrame则是用来调试render过程的,</p>
<h3 id="children">Children</h3>
<p>这个对象提供了一堆帮你处理<code>props.children</code>的方法,因为<code>children</code>是一个类似数组但是不是数组的数据结构,如果你要对其进行处理可以用<code>React.Children</code>外挂的方法。</p>
<h3 id="createref">createRef</h3>
<p>新的<code>ref</code>用法,React即将抛弃<code>&lt;div ref="myDiv" /&gt;</code>这种<code>string ref</code>的用法,将来你只能使用两种方式来使用<code>ref</code></p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">class App extends React.Component{

constructor() {
    this.ref = React.createRef()
}

render() {
    return &lt;div ref={this.ref} /&gt;
    // or
    return &lt;div ref={(node) =&gt; this.funRef = node} /&gt;
}

}
</pre>
</div>
<p>  </p>
<h3 id="component--purecomponent">Component &amp; PureComponent</h3>
<p>这两个类基本相同,唯一的区别是<code>PureComponent</code>的原型上多了一个标识</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">if (ctor.prototype &amp;&amp; ctor.prototype.isPureReactComponent) {
return (
    !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
</pre>
</div>
<p>  </p>
<p>这是检查组件是否需要更新的一个判断,<code>ctor</code>就是你声明的继承自<code>Component or PureComponent</code>的类,他会判断你是否继承自<code>PureComponent</code>,如果是的话就<code>shallowEqual</code>比较<code>state</code>和<code>props</code>。</p>
<p>顺便说一下:React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有<code>shouldComponentUpdate</code>方法,二就是这里的<code>PureComponent</code>判断</p>
<h3 id="createcontext">createContext</h3>
<p><code>createContext</code>是官方定稿的<code>context</code>方案,在这之前我们一直在用的老的<code>context API</code>都是React不推荐的API,现在新的API释出,官方也已经确定在17大版本会把老<code>API</code>去除。</p>
<p>新API的使用方法:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">const { Provider, Consumer } = React.createContext('defaultValue')

const ProviderComp = (props) =&gt; (
&lt;Provider value={'realValue'}&gt;
    {props.children}
&lt;/Provider&gt;
)

const ConsumerComp = () =&gt; (
&lt;Consumer&gt;
    {(value) =&gt; &lt;p&gt;{value}&lt;/p&gt;}
&lt;/Consumber&gt;
)
</pre>
</div>
<p>  </p>
<p>后面讲<code>context</code>会专门比较新老的API的差异,提前说一句,老API的性能不是一般的差</p>
<h3 id="forwardref">forwardRef</h3>
<p><code>forwardRef</code>是用来解决HOC组件传递<code>ref</code>的问题的,所谓HOC就是<code>Higher Order Component</code>,比如使用<code>redux</code>的时候,我们用<code>connect</code>来给组件绑定需要的state,这其中其实就是给我们的组件在外部包了一层组件,然后通过<code>...props</code>的方式把外部的<code>props</code>传入到实际组件。<code>forwardRef</code>的使用方法如下:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">const TargetComponent = React.forwardRef((props, ref) =&gt; (
&lt;TargetComponent ref={ref} /&gt;
))
</pre>
</div>
<p>  </p>
<p>这也是为什么要提供<code>createRef</code>作为新的<code>ref</code>使用方法的原因,如果用<code>string ref</code>就没法当作参数传递了。</p>
<p>这里只是简单说一下使用方法,后面讲<code>ref</code>的时候会详细分析。</p>
<h3 id="类型">类型</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE,
</pre>
</div>
<p>  </p>
<p>这四个都是React提供的<em>组件</em>,但他们呢其实都只是占位符,都是一个<code>Symbol</code>,在React实际检测到他们的时候会做一些特殊的处理,比如<code>StrictMode</code>和<code>AsyncMode</code>会让他们的子节点对应的Fiber的<code>mode</code>都变成和他们一样的<code>mode</code></p>
<h3 id="createelement--cloneelement--createfactory--isvalidelement">createElement &amp; cloneElement &amp; createFactory &amp; isValidElement</h3>
<p><code>createElement</code>可谓是React中最重要的API了,他是用来创建<code>ReactElement</code>的,但是很多同学却从没见过也没用过,这是为啥呢?因为你用了JSX,JSX并不是标准的js,所以要经过编译才能变成可运行的js,而编译之后,<code>createElement</code>就出现了:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">// jsx
&lt;div id="app"&gt;content&lt;/div&gt;

// js
React.createElement('div', { id: 'app' }, 'content')
</pre>
</div>
<p>  </p>
<p><code>cloneElement</code>就很明显了,是用来克隆一个<code>ReactElement</code>的</p>
<p><code>createFactory</code>是用来创建专门用来创建某一类<code>ReactElement</code>的工厂的,</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">export function createFactory(type) {
const factory = createElement.bind(null, type);
factory.type = type;
return factory;
}
</pre>
</div>
<p>  </p>
<p>他其实就是绑定了第一个参数的<code>createElement</code>,一般我们用JSX进行编程的时候不会用到这个API</p>
<p>&nbsp;</p>
<h1 id="reactelement">二、ReactElement</h1>
<p><code>ReactElement</code>通过<code>createElement</code>创建,调用该方法需要传入三个参数:</p>
<ul>
<li>type</li>
<li>config</li>
<li>children</li>
</ul>
<p><code>type</code>指代这个<code>ReactElement</code>的类型</p>
<ul>
<li>字符串比如<code>div</code>,<code>p</code>代表原生DOM,称为<code>HostComponent</code></li>
<li>Class类型是我们继承自<code>Component</code>或者<code>PureComponent</code>的组件,称为<code>ClassComponent</code></li>
<li>方法就是<code>functional Component</code></li>
<li>原生提供的<code>Fragment</code>、<code>AsyncMode</code>等是Symbol,会被特殊处理</li>
<li>TODO: 是否有其他的</li>
</ul>
<p>从源码可以看出虽然创建的时候都是通过<code>config</code>传入的,但是<code>key</code>和<code>ref</code>不会跟其他<code>config</code>中的变量一起被处理,而是单独作为变量出现在<code>ReactElement</code>上。</p>
<p>在最后创建<code>ReactElement</code>我们看到了这么一个变量<code>$$typeof</code>,这是个啥呢,在这里可以看出来他是一个常量:<code>REACT_ELEMENT_TYPE</code>,但有一个特例:<code>ReactDOM.createPortal</code>的时候是<code>REACT_PORTAL_TYPE</code>,不过他不是通过<code>createElement</code>创建的,所以他应该也不属于<code>ReactElement</code></p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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
}
</pre>
</div>
<p>  </p>
<p><code>ReactElement</code>只是一个用来承载信息的容器,他会告诉后续的操作这个节点的以下信息:</p>
<ol>
<li><code>type</code>类型,用于判断如何创建节点</li>
<li><code>key</code>和<code>ref</code>这些特殊信息</li>
<li><code>props</code>新的属性内容</li>
<li><code>$$typeof</code>用于确定是否属于<code>ReactElement</code></li>
</ol>
<p>这些信息对于后期构建应用的树结构是非常重要的,而React通过提供这种类型的数据,来脱离平台的限制</p>
<p>&nbsp;</p>
<h1>二、React Children</h1>
<blockquote>
<p>最开始<code>React.Children</code>这个 API 是不想讲的,一方面平时不怎么用,另一方面跟数组处理功能差不多,不深究实现是比较容易理解的。但是后来实际去看了一下源码之后发现,他的实现方式还是非常有趣的,尤其是<code>map</code>和<code>forEach</code>,我们就按照<code>map</code>的流程来看一下,<code>forEach</code>其实差不多,只是没有返回新的节点。</p>
</blockquote>
<p>先来看一下流程图:</p>
<p><img src="https://img2018.cnblogs.com/blog/988076/201908/988076-20190826102851481-665373990.png" alt=""></p>
<h3>开始源码</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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)
}

</pre>
</div>
<p>  </p>
<p><code>map</code>和<code>forEach</code>的最大区别就是有没有<code>return result</code>。</p>
<p><code>getPooledTraverseContext</code>就是从<code>pool</code>里面找一个对象,<code>releaseTraverseContext</code>会把当前的<code>context</code>对象清空然后放回到<code>pool</code>中。</p>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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 &lt; POOL_SIZE) {
    traverseContextPool.push(traverseContext)
}
}
</pre>
</div>
<p>  那么按照这个流程来看,是不是<code>pool</code>永远都只有一个值呢,毕竟推出之后操作完了就推入了,这么循环着。答案肯定是否的,这就要讲到<code>React.Children.map</code>的一个特性了,那就是对每个节点的<code>map</code>返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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 &lt; children.length; i++) {
      child = children
      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
}
</pre>
</div>
<p>  这里就是一层递归了,对于可循环的<code>children</code>,都会重复调用<code>traverseAllChildrenImpl</code>,直到是一个节点的情况,然后调用<code>callback</code>,也就是<code>mapSingleChildIntoContext</code></p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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 =&gt; c)
} else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      mappedChild = cloneAndReplaceKey(
      mappedChild,
      keyPrefix +
          (mappedChild.key &amp;&amp; (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      )
    }
    result.push(mappedChild)
}
}
</pre>
</div>
<p>  </p>
<p><code>mapSingleChildIntoContext</code>这个方法其实就是调用<code>React.Children.map(children, callback)</code>这里的<code>callback</code>,就是我们传入的第二个参数,并得到<code>map</code>之后的结果。注意重点来了,如果<code>map</code>之后的节点还是一个数组,那么再次进入<code>mapIntoWithKeyPrefixInternal</code>,那么这个时候我们就会再次从<code>pool</code>里面去<code>context</code>了,而<code>pool</code>的意义大概也就是在这里了,如果循环嵌套多了,可以减少很多对象创建和<code>gc</code>的损耗。</p>
<p>而如果不是数组并且是一个合规的<code>ReactElement</code>,就触达顶点了,替换一下<code>key</code>就推入<code>result</code>了。</p>
<p>React 这么实现主要是两个目的:</p>
<ol>
<li>拆分<code>map</code>出来的数组</li>
<li>因为对<code>Children</code>的处理一般在<code>render</code>里面,所以会比较频繁,所以设置一个<code>pool</code>减少声明和<code>gc</code>的开销</li>
</ol>
<p>这就是<code>Children.map</code>的实现,虽然不算什么特别神奇的代码,但是阅读一下还是挺有意思的。</p>
<h3 class="title-article">react性能优化之ConcurrentMode 和 flushSync</h3>
<p>  使用 ConcurrentMode 组件包裹的子组件的渲染过程的优先级会被降低,react 会先渲染优先级高的,然后将js线程空闲出来先干其他的事,如动画的渲染,完了之后再渲染优先级低的,当我们想提高子组件渲染的优先级的时候,可以使用flushSync方法来包裹需要进行的操作。</p><br><br>
来源:https://www.cnblogs.com/fuGuy/p/11407514.html
頁: [1]
查看完整版本: React源码解析——ReactAPI