JSX与虚拟DOM

jsx是一种语法糖,经过babel 编译后生成 React.createElement(component, props, ...children) 函数。

例如

const Element = (
  <div className="title">
    Hello 
    <span style={{fontSize:"20px",color:"#f00"}}>World!</span>
  </div>
)

经过babel编译后:

const Element = React.createElement(
  'div',
  {
    className: 'title',
  },
  'Hello',
  React.createElement(
    'span',
    {
      style: {
        fontSize: '20px',
        color: '#f00',
      },
    },
    'World!'
  )
);

jsx语法经过babel编译后生成一种对象,即虚拟dom,在react中,通过render函数将虚拟dom渲染成真实的dom绑定到对应节点中

import React from 'react'
import ReactDOM from 'react-dom'

// 虚拟dom
const Element = (
  <div className="title">
    Hello 
    <span style={{fontSize:"20px",color:"#f00"}}>World!</span>
  </div>
)

Element 经过babel转换后如下:

const Element = React.createElement(
  'div',
  {
    className: 'title',
  },
  'Hello',
  React.createElement(
    'span',
    {
      style: {
        fontSize: '20px',
        color: '#f00',
      },
    },
    'World!'
  )
)
//绑定节点
const el = document.getElementById('root')

// render方法渲染到页面
ReactDOM.render(Element, el)

这里需要两个函数完成页面的渲染:createElementrender方法

createElement函数

createElement函数将babel转换过后的参数简化,返回对象type和props

/**
 * 生成虚拟DOM对象
 * @param {string} type      dom节点类型
 * @param {object} config    属性对象
 * @param {*} [children]     子数组
 *
 * @return {object} {type,props}
 */
function createElement(type,config,children) {
  let props = {};
  for(let propsName in config){
    props[propsName] = config[propsName]
  };

  let childsLen = arguments.length - 2;
  if(childsLen===1){
    props.children = [children]
  }else if(childsLen>1){
    props.children = Array.prototype.slice.call(arguments, 2)
  }

  return { type, props }
}

render函数

render函数负责将虚拟dom转化为真实dom,我们将createElement生成的虚拟dom对象传入render函数的第一个参数,结构出type值和props对象

/**
 * @param {createElement} element    虚拟DOM对象
 * @param {HTMLElement}} container    绑定的dom节点
 */
function render(element, container) {
  if (typeof element === 'string') {
    return container.appendChild(document.createTextNode(element))
  }

  let type  = element.type;
  let props  = element.props;

  if (type.isReactComponent) { // 如果是类组件
    element = new type(props).render()
    type = element.type
    props = element.props
  } else if (typeof type === 'function') { // 如果是函数组件
    element = type(props)
    type = element.type
    props = element.props
  }

  const el = document.createElement(type)
  for (let propName in props) {
    let value = props[propName]
    if (propName === 'className') {
      el.className = value
    } else if (propName === 'style') {
      let cssText = Object.keys(value).map((attr) => {
          let _attr = attr.replace(/([A-Z])/g, (a) => `-${a.toLocaleLowerCase()}`);
          return `${_attr}:${value[attr]}`
        }).join(';');

      el.style.cssText = cssText
    } else if (propName === 'children') {
      value.forEach((item) => render(item, el))
    } else {
      el.setAttribute(propName, value)
    }
  }
  return container.appendChild(el)
}

这里我们还需要判断type值是类组件还是函数组件

类组件

关于如何判断类组件,我们可以在组件继承的Component类中添加静态属性isReactComponent供render函数判断

class Component {
  static isReactComponent = true;
  constructor(props){
    this.props = props;
  }
}

类组件

let Element = React.createElement(fnElemen, { name: 'Hello', fontSize: '28px' })

class clsComp extends React.Component {
  render() {
    return React.createElement(
      'h1',
      { className: 'title' },
      this.props.name,
      React.createElement(
        'span',
        {
          style: { color: '#0f0', fontSize: this.props.fontSize }
        },
        'World'
      )
    )
  }
};

let Element = React.createElement(clsComp, { name: 'Hello', fontSize: '28px' })

ReactDOM.render(Element, document.getElementById('root'))

函数组件

function fnElemen(props){
  return React.createElement(
    'h1',
    { className: 'title' },
    props.name,
    React.createElement(
      'span',
      { style: { color: '#0f0', fontSize: props.fontSize } },
      'World'
    )
  )
}

let Element = React.createElement(fnElemen, { name: 'Hello', fontSize: '28px' })

ReactDOM.render(Element, document.getElementById('root'))

开发中的常见问题

为何必须引用React

作用域内必须引入react,否则会导致编译失败,这是因为jsx会编译为React.createElement的形式调用,所以react在jsx的作用域内必须要引入。

用户定义的组件以大写字母开头

babel在编译时会把小写字母开头的元素判定为原生DOM标签,createElement 会把它编译成字符串,例如<div>或者 <span>,所以在编写组件的时候,需要以大写字母开头,这样createElement才会把第一个变量被编译为对象

import React from 'react';

// 错误!组件应该以大写字母开头:
function hello(props) {
  // 正确!这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签
  return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
  // 错误!React 会认为 <hello /> 是一个 HTML 标签,因为它没有以大写字母开头:
  return <hello toWhat="World" />;
}

Props 默认值

如果你没给 prop 赋值,它的默认值是 true。以下两个 JSX 表达式是等价的:

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

通常,我们不建议不传递 value 给 prop,因为这可能与 ES6 对象简写混淆,{foo}{foo: foo} 的简写,而不是 {foo: true}。这样实现只是为了保持和 HTML 中标签属性的行为一致。