进击React源码之磨刀试炼2

进击React源码之磨刀试炼部分为源码解读基础部分,会包含多篇文章,本篇为第二篇,第一篇《进击React源码之磨刀试炼1》入口(点击进入)。javascript

初探Component与PureComponent

若是有没用过PureComponent或不了解的同窗,能够看看这篇文章什么时候使用Component仍是PureComponent?html

猜猜组件内部如何实现?

Component(组件)做为React中最重要的概念,每当建立类组件都要继承ComponentPureComponent,在未开始看源码的时候,你们能够先跟本身谈谈对于ComponentPureComponent的印象,不妨根据经验猜一猜Component内部将会为咱们实现怎样的功能?java

先来写个简单的组件react

class CompDemo extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      msg: 'hello world'
    }
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({
        msg: 'Hello React'
      });
    }, 1000)
  }

  render() {
    return (
      <div className="CompDemo"> <div className="CompDemo__text"> {this.state.msg} </div> </div>
    )
  }
}
复制代码

经过这个简单的组件,咱们猜猜, Component/ PureComponent组件内部可能帮咱们处理了 props, state,定义了生命周期函数, setStaterender等不少功能。

源码实现

打开packages/react/src/ReactBaseClasses.js,打开后里面有不少英文注释,但愿你们无论经过什么手段先翻译看看,本身先大体了解一下。以后贴出的源码中我会过滤掉自带的注释和if(__DEV__)语句,有兴趣了解的同窗能够翻阅源码研究。git

Componentgithub

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

复制代码

以上就是Component相关的源码,它竟如此出奇的简洁!字面来看懂它也很简单,首先定义了Component构造函数,以后在其原型链上设置了isReactComponent(Component组件标志)、setState方法和forceUpdate方法。web

Component构造函数能够接收三个参数,其中propscontext咱们大多数人应该都接触过,在函数中还定义了this.refs为一个空对象,但updater就是一个比较陌生的东西了,在setStateforceUpdate方法中咱们能够看到它的使用:segmentfault

  • setState并无具体实现更新state的方法,而是调用了updaterenqueueSetStatesetState接收两个参数:partialState就是咱们要更新的state内容,callback可让咱们在state更新后作一些自定义的操做,this.updater.enqueueSetState在这里传入了四个参数,咱们能够猜到第一个为当前实例对象,第二个是咱们更新的内容,第三个是传入的callback,最后一个是当前操做的名称。这段代码上面invariant的做用是判断partialState是不是对象、函数或者null,若是不是则会给出提示。在这里咱们能够看出,setState第一个参数不只能够为Object,也能够是个函数,你们在实际操做中能够尝试使用。
  • forceUpdate相比于setState,只有callback,同时在使用enqueueForceUpdate时候也少传递了一个参数,其余参数跟setState中调用保持一致。

这个updater.enqueueForceUpdate来自ReactDomReactReactDom是分开的两个不一样的内容,不少复杂的操做都被封装在了ReactDom中,所以React才保持如此简洁。React在不一样平台(native和web)使用的都是相同的代码,可是不一样平台的DOM操做流程多是不一样的,所以将state的更新操做经过对象方式传递过来,可让不一样的平台去自定义本身的操做逻辑,React就能够专一于大致流程的实现。api

PureComponent数组

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;

Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
复制代码

看完Cpomponent的内容,再看PureComponent就很简单了,单看PureComponent的定义是与Component是彻底同样的,这里使用了寄生组合继承的方式,让PureComponent继承了Component,以后设置了isPureReactComponent标志为true。

若是有同窗对JavaScript继承不是很了解,这里找了一篇掘金上的文章深刻JavaScript继承原理 你们能够点击进入查看

Refs的用法与实现

ref的使用

经过ref咱们能够得到组件内某个子节点的信息病对其进行操做,ref的使用方式有三种:

class RefDemo extends PureComponent {
  constructor() {
    super()
    this.objRef = React.createRef()
  }

  componentDidMount() {
    setTimeout(() => {
      this.refs.stringRef.textContent = "String ref content changed";
      this.methodRef.textContent = "Method ref content changed";
      this.objRef.current.textContent = "Object ref content changed";
    }, 3000)
  }

  render() {
    return (
      <div className="RefDemo"> <div className="RefDemo__stringRef" ref="stringRef">this is string ref</div> <div className="RefDemo__methodRef" ref={el => this.methodRef = el}>this is method ref</div> <div className="RefDemo__objRef" ref={this.objRef}>this is object ref</div> </div>
    )
  }
}

export default RefDemo;
复制代码

Jietu20190818-124212

  1. string ref(不推荐,可能废弃):经过字符串方式设置ref,会在this.refs对象上挂在一个key为所设字符串的属性,用来表示该节点的实例对象。若是该节点为dom,则对应dom示例,若是是class component则对应该组件实例对象,若是是function component,则会出现错误,function component没有实例,但能够经过forward ref来使用ref
  2. method ref:经过function来建立ref(笔者在以前实习工做中基本都是使用这种方式,很是好用)。
  3. 经过createRef()建立对象,默认建立的对象为{current: null},将其传递个某个节点,在组件渲染结束后会将此节点的实例对象挂在到current

createRef的实现

源码位置packages/react/src/ReactCreactRef.js

export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}
复制代码

它上方有段注释an immutable object with a single mutable value,告诉咱们建立出来的对象具备单个可变值,可是这个对象是不可变的。在其内部跟咱们上面说的同样,建立了{current: null}并将其返回。

forwardRef的使用

const FunctionComp = React.forwardRef((props, ref) => (
  <div type="text" ref={ref}>Hello React</div>
))

class FnRefDemo extends PureComponent {
  constructor() {
    super();
    this.ref = React.createRef();
  }

  componentDidMount() {
    setTimeout(() => {
      this.ref.current.textContent = "Changed"
    }, 3000)
  }

  render() {
    return (
      <div className="RefDemo"> <FunctionComp ref={this.ref}/> </div> ) } } 复制代码

forwardRef的使用,可让Function Component使用ref,传递参数时须要注意传入第二个参数ref

forwardRef的实现

export default function forwardRef<Props, ElementType: React$ElementType>( render: (props: Props, ref: React$Ref<ElementType>) => React$Node, ) {
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}

复制代码

forwardRef接收一个函数做为参数,这个函数就是咱们的函数组件,它包含propsref属性,forwardRef最终返回的是一个对象,这个对象包含两个属性:

  1. $$typeof:这个属性看过上一篇文章的小伙伴应该还记得,它是标志React Element类型的东西。
  2. render: 咱们传递进来的函数组件。

这里说明一下,尽管forwardRef返回的对象中$$typeofREACT_FORWARD_REF_TYPE,可是最终建立的ReactElement的$$typeof仍然是REACT_ELEMENT_TYPE

这里文字描述有点绕,配合图片来看文字会好点。

enter description here

在上述forwardRef使用的代码中建立的FunctionComp{$$typeof:REACT_FORWARD_REF_TYPE,render}这个对象,在使用<FunctionComp ref={this.ref}/>时,它的本质是React.createElement(FunctionComp, {ref: xxxx}, null)这样的,此时FunctionComp是咱们传进createElement中的type参数,createElement返回的element$$typeof仍然是REACT_ELEMENT_TYPE

ReactChildren的使用方法和实现

ReactChildren的使用

function ParentComp ({children}) {
  return (
    <div className="parent"> <div className="title">Parent Component</div> <div className="content"> {children} </div> </div>
  )
}
复制代码

这样的代码你们平时用的应该多一点,在使用ParentComp组件时候,能够在标签中间写一些内容,这些内容就是children。

来看看React.Children.map的使用

function ParentComp ({children}) {
  return (
    <div className="parent"> <div className="title">Parent Component</div> <div className="content"> {React.Children.map(children, c => [c,c, [c]])} </div> </div>
  )
}

class ChildrenDemo extends PureComponent{
  constructor() {
    super()
    this.state = {}
  }

  render() {
    return (
      <div className="childrenDemo"> <ParentComp> <div>child 1 content</div> <div>child 2 content</div> <div>child 3 content</div> </ParentComp> </div>
    )
  }
}

export default ChildrenDemo;
复制代码

结果

咱们在使用这个API的时候,传递了两个参数,第一个是children,你们应该比较熟悉,第二个是一个回调函数,回调函数传入一个参数(表明children的一个元素),返回一个数组(数组不是一位数组,里面三个元素最后一个仍是数组),在结果中咱们能够看到,这个API将咱们返回的数组平铺为一层[c1,c1,c1,c2,c2,c2,c3,c3,c3],浏览器中显示的也就如上图所示。

有兴趣的小伙伴能够尝试阅读官方文档对于这个api的介绍

ReactChildren的实现

react.js中定义React时候咱们能够看到一段关于Children的定义

Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },
复制代码

Children包含5个API,这里咱们先详细讨论map API。这一部分并非很好懂,请你们看的时候必定要用心。

笔者读这一部分也是费了很大的劲,而后用思惟导图软件画出了这个思惟导图+流程图的东西(暂时就给它起名为思惟流程图,其实更流程一点,而不思惟),画得仍是比较详细的,因此就很大,小伙伴最好把这个图下载下来放大看(能够配合源码,也能够配合下文),图片地址user-gold-cdn.xitu.io/2019/8/21/1…

enter description here

因为图过小不清楚,下面也会分别截出每一个函数的流程图。

打开packages/react/src/ReactChildren.js,找到mapChildren

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
复制代码

enter description here

这段代码短小精悍,给咱们提供了直接使用的API。它内部逻辑也很是简单,首先看看children是否为null,若是若是为null就直接返回null,若是不是,则定义result(初始为空数组)来存放结果,通过mapIntoWithKeyPrefixInternal的一系列处理,获得结果。结果不论是null仍是result,其实咱们再写代码的时候都遇到过,若是一个组件中间什么都没传,结果就是null什么都不会显示,若是传递了一个<div>那就显示这个div,若是传递了一组div那就显示这一组(此时就是children不为null的状况),最后显示出来的东西也就是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);
}
复制代码

在进入这个函数的时候,必定要注意使用这个函数时候传递进来的参数到底是哪几个,否则后面传递次数稍微一多就会晕头转向。

enter description here

从上一个函数跳过来的时候传递了5个参数,你们能够注意一下这五个参数表明的是什么:

  1. children:咱们再组件中间写的JSX代码
  2. result: 最终处理完成存放结果的数组
  3. prefix: 前缀,这里为null
  4. func: 咱们在演示使用的过程当中传入的第二个参数,是个回调函数c => [c,c,[c]]
  5. context: 上下文对象

这个函数首先对prefix前缀字符串作了个处理,处理完以后仍是个字符串。而后经过getPooledTraverseContext函数从对象重用池中拿出一个对象,说到这里,咱们就不得不打断一下这个函数的讲解,忽然出现一个对象重用池的概念,不少人会很懵逼,而且若是强制把这个函数解析完再继续下一个,会让不少读者产生不少疑惑,不利于后面源码的理解。

暂时跳到getPooledTraverseContext看看对象重用池

const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext( mapResult, keyPrefix, mapFunction, mapContext, ) {
  if (traverseContextPool.length) {
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}
复制代码

enter description here

首先看在使用getPooledTraverseContext获取对象的时候,传递了4个参数:

  1. array: 上个函数中对应的result,表示最终返回结果的数组
  2. escapedPrefix: 前缀,一个字符串,没什么好说的
  3. func: 咱们使用API传递的回调函数 c=>[c,c,[c]]
  4. context: 上下文对象

而后咱们看看它作了什么,它去一个traverseContextPool数组(这个数组默认为空数组,最多存放10个元素)中尝试pop取出一个元素,若是能取出来的话,这个元素是一个对象,有5个属性,这里会把传进来的4个参数保存在这四个元素中,方便后面使用,另一个属性是个用来计数的计数器。若是没取出来,就返回一个新对象,包含的也是这五个属性。这里要跟你们说说对象重用池了。这个对象有5个属性,若是每次使用这个对象都从新建立一个,那么会有较大的建立对象开销,为了节省这部分建立的开销,咱们能够在使用完这个对象以后,把它的5个属性都置为空(count就是0了),而后扔回这个数组(对象重用池)中,后面要用的时候就直接从对象重用池中拿出来,没必要从新建立对象,增长开销了。

再回到mapIntoWithKeyPrefixInternal函数中继续向下读 经过上一步拿到一个带有5个属性的对象以后,继续通过traverseAllChildren函数的一系列处理,获得了最终的结果result,其中具体内容太多下面再说,而后经过releaseTraverseContext函数释放了那个带5个参数的对象。咱们先来看看如何释放的:

function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}
复制代码

这里也跟咱们上面说的对象重用池有所对应,这里先把这个对象的5个属性清空,而后看看对象重用池是否是有空,有空的话就把这个清空的属性放进去,方便下次使用,节省建立开销。

traverseAllChildren和traverseAllChildrenImpl的实现

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
复制代码

enter description here

这个函数基本没作什么重要的事,仅仅判断了children是否为null,若是是的话就返回0,不是的话就进行具体的处理。仍是强调这里传递的参数,必定要注意,看图就能够了,就不用文字描述了。

重要的是traverseAllChildrenImpl函数,这个函数有点长,这里给你们分红了两部分,能够分开看

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') {
      const iterator = iteratorFn.call(children);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getComponentKey(child, ii++);
        subtreeCount += traverseAllChildrenImpl(
          child,
          nextName,
          callback,
          traverseContext,
        );
      }
    } else if (type === 'object') {
      let addendum = '';
      const childrenString = '' + children;
      invariant(
        false,
        'Objects are not valid as a React child (found: %s).%s',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys(children).join(', ') + '}'
          : childrenString,
        addendum,
      );
    }
  }

  return subtreeCount;
}
复制代码

enter description here

上面的流程图说的很详细了,你们能够参照来看源码。这里就简单说一下这个函数的两部分分别做了什么事。 第一部分是对children类型进行了检查(没有检查为Array或迭代器对象的状况),若是检查children是合法的ReactElement就会进行callback的调用,这里必定要注意callback传进来的是谁,这里是callback为mapSingleChildIntoContext,一直让你们关注传参问题,就是怕你们看着看着就搞混了。 第二部分就是针对children是数组和迭代器对象的状况进行了处理(迭代器对象检查的原理是obj[Symbol.iterator],比较简单你们能够本身定位源码找一下具体实现),而后对他们进行遍历,每一个元素都从新执行traverseAllChildrenImpl函数造成递归。 它其实只让可渲染的单元素进行下一步callback的调用,若是是数组或迭代器,就进行遍历。

最后一步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,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}
复制代码

enter description here

这里咱们就用到了从对象重用池拿出来的对象,那个对象做用其实就是利用那5个属性帮咱们保存了一些须要使用的变量和函数,而后执行咱们传入的funcc => [c,c,[c]]),若是结果不是数组而是元素而且不为null就会直接存储到result结果中,若是是个数组就会对它进行遍历,从mapIntoWithKeyPrefixInternal开始从新执行造成递归调用,直到最后将嵌套数组中全部元素都拿出来放到result中,这样就造成了咱们最初看到的那种效果,无论咱们的回调函数是多少层的数组,最后都会变成一层。

小结

这里文字性的小结就留给你们,给你们画了一张总结性的流程图(有参考yck大神的图),但实际上是根据本身看源码画出来的并非搬运的。

enter description here

ReactChildren的其余方法

{
  forEach,
  count,
  toArray,
  only,
}
复制代码

对于这几个方法,你们能够自行查看了,建议先浏览一遍forEach,跟map很是类似,可是比map少了点东西。其余几个都是四五行的代码,你们本身看看。里面用到的函数咱们上面都有讲到。

小结

这篇文章跟你们一块儿读了ComponentrefsChildren相关的源码,最复杂的仍是数Children了,说实话,连看大神博客,看源码、画图带写文章,花了七八个小时,其实内容跟大神们的文章比起来仍是很不同的,若是基础不是很好的同窗,我感受这里会讲的更详细。 你们一块儿努力,明天的咱们必定会感谢今天努力的本身。

原创不易,若是本篇文章对你有帮助,但愿能够帮忙点个赞,有兴趣也能够帮忙github点个star,感谢各位。本篇文章github地址

相关文章
相关标签/搜索