《深入React技术栈》学习笔记

词汇表

smart component, dumb component

  • dumb component。接收prop,只负责组件展示,也叫展示型组件
  • smart component。管理state和生命周期,把数据传给dumb component。也叫容器型组件

dynamic children

组件内的子组件通过动态计算得到。

render() {
    return (<div>{React.Children.map(this.props.children, (child) => {...})}</div>);
}

无状态组件

  • 使用无状态函数构建的组件
  • 本身只接收props和context,没有state,没有生命周期
  • propTypes和defaultProps可以通过设置静态属性定义
  • 创建时始终保持一个实例,避免了不必要的检查和内存分配
    Q:如何保持一个实例?props更新怎么处理的

SytheticEvent 合成事件

  • React基于Virtual DOM实现了合成事件层,我们定义的事件处理器会收到一个合成事件对象实例
  • 符合W3C标准,支持事件冒泡,可以使用stopPropagation和preventDefault
  • 可使用nativeEvent访问原生DOM事件
  • 事件委派。所有事件绑定到最外层,使用统一的事件监听器,它维持了事件和处理函数 的映射
  • 每个方法上下文会指向该组件实例,ES6 class和纯函数这种自动绑定不存在

受控组件和非受控组件

区分:value是否受react控制

HOC

两种实现方式:

  • 属性代理
import React, { Component } from 'React';
const MyContainer = (WrappedComponent) => class extends Component {
    render() {
        return <WrappedComponent {...this.props} />;
    }
}
  • 反向继承
const MyContainer = (WrappedComponent) => class extends WrappedComponent {
    render() {
        return super.render();
    }
}

调用顺序的区别:属性代理先是被包裹组件unmout,反向继承是先HOC unmout

React源码解析

源码结构

源码版本15.0。src下的文件夹:addons、isomorphic、renders、shared、core、test:

  • addons。一系列工具方法
  • ismorphic。同构方法
  • shared。公用方法。Transaction等
  • test。测试类
  • core/tests。一些边界错误的测试用例
  • renderers。核心功能,包含大部分功能实现。
    • dom
      • client。包含DOM操作方法(findDOMNode、setInnerHTML等),以及事件方法,主要是非底层的实用性事件方法,如事件监听(ReactEventListener)、常用方法(TapEventPlugin、EnterLeaveEventPlugin) 以及一些合成事件
      • server。服务端渲染的实现和方法
      • shared。文本组件(ReactDOMTextComponent)、标签组件(ReactDOMComponent)、DOM属性操作、CSS属性操作
    • shared
      • event。通用事件的插件系统代码,包含更为底层的事件方法。例如插件中心、事件注册、事件传播等通用方法。
      • reconciler。协调器,最核心的部分。包含自定义组件的实现(ReactCompositeComponent)、DOM diff算法(ReactMultiChild)。


        react插件中心.png

Virtual DOM

Virtual DOM是浏览器端用JavaScript实现的一套DOM API。包含一整套Virtual DOM模型、生命周期的维护和管理、diff算法、将Virtual DOM展示为原生DOM的patch方法。

Virtual DOM模型

Virtual DOM模型的基本元素:

  • 标签名
  • 节点属性。包含样式、属性、事件等
  • 子节点
  • 标识id
    样例:
{
    tagName: "div", // 标签名
    properties: { // 属性
        style: {}, // 样式
    },
    children: [], // 子节点
    key: 1, // 唯一标识
}

Virtual DOM中的节点称为ReactNode,分为三种:

  • ReactElement
  • ReactFragment
  • ReactText
type ReactNode = ReactElement | ReactFragment | ReactText;
type ReactElement = ReactComponentElement | ReactDOMElement;
type ReactDOMElement = {
    type: string,
    props: {
        children: ReactNodeList,
        className: string,
        ...
    },
    key: string | number | boolean | null,
    ref: string | null,
};
type ReactComponentElement<TProps> = {
    type: ReactClass<TProps>,
    props: Tprops,
    key: string | number | boolean | null,
    ref: string | null,
};
type ReactFragment = Array<ReactNode | ReactEmpty>;
type ReactNodeList = ReactNode | ReactEmpty;
type ReactText = string | number;
type ReactEmpty = boolean | null | undefined;

React元素的创建

使用JSX创建的元素最终会被编译成调用React.createElement方法,例如:

// 输入
const app = <Nav color="blue"><Profile>text</Profile></Nav>;
// 输出
const app = React.createElement(
    Nav,
    { color: "blue" },
    React.createElement(Profile, null, "click"),
);

React.createElement代码逻辑:

  1. 初始化参数
  2. 第二参数里的内容赋值给props
  3. 后续参数全部赋值给props.children
  4. 对确少的props如果有默认的,设置默认值

初始化组件的入口

使用React创建组件的实例的时候,首先会调用instantiateReactComponent,它通过node类型区分不同的组件的入口。逻辑如下:

  • node为空时,初始化空组件ReactEmptyComponent.create(instantiateReactComponent)
  • node类型为object时,如果elemtn.type是字符串,初始化DOM标签组件 ReactNativeComponent.createInternalComponent(elemnt),否则初始化自定义组件new ReactCompositeComponentWrapper(elemnt)。
  • node类型为字符串或数字时,初始化文本组件ReactnativeComponent.createInstanceForText(node)
  • 其他情况不处理

文本组件

node类型为文本节点时,不算Virtual DOM元素,但React为了保持渲染的一致性,将其封装为文本组件ReactDOMTextComponent。

  • 在moutnComponent阶段,判断是否是通过transaction.useCreateElement创建判断是否是通过createElement方法创建,如果是创建相应的标签和domID,这样每个文本节点一样拥有自己的标识,也有Virtual DOM Diff的权利;否则,直接返回文本内容
    不再为裸露的文本内容包裹span是React15的更新点
  • 在receiveComponent阶段,通过DOMChildrenOperations.replaceDelimitedText来更新文本内容

DOM标签组件

ReactDOMComponent对Virtual DOM的处理包含两个部分:

  1. 更新属性。包括样式、更新属性、处理事件
    • mountComponent时,ReactDOMComponent先生成标记和标签,通过this._createOpenTagAndPutListeners(tranaction)来处理DOM节点的属性和事件:
      1. 如果存在事件,调用enqueuePutListener(this, propKey, propValue, transaction)添加事件代理。
      2. 如果存在样式,合并然后通过CSSPropertyOperations.createMarkupForStyles(propValue, this)创建样式
      3. 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性
      4. 通过DOMPropertyOperations.createMarkupForID(this.domID)创建唯一标识
    • receiveComponent时,通过this.updateComponent来更新DOM节点属性。先删除旧的不要的属性,再更新属性
  2. 更新子节点。包括更新内容、更新子节点,此部分涉及diff算法
    • mountComponent。通过this._createContentMarkup(transaction, props, context)来处理DOM节点内容,如果有子节点,则递归调用mountComponent渲染
    • receiveComponent。通过this._updateDOMChildren(lastProps, nextProps, transaction, context)更新内容,先删除不需要的子节点和内容,再更新子节点和内容,分别调用this.updateChildren和this.updateTextContent。updateChildre为diff内容

自定义组件

自定义组件实现了一套生命周期和setState机制

生命周期顺序

生命周期执行顺序:

  • 首次挂载组件,按顺序执行getDefaultProps、getInitialState、componentWillMount、render、componentDidMount
  • 卸载组件,执行componentWillUnmount
  • 重新挂载组件,和首次挂载相比,不再执行getDefaultProps
  • 组件更新,componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate
    生命周期主要通过三个阶段进行管理:MOUGTING、RECEIVE_PROPS、UNMOUTING
createClass创建自定义组件

该方法是创建自定义组件的入口,管理getDefaultProps生命周期,该方法只执行一次,所有实例共享默认的props。执行步骤:

  • 初始化,按顺序合并mixin
  • 执行getDefaultProps
  • 从原型上删除未定义的生命周期钩子函数
MOUNTING

执行逻辑:

  1. 初始化公共的props和contexts
  2. 判断是否是无状态组件并初始化
  3. 将实例存储为引用,放在全局的一个map里
  4. 初始化state
  5. 初始化更新队列
  6. componentWillMount,此时setState不会rerender,会合并state inst.state = this._processPendingState(inst.props, inst.state)在componentWillMount后执行,所以该生命周期内的state不是最新的。React利用更新队列this._pendingStateQueue、更新状态this._pendingReplaceState、this.pendingForceUpdate来实现setState异步更新
  7. render,render之后会diff,render执行后会有key比较
  8. 递归渲染
  9. componentDidMount
RECEIVE_PROPS

执行逻辑:

  1. 分布判断context和props是否改变,改变了则处理,并标记willReceive为true
  2. 如果willReceive为true,并且存在componentWillReceiveProps则执行
  3. 将新的state合并到更新队列
  4. 判断是否需要update,此时也会执行shouldComponentUpdate。如果不需要更新,则设置state和props,如果需要更新,则执行更新
  5. 如果存在componentDidUpdate暂存一份旧的state,props,context作为后续的参数传递
  6. 执行componentWillUpdate
  7. render,render之后会diff,render执行后会有key比较
  8. componentDidUpdate
    render执行前的生命周期,实例的state不是最新的,设置了inst.state = nextState之后才是
UNMOUNTING

此时会执行componentWillUnmount,调用setState不会rerender,因为所有更新队列和更新状态都是null

无状态组件

无状态组件只是一个render方法,没有组件类实例化过程也没有实例返回

生命周期顺序图.png

setState机制

异步更新

state通过队列更新,setState会将更新放入到队列,直接对state赋值不会放入更新队列,下次对队列状态合并时,会忽略之前被修改的

循环调用风险

队列的一次更新会获取_pendingElement、_pengdingStateQueue、_pendingForceUpdate;如果在shouldCompoentUpdate和componentWillUpdate里调用setState,此时this.pendingStateQueue != null,则permUpdateIfNessary会调用updateComponent,然后再调用这两个生命周期,造成循环调用

// 如果在 _pendingElement、_pendingStateQueue和_pendingForceUpdate更新组件 performUpdateIfNecessary: function(transaction) {
  if (this._pendingElement != null) {
    ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
  }
  if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
    this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
  }
}

调用栈

setState->newState存入pending队列 -> 调用enqueueUpdate,-> 判断是否批量更新,是则将组件保存到dirtyComponents,否,遍历dirtyComponents,updateComponent,更新pending state or props

setState是最终调用的enqueUpdate,然后执行batchingStrategy.batchedUpdates,该方法是ReactDefualtbatchingStrategy(注入上去的)的batchedUpdates方法,调用了transaction.perfomr,这个transaction也是注入上去的,属于事务部分提供的功能

事务

事务.png

事务的大致逻辑:

  1. 将需要执行的方法用wrapper封装,在通过perform方法执行,先执行所有wrapper的initialize,然后perform,然后close,一个wrapper定义了一组initialize和close,事务支持多个wrapper叠加
  2. 事务提供一个mixin方法供其他模块实现自己需要的事务,这个模块还需要额外实现getTransactionWrapper,来获取所有封装的initialize和close

diff算法

传统的通过循环递归节点依次进行比较的diff算法,算法复杂度高达o(n3)。
React将Virtual DOM树转换成acturl树的最少操作的过程叫做调和(reconciliation)。

diff策略

  1. Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  3. 对于同一层级的一组子节点他们可以通过唯一的ID进行区分
    React基于以上策略分别对tree diff、component diff、elment diff进行算法优化

tree diff

两棵树只会对同一层级进行比较,当出现跨层级移动时,会销毁节点创建新的

component diff

  1. 同类型组件,按照原策略比较Virtual DOM树
  2. 如果不是,将该组件设置为dirty component,从而替换整个组件
  3. 同类型组件,允许使用shouldComponentUpdate来判断组件是否需要进行diff算法分析

element diff

当节点处于同一层级的时候,diff提供了3中操作INSERT_MARKUP(插入)、MOVE_EXISTING(插入)、MOVE_NODE(删除)。

对同一层级节点,可以添加唯一key进行区分。

过程:
对新集合中的节点进行循环遍历,通过唯一的key判断集合中是否存在相同的节点,存在则进行移动操作,移动之前需要将当前节点在旧集合中的位置与lastIndex进行比较,如果小于则不移动

应该尽量减少最后一个元素移动到第一个的操作

React Patch

将只算出的节点反映在真实的DOM树上,源码位置:DOMChildrenOperations.js。会顺序遍历差异队列,根据更新类型,执行对应的操作

其他

熟悉事件系统代码
空组件初始化ReactEmptyComponent.create(instantiateReactComponent)里的参数是什么作用?
ReactDOMTextComponent初始化的时候创建的openingCommoent的作用?长什么样子?
react组件什么时候重新挂载
forceUpdate运行流程?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,176评论 5 469
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,190评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,232评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,953评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,879评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,177评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,626评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,295评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,436评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,365评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,414评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,096评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,685评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,771评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,987评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,438评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,032评论 2 341

推荐阅读更多精彩内容