什么是 JSX
JSX 是 ECMAScript 一个类似 XML 的语法扩展。基本上,它只是为 React.createElement() 函数提供语法糖,从而让在我们在 JavaScript 中,使用类 HTML 模板的语法,进行页面描述。
JSX编译(babel)
由于JSX是javascript的一种扩展,所以这就直接决定了浏览器不能像天然支持js一样支持jsx,所以jsx需要被编译后才能被识别,这个编译工作正是由Babel来实现的
我们先来看个简单的JSX demo,看看经过babel的编译他会变成什么?
上图可看出,jsx中的每个标签都会被编译为React.crerateElement函数调用,接下来我们就要结合源码看下这个React.crerateElement函数
createElement源码
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;}// 处理defaultPropsif (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函数内部源码
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方法
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;
至此,从开始的JSX就转换成了真实的DOM节点了,整天流程梳理后如下: