React源码系列-React.createElement()

228 阅读3分钟

createElement 是干嘛的?

用于创建虚拟DOM,react的DOM渲染方式便是通过虚拟DOM转换为真实的DOM最后渲染到页面上去

React 渲染的大致流程

DOM元素写在js中就是jsx

React在通过webpack打包之后,会通过babel生成一个 React.createElement() 方法
这个方法将jsx接收进去,最后返回一个vnode,也就是虚拟DOM

最后将转换好的vnode通过react-dom的render方法转换为真实dom,挂载到指定的元素上去

createElement 方法实现

在实现之前,我们先来看一下react转换过后虚拟dom的大致结构

{
  $$typeof: Symbol(react.element),
  key: null,
  props: {
    children: "hello world",
    className: "title",
    style: {color: '#fff'},
  },
  type: "h3",
  ref: null,
}
  • 其中 $$typeof 是一个标识符,用来表示你创建的是一个element元素还是一个文本
    
  • key 和 ref 是直接添加到dom上的属性,分别用于后续的 diff算法和读取真实DOM的操作
    
  • type 就是你要创建的标签
    
  • props 中存放的就是你标签的内容 children, 类名className和样式style
    

    下面开始正式实现createElement方法

      在react中,此方法会接收三个参数 1.type 2.config 3.children
      
      第一个参数是标签的类型
      第二个参数是类名、样式、key等属性配置
      第三个参数就表示的是你要渲染的值
      
    

    这里第三个参数中children比较特殊,他可以是多个,也可以是一个或者没有。当他是一个的时候就会是一个字符串,多个时会转换为一个数组,并且children可以接收转换过后的虚拟DOM对象

    // 大概类似这样
    React.createElement("h3", {
       className: "title",
       style: {
           color: "#fff"
       }
    }, "hello world", React.createElement("span", null, "hello"))
    

    1. 首先我们来定义一个函数

    function createElement(type, config, children) {
        // 返回值为一个虚拟DOM对象
        return {
            type,
        }
    }
    

    2. 然后我们处理较为复杂的props字段

    function createElement(type, config, children) {
        // config中的样式类名等属性是我们需要的,所以直接展开
        let props = {...config}
        
        if(config) {
            /*
              这里children的值可以是多个,所以进行多次判断
            */
            if(arguments.length > 3) {  
              /*
                这里可以使用更简单的写法,就是多个值的话截取参数的第三个往后,最后返回一个数组
                使用 Array.from() 把arguments转换成真实数组,然后再进行截取就可以了
              */ 
              props.children = Array.prototype.slice.call(arguments, 2)
            } else if (arguments.length === 3) {
              // children 只有一个值直接赋值就行,没值的话可以忽略不计
              props.children = children
            }
        }
        return {
            type,
            props, // 最后把props返回出来
        }
    }
    

    3. Key和Ref其实是定义在第一层的,但是是通过config参数接收的,所有我们要去单独处理他

    function createElement(type, config, children) {
        // 处理 key ref
        let key,ref
        if(config) {
            key = config.key
            ref = config.ref
            // 这里要将config中的key和ref删除
            // 防止下面处理children时将key和ref添加到props中
            delete config.key
            delete config.ref
        }
    
        // config中的样式类名等属性是我们需要的,所以直接展开
        let props = {...config}
        
        if(config) {
            
            // 这里children的值可以是多个,所以进行多次判断
            if(arguments.length > 3) { 
              props.children = Array.prototype.slice.call(arguments, 2)
            } else if (arguments.length === 3) {
              props.children = children
            }
        }
        return {
            // $$typeofs 这里这个属性用来区别你创建的是element还是文本
            // 文本类型后面虚拟dom转换为真实dom时会使用到
            $$typeofs: Symbol("react.element"),
            type,
            props,
            key,   // 把这两个属性添加上去即可
            ref,
        }
    }
    

    4. 最后就是调用我们封装好的createElement方法

    let element = createElement("h3", {
      className: "title",
      style: {
        color: "#fff"
      }
    }, "hello world", React.createElement("span", null, "hello"))
    
    这里只是对createElement函数进行了一次简单的封装,在后续实现render()方法中会将其进一步完善