1. JSX到JavaScript的转换
<div id="div" key="key">
<span>1</span>
<span>2</span>
</div>
React.createElement(
"div", // 大写开头会当做原生dom标签的字符串,而组件使用大写开头时,这里会成为变量引用
{ id: "div", key: "key" },
React.createElement("span", null, "1"),
React.createElement("span", null, "2")
);
createElement(type, config, children) {
// 从用户传入的 config 中把4种内置的属性 key、ref、self、source 单独挑出来,config 中剩余的参数作为用户的 props。
// 然后返回使用 ReactElement 创建的 element 对象。
return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );
}
2. ReactElement
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, // 可能是原生dom标签字符串如'span',也可能是一个class类(class组件),还可能是一个function(函数式组件),也可能是 forwardRef 返回的对象,或者 symbol 标记等
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
if (__DEV__) {
// 略
}
return element;
};
React.createElement(
"div",
{ id: "div", key: "key" },
React.createElement("span", null, "1"),
React.createElement("span", null, "2")
);
// 于是最初的 jsx 经过 React.createElement() 后成为了下面的对象树,
// 也是函数式组件返回的东西,也是 class 组件组件 render() 方法返回的东西
element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: {
userProps1: userProps1,
// ... 用户其他的自定义props
children: [
{
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
},
{
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
}
]
},
_owner: owner,
};
3. 基类 React.Component
Component 类可能不是想象中那样用于渲染子组件什么的,只是做一些绑定工作:
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
// PureComponent 继承了 Component
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
// pureComponentPrototype.isPureReactComponent = true;
// 常规的 setState,这里只是入队列
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');
};
// 可主动强制更新,这里也只是入队列
// enqueueForceUpdate 内部实现了 ”重载”
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
4. createRef & ref
ref 用于获取 dom 节点或者 class component 的实例。
有三种用法,第一种 string 方法不推荐使用,后面在 React 17 中应该会被废弃:
import React from 'react'
export default class RefDemo extends React.Component {
constructor() {
super()
this.objRef = React.createRef()
// { current: null }
}
componentDidMount() {
setTimeout(() => {
this.refs.stringRef.textContent = 'string ref got'
this.methodRef.textContent = 'method ref got'
this.objRef.current.textContent = 'obj ref got'
}, 1000)
}
render() {
return (
<>
<p ref="stringRef">span1</p>
<p ref={ele => (this.methodRef = ele)}>span3</p>
<p ref={this.objRef}>span3</p>
</>
)
}
}
// export default () => {
// return <div>Ref</div>
// }
Object.seal() 密封,阻止目标对象上增删属性,并且关闭所有属性的 configurable,但仍可以修改现有的、是 writable 的属性。
Object.freeze() 冻结,比密封更严格,会将 writable 也关闭,意味着现有属性“不可以”修改。但如果属性本身是个引用类型,比如 const object = {a: 1, b: []},那么即使冻结 object 后,object.b.push(666) 也是可以的。
另外,冻结也会冻结目标对象上的 prototype 原型对象。
createRef 源码:
import type {RefObject} from 'shared/ReactTypes';
// an immutable object with a single mutable value
export function createRef(): RefObject {
const refObject = {
current: null,
};
if (__DEV__) {
Object.seal(refObject);
}
return refObject;
}
5. forwardRef
字面意思,转发 ref ?为什么 ref 需要转发,不能像上面那样直接使用?
dom 节点或者 class component,是可以由 react 在组件渲染之后把实例绑定(或提供)至我们指定的“容器”中,之后我们就可以从“容器”中引用刚才的实例,进行想要的操作。
React.createRef() 返回的就是 { current: null },这个对象就可以理解为一个“容器”,我们以在 jsx 上声明的方式,提供给 react,渲染之后返回我们想要的实例引用。
image然而 function 函数式组件并没有实例,就是个函数。所以外部用户无差别地尝试为组件提供 ref “容器”希望回头能拿到实例时,如果遇到 function 组件,则会失败、报错。而这种报错可以通过转发 ref 来避免,因为 function 组件没有所谓的实例,但内部至少返回了 dom 或者另外的 class 组件吧,所以把 ref 转发给它们即可。
即外部用户最终拿到的实例引用,其实是函数式组件内层的实例。
自己的组件可能知道报错,不会去直接试图获取 function 组件的 ref,但如果是作为第三方组件库提供给其他 react 用户来调用,则要使用 forwardRef 来转发用户的诉求,并实现他们。
import React from 'react';
class App extends React.Component {
render() {
return <div>div</div>;
}
}
const TargetComponent = React.forwardRef((props, ref) => (
<input type="text" ref={ref}/>
));
export default class Comp extends React.Component {
constructor() {
super();
this.ref = React.createRef();
this.ref2 = React.createRef();
}
componentDidMount() {
// 虽然还是声明在 TargetComponent 函数组件上,但最终拿到了有效的实例引用,即内部的 dom 节点
this.ref.current.value = 'ref get input';
console.log('this.ref', this.ref);
console.log('this.ref2', this.ref2);
}
render() {
return <>
<TargetComponent ref={this.ref}/>
<App ref={this.ref2} />
</>;
}
}
React.forwardRef() 源码:
export default function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
if (__DEV__) {
// 用户不按规定的使用会抛出异常信息
}
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
}
可以看到向 forwardRef 传入的 render 函数至少没在这里被调用,只是用对象包了一层,并增加了一个 $$typeof
属性,值是个 symbol。所以上面例子中声明实际等价于:
const TargetComponent = {
$$typeof: REACT_FORWARD_REF_TYPE,
render: (props, ref) => (
<input type="text" ref={ref}/>
)
};
// 经过 React.createElement() 创建出的 element:
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
type: TargetComponent,
key: null,
ref: React.createRef(),
props: {},
// Record the component responsible for creating this element.
_owner: owner,
};
6. Context
使用 context 也有两种方式,childContextType 和 createContext。
childContextType 是老的方式,在将来的 React 17 中应该会被废弃,所以优先使用 createContext。
import React from 'react'
const MyContext = React.createContext('default');
const { Provider, Consumer } = MyContext;
class Parent extends React.Component {
state = {
newContext: '123',
};
render() {
return (
<>
<div>
<label>newContext:</label>
<input
type="text"
value={this.state.newContext}
onChange={e => this.setState({ newContext: e.target.value })}
/>
</div>
<Provider value={this.state.newContext}>{this.props.children}</Provider>
</>
)
}
}
class Parent2 extends React.Component {
render() {
return this.props.children
}
}
function Child1(props, context) {
console.log(MyContext);
console.log(context);
return <Consumer>{value => <p>newContext: {value}</p>}</Consumer>
}
export default () => (
<Parent>
<Parent2>
<Child1 />
</Parent2>
</Parent>
);
React.createContext() 源码:
返回的 context 对象中有 $$typeof: REACT_CONTEXT_TYPE
,且有 Provider 和 Consumer,Provider 只是用对象包了一下原 context,添加了 $$typeof: REACT_PROVIDER_TYPE
属性;而 Consumer 压根就是引用原 context。有点俄罗斯套娃的感觉,能想到的就是 $$typeof
会作为 react 更新时判断不同类型的依据。而套娃的操作,可能就是为了方便操作 context,引来引去总能找到那个 context。
export function createContext<T>(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
if (calculateChangedBits === undefined) {
calculateChangedBits = null;
} else {
if (__DEV__) {
// 略
);
}
}
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// These are circular
Provider: (null: any),
Consumer: (null: any),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
if (__DEV__) {
// 异常处理
} else {
context.Consumer = context;
}
return context;
}
打印 React.createContext() 返回的 context 对象,验证套娃操作:
image
7. ConcurrentMode
在 React 16.6 提出,让 react 整体渲染过程可以根据优先级排列,可以任务调度、可以中断渲染过程等。来提高渲染性能,减少页面卡顿。
flushSync 使用优先级最高的方式进行更新,用来提高 this.setState 优先级。
使用 ConcurrentMode 包裹的组件都是低优先级的,所以为了演示高低优先级带来的区别感受,对比使用 flushSync。
例子中使用 flushSync 时,setState 优先级最高,基本是立即更新,这也导致动画卡顿明显,因为 200ms setState 间隔太快了,可能还没来得及渲染完,又要更新。因此使用 ConcurrentMode 把更新的优先级降低,从而不会频繁更新动画,显得流畅许多。
import React, { ConcurrentMode } from 'react'
import { flushSync } from 'react-dom'
import './index.css'
class Parent extends React.Component {
state = {
async: true,
num: 1,
length: 20000,
}
componentDidMount() {
this.interval = setInterval(() => {
this.updateNum()
}, 200)
}
componentWillUnmount() {
// 别忘了清除interval
if (this.interval) {
clearInterval(this.interval)
}
}
updateNum() {
const newNum = this.state.num === 3 ? 0 : this.state.num + 1
if (this.state.async) {
this.setState({
num: newNum,
})
} else {
flushSync(() => {
this.setState({
num: newNum,
})
})
}
}
render() {
const children = []
const { length, num, async } = this.state
for (let i = 0; i < length; i++) {
children.push(
<div className="item" key={i}>
{num}
</div>,
)
}
return (
<div className="main">
async:{' '}
<input
type="checkbox"
checked={async}
onChange={() => flushSync(() => this.setState({ async: !async }))}
/>
<div className="wrapper">{children}</div>
</div>
)
}
}
export default () => (
<ConcurrentMode>
<Parent />
</ConcurrentMode>
)
@keyframes slide {
0% {
margin-left: 0;
/* transform: translateX(0); */
}
50% {
margin-left: 200px;
/* transform: translateX(200px); */
}
100% {
margin-left: 0;
/* transform: translateX(0); */
}
}
.wrapper {
width: 400px;
animation-duration: 3s;
animation-name: slide;
animation-iteration-count: infinite;
display: flex;
flex-wrap: wrap;
background: red;
}
.item {
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: 1px solid #aaa;
}
React.ConcurrentMode 源码:
没错,源码就是一个 Symbol 符号,显示 react 内部会判断该标记然后做些什么。
React.ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;
不难想象,经过 createElement() 创建出的 reactElement 树节点应该会是这样:
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: REACT_CONCURRENT_MODE_TYPE, // 看这里
key: null,
ref: null,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
8. Suspense
Suspense 是一种在组件所依赖的数据尚未加载 ok 时,负责和 react 进行沟通,展示中间态的机制。
react 将会等待数据加载完成然后进行 UI 更新。
传统做法是在组件第一次挂载之后,即 componentDidMount() 或 useEffect() 中加载数据。即:
- Start fetching
- Finish fetching(然后调用 setState)
- Start rendering(再次渲染)
而 Suspense 也是先获取数据,(而且可以比传统做法更早一步,在第一次渲染之前),接着立马就开始第一次渲染(甚至在网络请求被实际发出前),遇到悬而未决即数据尚未获取,则挂起(suspends)该组件,跳过,然后继续渲染 element 树中其他的组件,如果又遇到还没搞定的 Suspense,则继续挂起并跳过。
- Start fetching
- Start rendering
- Finish fetching
Suspense 具体使用时,有一个“边界”概念,只有当一个 Suspense 内部的所有“挂起”都落地后,Suspense 才会停止展示 fallback 中间态,然后一并展示内部的 UI,因此可以通过合理增添 Suspense 边界来控制这种粒度。
// 用于懒加载的组件:
import React from 'react'
export default () => <p>Lazy Comp</p>
import React, { Suspense, lazy } from 'react'
const LazyComp = lazy(() => import('./lazy.js'))
let data = ''
let promise = ''
function requestData() {
if (data) return data
if (promise) throw promise
promise = new Promise(resolve => {
setTimeout(() => {
data = 'Data resolved'
resolve()
}, 2000)
})
throw promise
}
function SuspenseComp() {
const data = requestData() // 数据尚未加载完成时,该组件的渲染会被挂起。
return <p>{data}</p>
}
export default () => (
<Suspense fallback="loading data">
<SuspenseComp />
<LazyComp />
</Suspense>
)
Suspense 源码:
Suspense: REACT_SUSPENSE_TYPE // 又是一个 symbol 常量
lazy 源码:
_ctor
就是调用 lazy() 时传入的函数,该函数应返回一个 Thenable(具有then方法的对象)。react 渲染到该组件时,会调用 _ctor
函数。
_status
用于记录当前 Thenable 的状态, -1
代表尚未解决,对应到 promise 中也就是 pending 状态。
_result
用于存放 resolve 后的结果。
import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
return {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};
}
另外,关于 Suspense 能解决异步竞态问题的理解:
异步请求带来的竞态问题,本质是因为异步请求和 React 分别处于各自的生命周期,二者并未相互同步、一一对应。往往需要等待一段时间,在数据返回之后才去调用 setState,如果快速操作 UI,多次发送相同请求时,由于异步请求时间的不确定性,可能第一条请求反而比第二条同样的请求,响应的更慢,这将导致页面展示了“旧”的数据,带来了混乱。
而使用 Suspense 时,是在UI触发之后,立即调用 setState 尝试去更新数据,如果数据还没返回,则 Suspense 会挂起(suspend)并展现 fallback 中间态。但这个等待正确数据的时间管理工作,已经交给 Suspense 内部去处理了,setState 的使命已经立即完成。暂时理解为脏活累活交给 Suspense 自行消化。
Suspense 结合 useTransition
使用更佳:
This scenario (Receded → Skeleton → Complete) is the default one. However, the Receded state is not very pleasant because it “hides” existing information. This is why React lets us opt into a different sequence (Pending → Skeleton → Complete) with
useTransition
.
When weuseTransition
, React will let us “stay” on the previous screen — and show a progress indicator there. We call that a Pending state. It feels much better than the Receded state because none of our existing content disappears, and the page stays interactive.
9. Hooks
hooks 用于 function 函数式组件,内部没有 class component 中那样用来维持内部状态的 this 对象,也没有典型的生命周期方法。
一个非常简单的例子,hooks 是一个大块的内容,这里不表。
/**
* 必须要react和react-dom 16.7以上
*/
import React, { useState, useEffect } from 'react';
export default () => {
const [name, setName] = useState('jokcy');
useEffect(() => {
console.log('component update');
return () => {
console.log('unbind');
};
}, []);
return (
<>
<p>My Name is: {name}</p>
<input type="text" value={name} onChange={e => setName(e.target.value)}/>
</>
);
}
useState 源码:
useEffect 等 use* 类似,都是在调用 dispatcher 上的方法。后期会再深入研究 hooks 内层的源码:
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
// 局部
function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
invariant(
dispatcher !== null,
'Hooks can only be called inside the body of a function component.',
);
return dispatcher;
}
/**
* Keeps track of the current owner.
*
* The current owner is the component who should own any components that are
* currently being constructed.
*/
const ReactCurrentOwner = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Fiber), // 代表当前正在被构建的组件实例
currentDispatcher: (null: null | Dispatcher),
};
export default ReactCurrentOwner;
10. Children
Children 上的方法有:
Children: {
map,
forEach,
count,
toArray,
only,
},
React.Children.map() 中 map 可以遍历 child 并映射展开:
import React from 'react'
function ChildrenDemo(props) {
console.log(props.children)
console.log(React.Children.map(props.children, c => [c, [c, c]]))
return props.children
}
export default () => (
<ChildrenDemo>
<span>1</span>
<span>2</span>
</ChildrenDemo>
)
map 遍历 children 时有个 contextPool 对象常量池的概念,用于复用对象。以节省递归遍历 child 时多次对象创建和 GC 回收的开销。
React 这么实现主要是两个目的:
- 拆分 map 出来的数组
-
因为对 Children 的处理一般在 render 里面,所以会比较频繁,所以设置一个对象池子减少声明和 GC 的开销。
image
11. Other
-
memo
,用于 function component,功能上对标 class component 中的pureComponent
。
浅层源码中,也没多少东西,只是返回了一个带有 $$typeof
标记的对象及 type
、 compare
,memo 这种返回类似前面 createRef 的返回:
export default function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean,
) {
if (__DEV__) {
if (!isValidElementType(type)) {
warningWithoutStack(
false,
'memo: The first argument must be a component. Instead ' +
'received: %s',
type === null ? 'null' : typeof type,
);
}
}
return {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
}
-
FragElement
(简写方式是空标签<></>
)
最外层源码就是 FragElement: REACT_FRAGMENT_TYPE
。
是临时节点,因为 React 要求不能直接返回多个兄弟节点,要么“包”一层,要么返回数组。而 FragElement 就是用来“包”一层的,相比于用真实的 div 去“包”一层,FragElement 并不会实际被创建。
- cloneElement
克隆一个节点,源码基本和 createElement 一样,只是第一个参数从 type
变为了 element
,实际即要克隆的 element 对象上属性,然后把参数丢给 ReactElement() 来返回一个新的 element 对象:
/**
* Clone and return a new ReactElement using element as the starting point.
* See https://reactjs.org/docs/react-api.html#cloneelement
*/
export function cloneElement(element, config, children) {
invariant(
!(element === null || element === undefined),
'React.cloneElement(...): The argument must be a React element, but you passed %s.',
element,
);
let propName;
// Original props are copied
const props = Object.assign({}, element.props);
// Reserved names are extracted
let key = element.key;
let ref = element.ref;
// Self is preserved since the owner is preserved.
const self = element._self;
// Source is preserved since cloneElement is unlikely to be targeted by a
// transpiler, and the original source is probably a better indicator of the
// true owner.
const source = element._source;
// Owner will be preserved, unless ref is overridden
let owner = element._owner;
if (config != null) {
if (hasValidRef(config)) {
// Silently steal the ref from the parent.
ref = config.ref;
owner = ReactCurrentOwner.current;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
// Remaining properties override existing props
let defaultProps;
if (element.type && element.type.defaultProps) {
defaultProps = element.type.defaultProps;
}
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
if (config[propName] === undefined && defaultProps !== undefined) {
// Resolve default props
props[propName] = defaultProps[propName];
} else {
props[propName] = config[propName];
}
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return ReactElement(element.type, key, ref, self, source, owner, props);
}
- createFactory
如果时候 jsx 语法,而不是手动用 JS 调用 createElement 来创建 element 树的话,基本不会用到该方法,createFactory 就只是包了一层,省的每次创建同样类型的 element,都传入第一个 type 参数了:
/**
* Return a function that produces ReactElements of a given type.
* See https://reactjs.org/docs/react-api.html#createfactory
*/
export function createFactory(type) {
const factory = createElement.bind(null, type);
// Expose the type on the factory and the prototype so that it can be
// easily accessed on elements. E.g. `<Foo />.type === Foo`.
// This should not be named `constructor` since this may not be the function
// that created the element, and it may not even be a constructor.
// Legacy hook: remove it
factory.type = type;
return factory;
}
- isValidElement
判断一个对象是否是合法的 element:
/**
* Verifies the object is a ReactElement.
* See https://reactjs.org/docs/react-api.html#isvalidelement
* @param {?object} object
* @return {boolean} True if `object` is a ReactElement.
* @final
*/
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
网友评论