引入
首先我们先写一段jsx代码,看看会被babel转化成什么:
<div style={{ marginTop: "100px" }}>
<div>hello world</div>
<React.Fragment>
<div>hello</div>
</React.Fragment>
hello
{toLearn.map((item) => (
<div key={item}>{item}</div>
))}
{status ? <TextComponent></TextComponent> : <div>三元运算符</div>}
{renderFoot()}
{console.log(JSXComponent)}
</div>
我们可以通过babel 编译为Babel · The compiler for next generation JavaScript (babeljs.io)
/*#__PURE__*/React.createElement("div", {
style: {
marginTop: "100px"
}
}, /*#__PURE__*/React.createElement("div", null, "hello world"), /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", null, "hello")), "hello", toLearn.map(item => /*#__PURE__*/React.createElement("div", {
key: item
}, item)), status ? /*#__PURE__*/React.createElement(TextComponent, null) : /*#__PURE__*/React.createElement("div", null, "\u4E09\u5143\u8FD0\u7B97\u7B26"), renderFoot(), console.log(JSXComponent));
我们可以看到无论是组件还是标签,他都会变为react.createElement()
那么其实我们可以来看看createElement是干什么。
正文
createElement
参考:React 顶层 API – React (reactjs.org)
定义: createElement(type,props,children)
-
type: 标签名(组件名或者DOM名)- 如果是DOM标签名,那么用字符串表示
- 如果是组件名,那么直接写组件名
-
props:属性就是标签上面有没有附加的属性,例如,click,style等
例子
<h1 color=“white”> 标题 </h1>props;是一种对象形式 -
children:子元素如果这个组件他是父组件,那么他的所有子组件都将要放在children里面
那么现在我们来详细看一下createElement:react/ReactElement.js at 17.0.2 · facebook/react (github.com)
在看createElement之前,我们先来看一下reactElement,它是用来创建虚拟DOM元素的
const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
if (__DEV__) {
// The validation flag is currently mutative. We put it on
// an external backing store so that we can freeze the whole object.
// This can be replaced with a WeakMap once they are implemented in
// commonly used development environments.
element._store = {};
// To make comparing ReactElements easier for testing purposes, we make
// the validation flag non-enumerable (where possible, which should
// include every environment we run tests in), so the test framework
// ignores it.
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
// self and source are DEV only properties.
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
// Two elements created in two different places should be considered
// equal for testing purposes and therefore we hide it from enumeration.
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
大致意思:
在开发环境的时候,将_store,_self,_source设置为不可枚举的状态。
这边可以对Object.defineProperty进行一点复习Object.defineProperty() - JavaScript | MDN (mozilla.org),他就是在一个对象定义一个新的属性,并设置属性值。他添加的属性,默认情况是不可以被修改的,除非改变的他的configurable或者writable
object.freeze():防止后续代码添加或者删除对象原型的属性,大概就是,只要被冻住了,啥也干不了。但是它只对一层有效,如果一个属性的值是个对象,则这个对象中的属性是可以修改的。
const obj = {
prop: 42,
a:{
value:3
}
};
const obj1 = Object.freeze(obj);
console.log(obj)
console.log(obj1)
obj1.prop = 33;
obj.a.value = 4;
// Throws an error in strict mode
console.log(obj)
console.log(obj1)
> Object { prop: 42, a: Object { value: 3 } }
> Object { prop: 42, a: Object { value: 3 } }
> Object { prop: 42, a: Object { value: 4 } }
> Object { prop: 42, a: Object { value: 4 } }
那么现在我们看一下createElement
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
// 用于获取所有的标签属性
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
//判断是不是由标签的属性之类的
// 获取出合法的key,ref,self,source
if (config != null) {
// eslint-disable-next-line no-undef
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
// 并且将出来上面四个的属性都放到props对象里
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
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;
// 当值为1时,说明,只有一个组件或元素
// 那么直接复制到props.children就好了
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// 如果大于1,那么就说明,里面的组件不止一个,那么就用数组存起来
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// Resolve default props
// 看看这个标签有没有默认的属性,也加上去
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
// 然后暴露出去
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
主要逻辑:
-
他首先会从
config获取到self,source,key,ref这四个,然后将它们保存起来,这个是需要传给reactElement的 -
然后我们会将
config其他的属性,或者是标签的默认属性等全部都放到props对象里面。 -
然后我们会判断这个元素是不是父组件
children属性。其实判断的依据也比较好理解判断jsx被转变过来的babel的
createElement里的参数个数<3:那么说明,只有type和config参数,没有子元素==3:那么也就是,children占到了一个,那么就说明只有一个子元素,那就直接赋值过去>3:那么就说明子元素不止一个,那么props的children就需要遍历往里面传
但是react17以后,jsx将不会转变为reactElement而是从babel的package中引入新的入口,并调用。
function App() {
return <h1>Hello World</h1>;
}
会转变为:
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
这个是由编译器转换的,不需要你手动的去进行。
他转化完就是可以直接供reactDOM.render()的reactElement对象
Q:
这也就是为什么在react17前,react虽然没用到,但是也需要引入的原因
A:
因为当时jsx转换为reactElement对象的时候,需要借助
React.createElement但是react17以后,可以直接从新的入口引入,不需要借助react了。
但是官方还是建议,引入react的,以便使用react提供的Hooks或者其他导出
参考链接:
介绍全新的 JSX 转换 – React Blog (reactjs.org)
React17源码解析(2) —— jsx 转换及 React.createElement - 掘金 (juejin.cn)
最后会在协调和调度阶段,所有的react Element对象都会转变为filber对象
首先我们先来介绍一下,协调和调度
reconciliation:协调
我理解就是,协调其实就是和diff算法挂钩的
-
当
props或者state发生改变 -
那么就会调用
render()函数,生成一个VDOM树 -
然后通过diff算法来比对这两个树,并进行更新
-
最后渲染为真实的
DOM树
diff算法
同层比较,不同的节点产生不同的树,react会优先比较两个树的根节点。
-
当根节点类型不同时
那么就会触发完整的重建流程,就是重新创建一个以新的节点为根节点的树
并且会将旧的根节点以下的子节点,状态等全部删除。
例如:当前节点是
<h1>现在变为了<img>,那么就要重新构建 -
当根节点类型相同时
那么react就会保留这个DOM节点,仅对需要更改的属性进行更改
例如:
<div style={{color: 'red', fontWeight: 'bold'}} /> <div style={{color: 'green', fontWeight: 'bold'}} />react在进行更新的时候,只会去更新
color,其他都不会变
对子元素采用,递归,因为他只比较节点类型,所以在遍历的时候尽量使用key
当子元素有key的时候,react会根据key来判断
例如:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
类似于这样,他就不会将子元素属性改变,他会将2015和2016后移,在前面添加2014
schedule:调度
大概意思就是,不需要你去调用组件,react会调用,他会根据,优先级,时间来进行调用
在协调阶段,react会将react Element元素转变为fiber对象。
filber
每一个filber有都有三种关系
child:子节点return:父节点sibling:兄弟节点
大致流程:
- 首先从根
react Element开始遍历,并为他创建filber对象 - 然后遍历他的子节点,为子节点也创建
filber对象,直到到达了叶子节点 - 然后去检查有没有兄弟节点,遍历兄弟节点,然后兄弟的子节点
- 如果没有兄弟节点,返回父节点
对于类似于map便利的,他们会在外层加上fragment,map返回数组结构作为fragment的子节点
参考链接: