JSX 是 ECMAScript 一个类似 XML 的语法扩展。基本上,它只是为 React.createElement() 函数提供语法糖,从而让在我们在 JavaScript 中,使用类 HTML 模板的语法,进行页面描述。
由于JSX是javascript的一种扩展,所以这就直接决定了浏览器不能像天然支持js一样支持jsx,所以jsx需要被编译后才能被识别,这个编译工作正是由Babel来实现的
我们先来看个简单的JSX demo,看看经过babel的编译他会变成什么?
上图可看出,jsx中的每个标签都会被编译为React.crerateElement函数调用,接下来我们就要结合源码看下这个React.crerateElement函数
export function createElement(type, config, children) {
let propName; // 用来存储后面需要用到的元素属性
const props = {}; // 用来存储元素属性的键值对集合
// 以下4个属性都是React元素的属性,暂时不用管
let key = null;
let ref = null;
let self = null;
let source = null;
// config对象是传入的元素的属性
if (config != null) {
// 进来第一件事是依次对上述4个属性赋值
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
// 此处将key值字符串化
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是把config里的属性一个个地搬到props对象中(前面定义的)
for (propName in config) {
if (
// 筛选出可以提进props对象里的属性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength为当前元素的子元素个数,减去2是type和config占用的长度
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];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// 处理defaultProps
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);
}
}
}
// 最后调动ReactElement方法,传入刚处理的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
createElement方法接受3个参数:
type: 用于标识节点的类型,比如‘div’、'span’等
config:以对象形式传入,组件所有的属性都会以键值对的形式存储在config对象中
children:以对象形式传入,他记录的是组件标签之间嵌套的内容
我们根据上述代码可以将createElement 函数进行拆解:
createElement 每一步基本都是在格式化数据,说白点就是它更像是开发者和ReactElement函数调用之间的参数中介,它接受较为简单的参数,然后按照ReactElement函数入参的预期对参数进行相应的格式化,然后最终通过调用ReactElement函数来实现元素的创建 ,所以接下来我们重点来看下ReactElement函数内部源码
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE, // 它是一个常量,用来标识该对象是一个ReactElement
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 记录创造该元素的组件
_owner: owner,
};
if (__DEV__) { // 这里是对__DEV__环境下的处理,对逻辑理解没什么影响,可以先不看
element._store = {};
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
所以从上面源码中可以看出,ReactElement函数也是很简单的,它只是对参数进行了组装,组装成了element对象返回,其实这里说的element对象就是我们常提到的虚拟DOM,我们可以打印一个出来看看:
function App() {
const element =(
<div className="App">
hello jsx
</div>
);
console.log(element,'element')
return element;
}
export default App;
没错,上面的就是虚拟DOM,它就长这个吊样,那么它是怎么变成最终的真实DOM的呢?
此时不少同学应该已经能猜到了,没错就是ReactDOM.render方法
// 传入两个参数,虚拟dom, 应用容器
function render(vDom, container) {
let dom;
// 判断当前的节点是文本还是对象
if (vDom.type === 'TEXT') {
dom = document.createTextNode(vDom.props.nodeValue);
} else {
// 当前节点是一个对象
dom = document.createElement(vDom.type);
}
// 虚拟 dom 的属性,将 vDom 的除了 children 的属性都挂在到 dom 对象上
if (vDom.props) {
Object.keys(vDom.props)
// 过滤掉了 children 属性
.filter(key => key !== 'children')
// 循环剩余所有属性添加到dom上
.forEach(item => {
dom[item] = vDom.props[item];
});
}
// 通过递归调用实现子元素
if (vDom.props && vDom.props.children && vDom.props.children.length > 0) {
vDom.props.children.forEach(child => {
render(child, dom);
});
}
container.appendChild(dom);
}
const ReactDOM = {
render,
};
export default ReactDOM;