转载于:https://www.yuque.com/ant-h5/sourcecode/pizqme
作者: ant-h5, 蚂蚁金服前端团队
转载方式: 手打
转载者: lemon-Xu
React设计灵感
react最初的设计灵感源于游戏, 游戏渲染的机制: 当数据变化时, 界面仅仅更新变化的一部分形成新的一帧渲染.
设计react的核心是认为UI只是把数据通过映射关系变成另一种形式的数据, 也就是展现方式. 传统上, web框架使用模板或者html指令构造页面.
react处理构建用户界面通过将他们分解为虚拟组件, 虚拟组件是react的核心, 整个react框架的设计理念, 都是围绕虚拟组件进行的.
1.组件
在界面变更时, react不直接操作dom, 而是通过组件对比(dom-diff), 找到界面更新前后组件的差异, 而只更新又变化的dom节点, 现有技术的时间时间复杂度O(n^3), n是页面上的元素, 这意味这如果页面上有1000个节点, 会对比10亿次, react重写了dom-diff算法, 把时间复杂度降低到O(n).
react把组件分成了三类, 三类合称为: Reat Component, 它另一个名字我们也许更加熟悉: Virtual DOM.
1.1 ReactTextComponent
文件组件:
<div>
<span>hello</span>
world
</div>
这里, world就是文件组件, 为什么单独给world设置成文字组件? 每个文字组件外面, 都需要包装一层span用来设置id(或者key), 用来diff.
1.2 ReactNativeComponent
html原生组件:
<div>
<span>hello</span>
world
</div>
还是刚才那个例子, div和span都是原生组件. react的主要算法(如mount, dom-diff)都是在这里实现. 用来dom-diff的在渲染阶段会直接设置在原生组件标签上. 就像下面真样:
1.3 ReactCompositeComponent
react复合组件:
<Table>
<Table.Columns .../>
...
</Table>
这次我们来换个例子, 复合组件就是我们常见的react组件, 只不过在react内部叫做复合组件. 而react组件是三种组件的合成.
复合组件有以下特点: 符合组件可以聚合其他的复合组件与原生组件, 但是最底层的复合组件一定只能集合原生组件. 复合组件通常以大写开头.
1.4组件实现
每个组件都有自己的props和children, props就是组件的属性, 比如style, id等. children是当前组件的子组件. 比如:
<Form>
<From.Item>
用户名:
<input placeholder="请输入用户名" />
</Form.Item>
<From>
这段jsx会创建3个组件: From, Form, item, inpu, 前两个是复合组件, 最后一个是原生组件, 连小学生都能看懂他们的关系:
- From
- children
:[ Form.item ]
- children
- From.item
- children: [span<ReactTextComponent, input
>]
- children: [span<ReactTextComponent, input
- span
- children: string
- children: string
- input
- props:{placeholder: string
} - children: [ ]
- props:{placeholder: string
在众多教材中, 这组树形结构被描述成为一个json:
{
"component": Form<ReactCompositeComponent>,
"children": [{
"component": Form.Item<ReactCompositeComponent>,
"children": [{
"component": span<ReactNativeComponent>,
"children": string<String>
}, {
"component": input<ReactNativeComponent>,
"props": { placeholder: string<String> },
"children": []
}]
}]
}
每个虚拟组件都有自己的children, 这样一来, 从container元素开始的dom树就有一棵结构相同的虚拟组件树.
2. dom diff
在实际项目中, 随着页面数据变化(用户交互)或者后端数据返回, 更新大多数只有三种情况:
- dom的属性或者内容更新(update).
- dom元素类型发生变化(insert).
- dom元素的位置发生变化, 或者新增(insert), 或者删除(remove)
当然还有第四种: 对于跨层级的移动更新少之又少, 比如像这种
<div>
<title>子标签</title>
<input />
</div>
更新成
<title>标签</title>
<div>
<input />
</div>
当然一些业务需要时(比如某同学转班或者能从左表格移动到右表格的穿梭框), 我们可以认为它是dom删除与dom插入两个操作, 而不是一次move(insertBefore), 因为这种业务并不多见.
大多数情况下,我们只需要diff同一子节点下面的元素变化情况, 比如:
<div>
<h1>h1</h1>
<h2>h2</h2>
<h3>h3</h3>
</div>
更新成
<div>
<h2>h2</h2>
<p>p</p>
<h3>new h3</h3>
<h1>h1</h1>
</div>
react是如何做到的呢?
最外层的div标签一致, 然后对比div的children.
- 循环第一个新节点h2和老节点h1, 发现节点数据类型不同, 删除掉h1, 插入h2.
- 同理, p不等于h2, 删除h2, 插入p.
- 继续, h3等于h3, 更新h3的内容new h3.
- 最后, 发现之前没有h1, 插入h1.
这就是一个最简单的dom-diff结果. 真实的dom-diff比这个复杂一点点, 原理大体相似, 我们先做个实验, 来验证下流程.
-
经过判断和操作两个步骤.
-
特殊说明
- 如果一个有key, 一个没有key, 仍然是新增节点. 比如新节点没有key, 老节点有key, 仍然执行插入操作.
- 如果能找到key, 但是key的顺序不一样, 则使用key去老的children按照顺序遍历, 当老的children顺序遍历完, 所有新节点全部重新插入. 比如abcdef改成了bcfdea, 则bcf在老节点都能找到, 而f已经是老节点最后一个节点, 所以之后的节点都是重新插入.
- 以数组范围的作用域, 计算结果
3. 层次
3.1 展示层
用户声明创建虚拟组件以及渲染虚拟组件.
3.2 Virtual组件层
react的绝大多数代码, 包括虚拟组件的创建, 渲染和更新流程dom-diff.
3.3 基础功能
所有公共代码, 提供底层功能.
整体架构
- ReactMount: 顾名思义, 负责react组件的渲染
- ReactDOM: 用来创建native组件
- ReactComponent: react虚拟组件
- ReactOwnerReactCurrentOwner: 父子组件指针
- ReactDOMIDOperations: 真实的dom操作
- ReactMuticalChildren: dom-diff的实现
- 功能层(黄色部分): 为react业务提供基础功能, 第六章会详细讲解
模块调用关系以及流程图
- 1.配置声明: 用户代码
- 2.组件实例化: ReactComponsiteComponent.createClass
- 3.组件渲染: ReactMount.renderComponent
- 4.处理事件回调: renderComponent -> ReactMount.prepareToLevelEvents
- 5.挂载事件: ReactNativeComponent._updateDOMProperties -> ReactEvent.putListener
- 6.界面更新: ReactComposite.setState | ReactComponsite.reveiveProps
- 7.dom-diff: receiveProps -> ReactMutiChild.updateMultiChild
- 8.组件渲染或更新: ReactDOMIDOperations -> DOMChildrenOperations
转载于:https://www.yuque.com/ant-h5/sourcecode/pizqme
作者: ant-h5, 蚂蚁金服前端团队
转载方式: 手打
转载者: lemon-Xu