一文搞懂JSX->真实DOM的过程

227 阅读4分钟

简单版(只处理一个jsx语法)

1.基于BABEL中的语法解析模块(babel-preset-react)把JSX语法编译为React.CreateElement(...)结构.

根据babel和babel-preset-react插件中正则解析,得到如图所示

2.执行React.createElement(type,props,children),会创建一个对象,这个对象就是虚拟DOM.

React.createElement("h1", {
    ref: "ref1",
    key: "key1",
    id: "title",
    className: "title",
    style: {
        "font-size": "12px",
        "color": "red"
    }
}, "\u54C8\u54C8\u54C8")

// 执行大的的结果是一个对象

{
    key: 'key1',
    props:{
        children: "哈哈哈"
        clasName: "title"
        id: "title"
        style: {color: "red"}
    },
    ref: "ref1",
    type: "h1",
    __proto__: Object
}

实现React.createElement(type,props,children)方法

步骤:

1.创建一个对象(默认要有四个属性: Type/Props/Key/Ref),然后把对象返回 2.参数处理,按照React.createElement(type,props,children)执行的结果处理

/*
 * 函数名: createElement
 * 参数: 
    type: 标签名,       必传
    props: 属性值等,    必须
    children:  内容     可不传
 * 返回: 返回一个对象,如上面React.createElement()执行的结果
*/
createElement(type,props,children){
    let obj = {
        type: null,
        props: {
            children: '',
        }
        key: null,
        ref: null
    };
   
    // es6解析
    obj = { 
        ...obj 
        type,       // => type:type(参数)
        props:{
            ...props,   // 把参数props结构了
            children    // => children: children(参数)
        }
    }
    // 处理key和ref
    if('key' in obj.props){
        obj.key = obj.props.key;
        obj.props.key = undefined
    }
    if('ref' in obj.props){
        obj.ref = obj.props.ref;
        obj.props.ref = undefined
    }
    return obj;
}

ReactDOM.render(JSX语法最后生成的对象,容器),基于RENDER方法把生成的对象动态创建为DOM元素,插入到指定的容器中.

ReactDOM.render(<div>
    <h1 key="key1" id="title" className="title" style={{"font-size":"12px", "color": "red"}}>哈哈哈</h1>
</div>, document.getElementById('root'));

render方法的实现

/*
 * 函数名: render
 * 参数: 
    obj: (React.createElement执行的结果), 
    container: document.getElementById('root')),
    callback: 回调函数
 * 返回: 渲染到页面上
*/
render(obj,container,callback){
    let {type,props} = obj || {};
    // 根据type创建标签
    let newElement = document.createElement(type);
       
    /*
     * setAttribute 
     * 基于setAttribute可以设置的属性可以表现在HTML结构上
    */ 
    
    // 根据props创建标签上的属性
    for(let attr in props){
        // 只需要处理props私有属性,原型上的属性不需要去处理
        if(!props.hasOwnProperty(attr)) break;
        
        // props[attr]可能是undefined,可能是null
        if(!props[attr]) continue;
        
        let value = props[attr];
        
        // 处理attr == 'style'的时候
        if(attr === 'style'){
            if(!value) continue;
            // props['style']是一个对象
            for(let stylekey in value){
                // 对象只处理自己私有的属性
                if(value.hasOwnProperty(stylekey)){
                    // 把对象的每一个键值对压入到标签的style中
                    newElement['style'][stylekey] = value[stylekey];
                }
            }
            continue;
        }

        // 处理className(呈现在页面上的是class)
        if(attr === 'className'){
            newElement.setAttribute('class') = value;
        }
        
        // 处理children(简单的,我们只有文本节点)
        if(attr === 'children'){
            if(typeof value === 'string'){
                // 通过createTextNode创建文本节点,然后压入到标签中
                let text = document.createTextNode(value)
                newElement.appendChild(text);
            }
        }
    }
    // 把新生成的标签压入到容器中
    container.appendChild(newElement);
}

处理复杂的JSX语法

基于babel转义

发现,createElement可以传入多个参数,基于React,createElement生成的对象就为

createElement方法创建虚拟DOM

方法改造

/*
 * 函数名: createElement
 * 参数: 
    type: 标签名,       必传
    props: 属性值等,    必须
    children: 
        可能是一个字符串,也有可能是一个数组
 * 返回: 返回一个对象,如上面React.createElement()执行的结果
*/
createElement(type,props,...childrens){
    props = props ? props : {};
    let obj = {
        type: null,
        props: {
            children: '',
        }
        key: null,
        ref: null
    };
   
    // es6解析
    obj = { 
        ...obj 
        type,       // => type:type(参数)
        props:{
            ...props,   // 把参数props结构了
            // 如果childrens数组只有一项,或者是一个空数组,
            // 如果childrens数组有多项
            // 此时我们生成的children有可能是一个字符串(或者为空),也有可能是一个数组,
            children: childrens.length<=1?(childrens[0] || ''): childrens    // => children: children(参数)
        }
    }
    // 处理key和ref
    if('key' in obj.props){
        obj.key = obj.props.key;
        obj.props.key = undefined
    }
    if('ref' in obj.props){
        obj.ref = obj.props.ref;
        obj.props.ref = undefined
    }
    return obj;
}

根据生成的obj对象(虚拟DOM),render函数渲染的 props[attr]=== children部分,也要做相对应的处理

render函数渲染

render(obj,container,callback){
    let {type,props} = obj || {};
    // 根据type创建标签
    let newElement = document.createElement(type);
       
    /*
     * setAttribute 
     * 基于setAttribute可以设置的属性可以表现在HTML结构上
    */ 
    
    // 根据props创建标签上的属性
    for(let attr in props){
        // 只需要处理props私有属性,原型上的属性不需要去处理
        if(!props.hasOwnProperty(attr)) break;
        
        // props[attr]可能是undefined,可能是null
        if(!props[attr]) continue;
        
        let value = props[attr];
        
        // 处理attr == 'style'的时候
        if(attr === 'style'){
            if(!value) continue;
            // props['style']是一个对象
            for(let stylekey in value){
                // 对象只处理自己私有的属性
                if(value.hasOwnProperty(stylekey)){
                    // 把对象的每一个键值对压入到标签的style中
                    newElement['style'][stylekey] = value[stylekey];
                }
            }
            continue;
        }

        // 处理className(呈现在页面上的是class)
        if(attr === 'className'){
            newElement.setAttribute('class') = value;
        }
        
        // 处理children(复杂的,
            // children
            // 可能是一个字符串
            // 也可能是一个对象
            // 还可能是一个数组,
                // 数组中有字符串,也要JSX对象
            
        if(attr === 'children'){
            
            // 统一作出数组处理
            if(!(value instanceof Array)){
                let value = [value]
            }
            
            // 变量数组
            value.forEach((item,index)=>{
                if(typeof value === 'string'){
                    // 通过createTextNode创建文本节点,然后压入到标签中
                    // 此时创建的就是当前的item像
                    let text = document.createTextNode(item)
                    newElement.appendChild(text);
                }else{
                    // 就是一个jsx对象,重新执行render方法
                    // item为jsx对象
                    // newElement为容器
                    render(item,newElement);
                }
            })
        }
    }
    // 把新生成的标签压入到容器中
    container.appendChild(newElement);
}

总结:

1. 基于babel和babel-preset-react转义
2. 基于React.createElement创建虚拟DOM
3. 根据虚拟DOM生成标签tree.

细节之处,略有疏漏,如有不足或者错误的地方,欢迎大佬批评并加以指正.谢谢.