阅读 648

进击ReactNative-徐如林-React源码解析

阅读原文
有的人可能会不理解,大前端平台化的战火为谁而燃,吾辈何以为战?
专注于移动互联网大前端致富,一直是我们最崇高的理想,而ReactNative是横亘在中间的桥头堡。
纵观行业风向,有作壁上观者,有磨刀霍霍者,有从入门到放弃者,有大刀阔斧者,但是缺乏深潜微操者。
啊哈,是时候该我出手了。
祭出“大海航术”,经过一年来不懈钻研,基于React Developer Tools研发插件,实时绘制运行时三棵树--Fiber双树Native View树React方法调用树,在上帝视角和时间旅行的引领下,冲破波诡云谲的Fiber算法迷航,日照大海现双龙。

本文主要针对ReactNative(以下简称 RN)的React.js源码进行分析,先说清楚开发者接触到的API,然后再深挖对应底层实现逻辑,最后找找微操的空间。如果有对RN不太熟悉的朋友,建议看一下《进击ReactNative-疾如风》热热身,该文从“原理+实践,现学现做”的角度手写石器时代RN,粗线条描述跨平台套路,迂回包抄,相对比较轻松!本文则正面刚React源码,略显烧脑。

话说,做大事,就要用大斧头。先耍耍阿里“三板斧”撼动一下。

定目标

传道(攻坚方法论)

近几年的移动互联网北漂生涯,给我结结实实的上了一课:人生,除了发财,就是不断探索、抽象、践行、强化自己的方法论、稳固自己的价值观,过程呈螺旋式上升,只要把握住总结复盘的秘诀,成长就很快乐。

我攻坚RN的原动力,就是借假修真。平台化技术最终王者也许花落Flutter或者小程序(还有很多人在纠结到底哪家强,耽误了学习,其实这好比考清华还是考北大,Top2高校有那么难选么,真正难选的是Top3高校),但这不重要,我能举一,必能反三,这就是霸道。我旨在强化出一套王者无界的方法论,如何从零将RN技能练到比肩高阶Android的熟练度,并且同样适用于进击Flutter和小程序。

授业(懂算法)

现在市面上高水准的RN解析文章太少了(老外写的硬核文章居多),而且大多停留在理论层面,只给出算法理论和源码片段,难以深入微操,只能作者说啥就是啥,反正不明觉厉。也罢,自力更生啃源码必须提上日程。

我始终相信,只有源码才是唯一的真相,不二的注释,思想的火花,王者的农药。

事实上,终于在眼泪中明白并验证了这一点,源码大法好啊,得到的比想要的多得多(贫穷限制了我的想象)。往小的说,技术成长(自嗨)。往大的说,核心竞争力(钱)。

本文和你分享的是如何通过先进生产力相对轻松地看懂代码,区别于呆板的流水式英文阅读,挑战一下:

  1. 承上(用户态--上层API怎么用)
    • 组件中方法(constructor、setState、forceUpdate、render)的作用是什么?
    • 生命周期调用时机是什么?
    • 子组件变化,父组件和兄弟组件是否会刷新?state和props变化,有什么不一样?
    • PureComponent比Component好在哪里,怎么能做得更好?
    • 最佳实践(JSX不创建临时函数,Immutable,性能优化)?
  2. 启下(内核态--底层原理怎么玩)
    • 各种概念的含义,对应数据结构是什么?
    • 深入浅出Fiber双树算法?
    • Diff算法在哪?
    • Native操作指令从哪来?

解惑(考考你)

聪明的童靴往往都会有一些亟需亲自操刀的疑问,我也不能免俗。问题有了,那满意的答案呢?

组件

  1. 明明只写了几个组件,通过react-devtools看到的却是一堆布局,而且还有Context.Consumer,这些啥时候冒出来的,干啥的?
  2. React组件和Native View看起来不是一一对应的,那么映射关系是什么?
  3. 组件普通API调用时机、作用和最佳实践?
// 组件类
class Component<P, S> {
	// 变量
	props;
	state;
	// 方法
	constructor(props, context);
	setState(state, callback): void;
	forceUpdate(callBack): void;
	render(): ReactNode;
}

复制代码

生命周期

  1. 区分哪些方法只会调用一次,哪些可能会调用多次?哪些方法中能使用setState,哪些不能?
  2. 区分每个方法调用条件,是props改变还是state,是初始化,更新还是都有?
  3. React16.3开始废弃和新增的方法是哪些,补位策略是什么?废弃方法现在还能不能用,新旧方法混用又怎样?
  4. 组件生命周期API调用时机、作用和最佳实践?
// 静态生命周期
interface StaticLifecycle {
    getDerivedStateFromProps?: GetDerivedStateFromProps;
}
// 新生命周期
interface NewLifecycle<P, S, SS> {
    getSnapshotBeforeUpdate?(prevProps, prevState): SS | null;
    componentDidUpdate?(prevProps, prevState, snapshot): void;
}
// 废弃生命周期
interface DeprecatedLifecycle<P, S> {
    componentWillMount?(): void;
    UNSAFE_componentWillMount?(): void;
    componentWillReceiveProps?(nextProps, nextContext): void;
    UNSAFE_componentWillReceiveProps?(nextProps, nextContext): void;
    componentWillUpdate?(nextProps, nextState, nextContext): void;
    UNSAFE_componentWillUpdate?(nextProps, nextState, nextContext): void;
}
// 组件生命周期(继承新和废弃生命周期)
interface ComponentLifecycle<P, S, SS> extends NewLifecycle<P, S, SS>, DeprecatedLifecycle<P, S> {
    componentDidMount?(): void;
    shouldComponentUpdate?(nextProps, nextState, nextContext): boolean;
    componentWillUnmount?(): void;
    componentDidCatch?(error, errorInfo): void;
}
复制代码

数据结构

  1. 区分Element、Instance、DOM、Component、Fiber的不同含义以及之间关系?
  2. Fiber节点数据结构中各属性含义?

Virtual DOM

  1. Virtual DOM遇到了哪些假问题,又解决了哪些真问题?
  2. React有棵DOM树,树在哪,怎么看,怎么操作对应Native View树?

Diff算法

  1. Diff算法的策略是什么,能得出哪些最佳实践?
  2. 都说React有个Diff算法(Tree Diff 分层求异;Component Diff 同类同树,异类异树;Element Diff 增删移复用key),代码在哪里,怎么比较的?

原理

  1. React高效在哪?怎么做到的?
  2. React工作流程?
  3. 如何关联Native自定义组件?
  4. Fiber双树是啥?凭什么这么牛?

追过程

学习

我们不是一个人在战斗(想发财),切忌闭门造车,只有集思广益,站在巨人的肩膀上才能事半功倍。

网上一顿关键字索引,找点时间,给点耐心,泛读 + 精读数十篇后,你的感觉才能慢慢滴上来。

本着坚定看多,数量堆死力量,经过不间断的阅读输出,姿势见涨,比方说通过XMind自由缩放源码地图帮助理解、手写RN寻求理论加实践、抽象伪代码表述助力说清楚等。

硬核带货时间,安利一下我的博客主页微信朋友圈,我会阶段性将看到的ReactNative优秀文章汇总起来。发盆友圈,我是认真的,停是不可能停下来的,天天上班天天发。欢迎相互切磋,共同进步。

Fiber架构里程碑

**Why:**一路狂奔式地更新,无暇处理用户响应,引发界面咔咔咔。

**What:**Fiber(纤维),是比线程控制更精密的并发处理机制。支持更新过程碎片化,化整为零,允许紧急任务插队,可中断恢复。本质上,它还是一个工具,用来帮助开发者操纵DOM API,从而构建出页面。

**How Much:**纵享丝滑。

**硬核资料:**业界大牛Lin Clark在2017 React大会的演讲Lin Clark - A Cartoon Intro to Fiber - React Conf 2017。这个内容太棒啦,墙裂建议大家看一看(没有字幕,英文流利的同学可以挑战一下,或者像我一样发挥暴躁的想象力假装听懂了)。网上大部分Fiber算法分析都引用了她的卡通图

术语

Component:组件,是可复用的小的代码片段,它们返回要在页面中渲染的React元素。分为类组件(继承Component的普通组件和继承PureComponent的纯组件)和函数式组件(直接返回Element的函数)。

// 普通组件
class App extends React.Component {
    render() {
        return <Text style={{color: 'black'}}>{'点击数0'}</Text>;
    }
}
复制代码
// 纯组件
class App extends React.PureComponent {
    render() {
        return <Text style={{color: 'black'}}>{'点击数0'}</Text>;
    }
}
复制代码
// 函数式组件
const App = function () {
    return <Text style={{color: 'black'}}>{'点击数0'}</Text>;
}
复制代码

JSX:是类Html标签式写法转化为纯对象Element函数调用式写法的语法糖。Babel会把JSX转译成一个名为 React.createElement 函数调用.

class App extends React.Component {
	render() {
		// Babel转换JSX后
		return React.createElement(
			// 类型type
			{$$typeof: Symbol(react.forward_ref), displayName: "Text", propTypes: {…}, render: ƒ},
			// 属性props
			{style: {color: "black"}, __source: {…}},
			// 子节点children
			"点击数0"
		);
	}
}
复制代码

Instance:组件实例,组件类实例化的结果,ref指向组件实例(函数式组件不能实例化)。在生成Fiber节点时会调用new Component()创建。

// App
{
	forceUpdate: ƒ (),
	isReactComponent: ƒ (),
	setState: ƒ (),
	componentDidMount: ƒ (),
	componentWillUnmount: ƒ (),
	constructor: ƒ App(props),
	isMounted: (...),
	render: ƒ (),
	replaceState: (...),
	__proto__: Component
}
复制代码

Element:元素,描述了你在屏幕上想看到的内容。是DOM节点的一种纯对象描述,即虚拟DOM,对应组件render方法主要返回值。详见React.createElement

// App
{
	// React Element唯一标识
	$$typeof: Symbol(react.element),
	// 开发者指定唯一标识,用于复用
	key: null,
	// 属性
	props: {rootTag: 241},
	// 引用
	ref: null,
	// 类型
	type: ƒ App(props),
	_owner: null,
	_store: {validated: true},
	_self: null,
	_source: {fileName: "/Users/shengshuqiang/dream/AdvanceOnReactNative/Aw…native/Libraries/ReactNative/renderApplication.js", lineNumber: 38},
	__proto__: Object
}
// Text
{
	$$typeof: Symbol(react.element),
	key: null,
	props: {style: {color: "black"}, children: "点击数0"},
	ref: null,
	type: {$$typeof: Symbol(react.forward_ref), displayName: "Text", propTypes: {…}, render: ƒ},
	_owner: FiberNode {id: 11, tag: 1, key: null, elementType: ƒ, type: ƒ, …},
	_store: {validated: false},
	_self: null,
	_source: {fileName: "/Users/shengshuqiang/dream/AdvanceOnReactNative/AwesomeProject/App.js", lineNumber: 213},
	__proto__: Object
}
复制代码

FiberNode:碎片化更新中可操作的细粒度节点,用于存储中间态计算结果,为“可紧急插队、可中断恢复”的页面刷新提供技术支持。详见ReactNative.createFiberFromElement

// FiberNode
{
	actualDuration: 175.7499999985157,
	actualStartTime: 9793.884999999136,
	// 候补树,在调用render或setState后,会克隆出一个镜像fiber,diff产生出的变化会标记在镜像fiber上。而alternate就是链接当前fiber tree和镜像fiber tree, 用于断点恢复
	alternate: null,
	// 第一个子节点
	child: FiberNode {id: 12, tag: 11, key: null, elementType: {…}, type: {…}, …},
	// TODO
	childExpirationTime: 0,
	contextDependencies: null,
	// 副作用,增删改操作。Placement=2;Update=4;PlacementAndUpdate=6;Deletion=8;
	effectTag: 5,
	// 描述了它对应的组件。对于复合组件,类型是函数或类组件本身。对于宿主组件(div,span等),类型是字符串。定义此 Fiber 节点的函数或类。对于类组件,它指向构造函数,对于 DOM 元素,它指定 HTML 标记。我经常使用这个字段来理解 Fiber 节点与哪个元素相关。
	// ClassComponent对应为函数,如APPContainer()。ForwardRef、ContextConsumer、ContextProvider对应为对象,如{$$typeof: Symbol(react.forward_ref), render: ƒ, displayName: "View"}。HostComponent对应为字符串,如“RCTView”。HostText对应为null。
	elementType: ƒ App(props),
	// TODO
	expirationTime: 0,
	// 用来保存中断前后 effect 的状态,用户中断后恢复之前的操作。这个意思还是很迷糊的,因为 Fiber 使用了可中断的架构
	firstEffect: FiberNode {id: 13, tag: 1, key: null, elementType: ƒ, type: ƒ, …},
	// 我添加的Fiber节点唯一标识(采用id自增生成),用于生成Fiber双树
	id: 11,
	index: 0,
	// 复用标识
	key: null,
	// 参考firstEffect
	lastEffect: FiberNode {id: 13, tag: 1, key: null, elementType: ƒ, type: ƒ, …},
	// 在前一个渲染中用于创建输出的 Fiber 的 props
	memoizedProps: {rootTag: 191},
	// 用于创建输出的 Fiber 状态。处理更新时,它会反映当前在屏幕上呈现的状态
	memoizedState: null,
	mode: 4,
	// workInProgress tree上每个节点都有一个effect list,用来存放需要更新的内容。此节点更新完毕会向子节点或邻近节点合并 effect list
	nextEffect: FiberNode {id: 10, tag: 5, key: null, elementType: "RCTView", type: "RCTView", …},
	// props是函数的参数。一个 fiber 的pendingProps在执行开始时设置,并在结束时设置memoizedProps。已从 React 元素中的新数据更新并且需要应用于子组件或 DOM 元素的 props
	pendingProps: {rootTag: 191},
	ref: null,
	// 父节点
	return: FiberNode {id: 10, tag: 5, key: null, elementType: "RCTView", type: "RCTView", …},
	selfBaseDuration: 28.63000000070315,
	// 兄弟节点
	sibling: null,
	// 保存组件的类实例、DOM 节点或与 Fiber 节点关联的其他 React 元素类型的引用。总的来说,我们可以认为该属性用于保持与一个 Fiber 节点相关联的局部状态。(HostRoot对应{containerInfo};ClassComponent对应为new的函数对象实例;HostComponent对应为ReactNativeFiberHostComponent,包含_children和_nativeTag;HostText对应为nativeTag)
	stateNode: hookClazz {props: {…}, context: {…}, refs: {…}, updater: {…}, _reactInternalFiber: FiberNode, …},
	// 它在协调算法中用于确定需要完成的工作。如前所述,工作取决于React元素的类型
	tag: 1,
	treeBaseDuration: 155.36499999871012,
	// 同elementType
	type: ƒ App(props),
	// state更新队列。状态更新、回调和 DOM 更新的队列
	updateQueue: null,
	_debugID: 12,
	_debugIsCurrentlyTiming: false,
	_debugOwner: null,
	_debugSource: {fileName: "/Users/shengshuqiang/dream/AdvanceOnReactNative/Aw…native/Libraries/ReactNative/renderApplication.js", lineNumber: 38},
	__proto__: Object
}
复制代码

DOM:文档对象模型(Document Object Model),简单说就是界面控件树(对应Html是DOM树,对应Native是View树)的节点。

UIManager.createView	[3,"RCTRawText",1,{"text":"点击数0"}]
UIManager.createView	[5,"RCTText",1,{"ellipsizeMode":"tail","allowFontScaling":true,"accessible":true,"color":-16777216}]
UIManager.setChildren	[5,[3]]
UIManager.createView	[7,"RCTView",1,{"flex":1,"pointerEvents":"box-none","collapsable":true}]
UIManager.setChildren	[7,[5]]
UIManager.createView	[9,"RCTView",1,{"pointerEvents":"box-none","flex":1}]
UIManager.setChildren	[9,[7]]
UIManager.setChildren	[1,[9]]
复制代码

运行(Playground)

搭一个自己专属的游乐场--本地可运行环境(开发平台macOS,目标平台Android)。

  1. 安装软件:Webstorm(前端开发环境)、AndroidStudio(Android开发环境,送Android模拟器)。
  2. 安装依赖:安装Xcode(iOS开发环境,送iPhone模拟器)就顺带解决了。
  3. 使用 React Native 命令行工具来创建一个名为"AwesomeProject"的新项目:react-native init AwesomeProject
  4. 欧了,简单Demo(页面一个红色按钮,初始显示点击数0,点击切换为“汽车”图标)测试一下。该Demo主要用于观察初始渲染和用户点击渲染。
  5. 更多配置详见React Native 中文网-搭建开发环境

源码

我们来读源码(16.8.3 react,0.59.8 react-native)吧!

  • RN上层JS代码主要实现在ReactNativeRenderer-dev.js这一个文件,代码行数2W出头(区区2W,好像压力也没辣么大)。
  • react.development.js:纯JS侧React相关定义和简单实现。
  • react.d.ts:接口定义,详见本地目录/Applications/WebStorm.app/Contents/plugins/JavaScriptLanguage/jsLanguageServicesImpl/external/react.d.ts。
#  源码目录/Users/shengshuqiang/dream/AdvanceOnReactNative/AwesomeProject/node_modules
.
├── react
│   └── cjs
│       └── react.development.js # 纯JS侧React相关定义和简单实现
└── react-native
    ├── LICENSE
    └── Libraries
        ├── Components # 官方提供的各种组件,如View、ScrollView、Touchable等
        └── Renderer
            └── oss
                ├── GreateNavigationArt.js # “大海航术”核心实现,主要hook调用,打印调用栈日志和dump Fiber双树信息,约600行
                └── ReactNativeRenderer-dev.js # ReactNative上层JS代码核心实现,约2W行

复制代码

迷航

我的读码套路:读博客➢跑Demo➢打日志➢打断点➢大猜想➢证因果➢得结论。

  1. 开局大量读技术博客,建立一个知识地图。
  2. 写个Demo先嗨一把,敲敲打打找点感觉。
  3. 为了避免从入门到放弃,一定要先日志抬头看路,后断点埋头搬砖。稍微复杂点的算法,通常上来就是一顿大循环和深递归,没有强目标导向,只能GG。
  4. 到了尽情发挥你野兽般的想象力(悟)的时候了,Why?What?How?
  5. 带着问题找答案,追根溯源。
  6. 再回首,总结复盘。回答有啥用,能不能自圆其说,可不可以唬住不懂的人(包括我自己),是不是假装懂了。

说了这么多,我也记不住。抽象一下,这不就是在茫茫大海航行的技术么,就叫“航海术”吧。

对付简单的算法,这招基本够用,否则就真的钱难挣了。
但是,Fiber算法,忒难了。第一个回合硬着头皮看下来,只知道一堆乱七八糟的调用,混杂着各种光怪陆离的Fiber属性,而且用到了复杂的树数据结构,还是双树。

这些,小本子根本记不过来。来张我的笔记感受一下(不用细看,我也没打算讲这张图,大家看个意思),一波操作下来,差不多要2天闭关专注的投入,要是被打断了,都找不到北。

按这个套路,日志调试瞎猜,发现装不下去了,我太难了。一度跌入绝望之谷,挣扎着把源码看了三遍(毕竟指望这一波发财),仍然没什么收获,等着顿悟吧。

微光

直到那一天,我终于等到了这个变数--如果能可视化Fiber双树在运行时的状态变化,是否有望突破React技术壁垒?

脑子再活一点的我就想:“可不可以写个脚本把Fiber双树画出来”,随后的问题就是“能不能写个插件实时绘制运行时Fiber双树”,进一步“绘制实时方法调用树(看着有点像抽象语法树),有问题吗?”能有啥问题,没问题,那就干。

说到底,“海航术”通过日志和调试阅读源码的方向是没有问题的,有问题的是仅通过分析上万条日志信息,过程枯燥乏味,很难通过想象串联这么大量级的信息。如果借助工具提高生产力,可视化图像具象日志信息,那就能攻守易势。特别对于这种抽象的树形结构,没有什么比画图更通俗易懂了。

本着**DRY(Dont Repeat Yourself)**原则,一步步迭代插件。当然,过程是艰辛的,无法一蹴而就。能想到接入react-devtools插件,是因为李阳大牛推荐过该工具帮助分析Virtual DOM树,恰巧彼时团队内部也有童靴在扩展该工具。接入插件当时并没有把握,表面上是扩大战果,但也可能被拖入新的泥潭,舍本逐末。幸好运气不错,在瓶颈期通过董思文和陈卓双大牛的点拨下,灰常顺利的搞出来了。

这里必须给React Developer Tools点32个赞,这是我迄今见过最好的架构,我就一JS倔强青铜的水平,竟然看着文档能把源码跑起来(过程中编译相关小问题找公司FE大牛给解了),进一步把自己的脚本集成进去,模仿已有脚本一顿Ctrl+F、Ctrl+C、Ctrl+V就成了,延展性可见一斑,不服不行。

大海航术

海航术”的大方向(日志、调试、想象)是正确的,这个想象操作空间太大,是个非标品。“大海航术”的就大在可视化放飞想象力。

  1. React方法调用树图为主线,监控每一个方法调用,不轻易放过任何一个细节,弄清楚他是谁、从哪来、到哪去。同时以Fiber节点操作为里程碑,dump出当前Fiber树(Fiber双树图数据源),衍生出可供时间旅行的慢动作回放,便于步步为营式探索。
  2. Fiber双树图为小因果,讲清楚Fiber树的每次变化。Fiber算法的核心就是分段式操作Fiber树计算出副作用(DOM操作),然后一次提交(刷新页面)。带着问题去阅读是一种怎样的体验?
  3. Native View树图为分水岭,说明白Native View树的每次变化。Fiber算法的目标就是生成操作Native View树的一系列指令。

让我们一起欣赏一下大海航术的视觉盛宴▼:

更多详见Html Demo 页面

用户态(浅水区)

React官方文档和简单Debug能解决大部分问题(前提你得知道问题是什么),剩下的交给时间(时间是一种解药,也是一种毒药)。

组件API

组件变量/方法概念调用时机作用最佳实践
props属性使用时设置属性
this.props读取
存储父组件传递的信息1. UI组件,根据属性纯展示,没有内部逻辑
state状态setState设置
this.state读取
存储自身维护的状态1. 容器组件,纯逻辑处理,通过组合UI组件完成渲染
2. 构造函数直接this.state赋值。否则setState替代
3. 反面模式: 直接复制 prop 到 state
constructor构造函数在React组件挂载之前用于创建组件实例1. 初始化state或进行方法绑定,否则不需要实现
2. 不是props传递的唯一入口(仅初始化调用,后续props更新不调用)
3. super(props)必须在使用this.props之前调用
setState设置状态用户主动调用将更改排入队列并通知重新渲染1. 控制影响组件粒度,避免大规模刷新(性能杀手)
2. 区分哪些生命周期中不能调用setState,避免死循环
3. 仅影响组件显示状态的数据放在state里面,其他数据可用成员变量存储
4. 不总是立即更新组件,会批量推迟更新
forceUpdate强制更新用户主动调用跳过shouldComponentUpdate直接触发render1. 谨慎使用
2. 如render方法依赖于其他数据,则可调用forceUpdate强制刷新
render渲染
唯一必须实现
Diff比较前描述当前组件的颜值1. 合理通过组件进行封装,确保可读性和可维护性
2. 减少inline-function
3. 养成良好的编程习惯(可扩展性、鲁棒性、可靠性、易用性、可移植性等)

生命周期

每个组件都包含“生命周期方法”,你可以重写这些方法,以便于在运行过程中特定的阶段执行这些方法。

生命周期概念类型调用时机调用次数调用setState作用最佳实践
static getDerivedStateFromProps从属性获取状态常规方法在调用render方法之前调用(初始挂载及后续更新时都调用)多次不支持(无法持有引用)返回一个对象来更新 state(返回null则不更新任何内容)适用于state值在任何时候都取决于props
getSnapshotBeforeUpdate更新前获取快照回调新增方法在最近一次渲染输出(提交到 DOM 节点)之前调用多次支持使得组件能在发生更改之前从 DOM 中捕获一些信息不常见,可能出现在 UI 处理中(如滚动位置)
componentDidUpdate组件已更新回调新增方法在更新后会被立即调用(首次渲染和shouldComponentUpdate返回false时不会调用)多次支持当组件更新后,可以在此处对 DOM 进行操作对更新前后的 props 进行了比较判断后触发逻辑(props变化时触发网络请求)
componentWillMount
UNSAFE_componentWillMount
组件待挂载回调废弃方法在挂载之前被调用一次支持用于触发挂载前逻辑避免在此方法中引入任何副作用或订阅(改用componentDidMount)
componentWillReceiveProps
UNSAFE_componentWillReceiveProps
组件待接收属性回调废弃方法在已挂载的组件接收新的 props 之前被调用(初始渲染和setState不调用)多次支持用于触发接收属性前逻辑1. 不建议用(使用通常会出现 bug 和不一致性)
2. 更新状态以响应 prop 更改
componentWillUpdate
UNSAFE_componentWillUpdate
组件待更新回调废弃方法当组件收到新的 props 或 state 时,在渲染之前调用(初始渲染和shouldComponentUpdate返回false不会调用)多次不支持(避免循环调用)用于触发组件更新前逻辑不建议用(改用componentDidUpdate)
componentDidMount组件已挂载回调常规方法在组件挂载后(插入 DOM 树中)立即调用一次支持用于触发挂载后逻辑依赖于 DOM 节点的初始化(添加订阅、网络请求)应该放在这里
shouldComponentUpdate组件是否更新常规方法当props或state发生变化时,会在渲染执行之前被调用(首次渲染或使用forceUpdate时不会调用)多次不支持(避免循环调用)减少不必要的渲染(性能优化)1.考虑使用内置的PureComponent组件
2. 不建议深比较(性能杀手)
componentWillUnmount组件待卸载回调常规方法在组件卸载及销毁之前直接调用一次不支持(该组件将永远不会重新渲染和挂载)用于触发卸载前逻辑执行必要的清理操作(清除timer、取消网络请求、注销订阅等)
componentDidCatch子组件出错回调常规方法在子组件抛出错误后被调用多次支持用于记录错误上报错误日志

备注:

  • 新增和废弃生命周期混用时,只会知悉新的生命周期。
  • 生命周期中是调用setState的前提是:没有循环调用风险(shouldComponentUpdate和componentWillUpdate中调用会导致循环调用)、受限(必须包裹在条件语件里)条件下运行调用、有意义(componentWillUnmount调用无意义)、能调用(static getDerivedStateFromProps里面无法调用),详见React setState 实现机制及循环调用风险

内核态(深水区)

React源码解析,需要牢记:React组件是数据的函数,v = f(d)。抓住输入和输出,才能有的放矢。本次解析分为二段,初始渲染时间线(用户进入页面Fiber算法干啥类)、用户点击渲染时间线(用户点击按钮切换文本为图标,Fiber算法又干啥类)。这两个场景是所有Fiber算法行为的本源,万变不离其宗。然后再用简单伪代码回顾一下。

初始渲染时间线

初始化页面布局(里面有一堆组件,远比我们写的要多)

初始化JS2Native通信(通信主要是通过桥UIManager调用createView创建、setChildren关联(增删改)和updateView更新)

1. invoke    UIManager.createView    [3,"RCTRawText",11,{"text":"点击数0"}]
2. invoke    UIManager.createView    [5,"RCTText",11,{"ellipsizeMode":"tail","allowFontScaling":true,"accessible":true,"fontSize":30,"color":-1,"textAlignVertical":"center","textAlign":"center"}]
3. invoke    UIManager.setChildren    [5,[3]]
4. invoke    UIManager.createView    [7,"RCTView",11,{"backgroundColor":-65536,"height":150,"width":300,"accessible":true}]
5. invoke    UIManager.setChildren    [7,[5]]
6. invoke    UIManager.createView    [9,"RCTView",11,{"flex":1,"pointerEvents":"box-none","collapsable":true}]
7. invoke    UIManager.setChildren    [9,[7]]
8. 
9. invoke    UIManager.createView    [13,"RCTView",11,{"pointerEvents":"box-none","flex":1}]
10. invoke    UIManager.setChildren    [13,[9]]
11. 
12. invoke    UIManager.setChildren    [11,[13]]
复制代码

初始化Fiber树

初始化NativeView树

手机横过来看

用户点击渲染时间线

用户点击页面组件布局

用户点击JS2Native通信

1. invoke    UIManager.measure    [7,27]
1. invoke    UIManager.playTouchSound    []
2. invoke    UIManager.createView    [15,"RCTImageView",11,{"loadingIndicatorSrc":null,"defaultSrc":null,"src":[{"uri":"http://demo.sc.chinaz.com/Files/pic/icons/5918/c12.png"}],"shouldNotifyLoadEvents":false,"opacity":0.85,"overflow":"hidden","height":100,"width":100}]
3. invoke    UIManager.manageChildren    [7,[],[],[],[],[0]]
4. invoke    UIManager.manageChildren    [7,[],[],[15],[0],[]]
5. invoke    UIManager.updateView    [7,"RCTView",{"backgroundColor":-16777216}]
1. invoke    UIManager.updateView    [15,"RCTImageView",{"opacity":null}]
2. invoke    UIManager.updateView    [7,"RCTView",{"backgroundColor":-65536}]
复制代码

用户点击Fiber树

用户点击NativeView树

手机横过来看

小结

简约伪代码

Talk is cheap. Show me the code.

详见简约伪代码

方法调用图

拿结果

QA

  1. 问:明明只写了几个组件,通过React Developer Tools看到的是一堆布局,而且还有Context.Consumer,这些都是干啥的?
    答:查看View.js源码,发现里面会再次render出Context.Consumer。也就是我们写的<View></View>最终生成的树是


    同样,<Text></Text>对应


    我们写的组件其实外面会被包裹一层,比方显示yellowbox提示啥的
  2. 问:React的组件和Native看起来好像不是一一对应的,这个映射策略是什么?
    答:只有HostComponent和HostText会映射到Native View,其他类型不会,只是用于运算和记录状态。
    1. 我们通过react-devtools看到的reactdom树不是完全的。下面是react-devtools上显示的:,文本节点没有,实际最外层还有一个HostRoot节点。
    2. reactdom树中只有部分dom节点(宿主节点,对应文本和Native组件)是显示在界面上的,其他的并不展示。Fiber中的tag表示类型,创建NativeView时(createInstance和createTextInstance)的tag是组件唯一标识,从数字3开始累积2生成。
  3. 问:Element、Instance、DOM之间关系?
    答:
  4. 问:都说React有个diffing算法,这个在代码哪里,怎么比较的,文案变了会设计diff算法吗?
    答:diffing算法在reconciliation模块里面,对应函数为ChildReconciler。,文本节点和数组见reconcileSingleTextNode和reconcileChildrenArray。更多可以参考React 源码剖析系列 - 不可思议的 react diff
  5. 问:浅比较shouldComponentUpdate说的是什么,到底应该怎么用?
    答:判断组件是否更新时调用,优先调用shouldComponentUpdate方法,无该该方法是判断是否是纯组件,是则浅比较(判断对象props和state前后是否改变,只对比一级属性是否严格相等===)
  6. 问:React有棵DOM树,树在哪,怎么看,怎么操作Native的DOM树?
    答:在我扩展的插件上看。
  7. 问:setState到底干啥了?
    答:触发Fiber双树重新diff渲染,具体调用可以使用方法调用树追踪。
  8. 问:React高效在哪?
    答:基于优先级的可中断的树遍历算法,且diff算法复杂度O(n)。
  9. 问:React工作流程?
    答:文章中有。
  10. 问:如何关联Native自定义组件?
    答:这是个好问题,留给读者自行解答。
  11. 问:Fiber节点数据结构中各属性含义?
    答:
    1. return, child, sibling:

    2. key: 复用标识。
    3. tag:它在协调算法中用于确定需要完成的工作。如前所述,工作取决于React元素的类型。
    4. stateNode:保存组件的类实例、DOM 节点或与 Fiber 节点关联的其他 React 元素类型的引用。总的来说,我们可以认为该属性用于保持与一个 Fiber 节点相关联的局部状态。
    a. HostRoot对应{containerInfo}。
    b. ClassComponent对应为new的函数对象实例。
    c. HostComponent对应为ReactNativeFiberHostComponent,包含_children和_nativeTag。
    d. HostText对应为nativeTag。
    5. elementType/type: 描述了它对应的组件。对于复合组件,类型是函数或类组件本身。对于宿主组件(div,span等),类型是字符串。定义此 Fiber 节点的函数或类。对于类组件,它指向构造函数,对于 DOM 元素,它指定 HTML 标记。我经常使用这个字段来理解 Fiber 节点与哪个元素相关。
    a. ClassComponent对应为函数,如APPContainer()。
    b. ForwardRef、ContextConsumer、ContextProvider对应为对象,如{$$typeof: Symbol(react.forward_ref), render: ƒ, displayName: "View"}。
    c. HostComponent对应为字符串,如“RCTView”。
    d. HostText对应为null。e. memoizedProps:在前一个渲染中用于创建输出的 Fiber 的 props。
    f. memoizedState:用于创建输出的 Fiber 状态。处理更新时,它会反映当前在屏幕上呈现的状态。
    g. pendingProps:props是函数的参数。一个 fiber 的pendingProps在执行开始时设置,并在结束时设置memoizedProps。已从 React 元素中的新数据更新并且需要应用于子组件或 DOM 元素的 props。
    h. updateQueue: state更新队列。状态更新、回调和 DOM 更新的队列。
    i. firstEffect 、lastEffect 等玩意是用来保存中断前后 effect 的状态,用户中断后恢复之前的操作。这个意思还是很迷糊的,因为 Fiber 使用了可中断的架构。
    j. effectTag:副作用,增删改操作。
    k. alternate:在调用render或setState后,会克隆出一个镜像fiber,diff产生出的变化会标记在镜像fiber上。而alternate就是链接当前fiber tree和镜像fiber tree, 用于断点恢复。workInProgress tree上每个节点都有一个effect list,用来存放需要更新的内容。此节点更新完毕会向子节点或邻近节点合并 effect list。

高性能实践

让浏览器休息好,浏览器就能跑得更快。

留到下期。

问题定位利器

基于上面插件,同理研发。

方法钩子

我的数据映射关系怎么来的,这个插件是怎么写的,这又可以再写一篇。

长歌

  1. 永远不要只满足于世界的表象,要敢于探寻未知的可能。--《守望先锋》
  2. 天不生我李淳罡,剑道万古长如夜。剑来!--《雪中悍刀行》
  3. 在本帅眼里没有圣女,也无所谓蛊王。--《画江湖之不良人》
  4. 世间万事,风云变幻,苍黄翻覆,纵使波谲云诡,但制心一处,便无事不办,天定胜人,人定兮胜天!李淳风,霸道如何,天道又如何?我,不在乎。--《画江湖之不良人》
  5. 三界唯心,万法唯识。唯心所变,唯识所现。--《佛法》
  6. 谋逆?!哈哈哈哈哈!我陆危楼何惧谋逆叛教之说,不过从头再来罢了!--《圣焰暝影》
  7. 当其他人盲目追随真理的时候,记住,万物皆虚;当其他人被道德和法律束缚的时候,记住,万事皆允。我们躬耕于黑暗,服侍着光明。--《刺客信条》
  8. 天生万物以养人,世人犹怨天不仁。--《七杀碑》
  9. 好男儿,别父母,只为苍生不为主。-- 《红巾军军歌》

结语

感谢岳母大人和媳妇大人的默默付出,感谢士兴大佬、朝旭大神、车昊大哥、张杰大哥、思文大拿、陈卓大牛的技术支持和迷津指点。

曾经在知乎看到一个问题,“能魔改react-native源码的是什么水平的前端?”我挑战了这个水平。

Airbnb摇了摇头,说“ RN太难了”,然后倒下了。但是我们,必须,站到台前,领导大家~

谨以此文,献给(那些)曾经热爱互联网技术,并和并肩作战的伙伴们一同度过时光的人们,呈现这重逢的此刻。

参考

  1. React16源码之React Fiber架构
  2. React Fiber架构
  3. 「译」React Fiber 那些事: 深入解析新的协调算法
  4. 浅析React Diff 与 Fiber
  5. 200行代码实现简版react
  6. React 源码剖析系列 - 不可思议的 react diff
  7. React源码分析
  8. [react] React Fiber 初探
  9. Virtual DOM 的实现和 React Fiber 简介
  10. React中一个没人能解释清楚的问题——为什么要使用Virtual DOM
  11. 深度剖析:如何实现一个 Virtual DOM 算法 #13
  12. React Fiber 架构【译】
  13. React Fiber是什么
  14. 浅谈React16框架 - Fiber
  15. 如何理解 React Fiber 架构?
  16. React 16 架构研究记录(文末有彩蛋)
  17. 对React生命周期的理解
  18. [译] 图解 React
  19. [译] 图解 React Native
  20. react-devtools
文章分类
Android
文章标签