【 React 】React JSX 转换成真实DOM的过程?

1. 是什么

react通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后React会将这些「变化」更新到屏幕上

在前面文章了解中,JSX通过babel最终转化成React.createElement这种形式,例如:

javascript 复制代码
<div>
     < img src="avatar.png" className="profile" />
    <Hello />
</div>

会被babel 转换成如下:

javascript 复制代码
React.createElement(
    "div",
    null,
    React.createElement("img", {
        src: "avatar.png",
        className: "profile"
    }),
    React.createElement(Hello, null) 
);

在转化过程中,babel在编译时会判断JSX中组件的首字母:

  • 当首字母为小写时,其被认定为原生DOM标签,createElement的第一个变量被编译为字符串
  • 当首字母为大写时,其被认定为自定义组件,createElement的第一个变量被编译为对象

最终都会通过RenderDOM.render(...)方法进行挂载,如下:

javascript 复制代码
ReactDOM.render(<App />, document.getElementById("root"));

2. 过程

在react中,节点大致可以分成四个类别:

  • 原生标签节点
  • 文本节点
  • 函数组件
  • 类组件

如下所示:

javascript 复制代码
class ClassComponent extends Component {
    static defaultProps = {
        color: "pink"
    };
    render() {
        return ( 
            <div className="border"> 
                <h3>ClassComponent</h3> 
                <p className={this.props.color}>{this.props.name}</p >
            </div> 
        ); 
    }
}
function FunctionComponent(props) {
    return ( 
        <div className="border">
            FunctionComponent
            <p>{props.name}</p >
        </div> 
    ); 
}
const jsx = ( 
    <div className="border">
        <p>xx</p > 
        < a href=" ">xxx</ a> 
        <FunctionComponent name=" " />
        <ClassComponent name=" " color="red" />
    </div> 
);

这些类别最终都会被转化成React.createElement这种形式

React.createElement其被调用时会传入标签类型type,标签属性props及若干子元素children ,作用是生成一个虚拟Dom对象,如下所示:

javascript 复制代码
function createElement(type, config, ...children) {
    if (config) {
    delete config.__self;
    delete config.__source; }
    //源码中做了详细处理,比如过滤掉 ! key ref等
    const props = {
    ...config,
    children: children.map(child =>
    typeof child === "object" ? child : createTextNode(child) ) };
    return {
    type,
    props
    }; 
}
function createTextNode(text) {
    return {
    type: TEXT,
    props: {
    children: [],
    nodeValue: text
    } }; 
}
export default {
    createElement
};

createElement会根据传入的节点信息进行一个判断:

  • 如果是原生标签节点,type是字符串,如div、span
  • 如果是文本节点,type就没有,这里是TEXT
  • 如果是函数组件,type是函数名
  • 如果是类组件,type是类名

虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,使用方法如下:

javascript 复制代码
ReactDOM.render(element,container[,callback])

当首次调用时,容器节点里的所有DOM元素都会被替换,后续的调用则会使用React的diff算法进行高效的更新

如果提供了可选的回调函数callback ,该回调将在组件被渲染或更新之后被执行render大致实

现方法如下:

javascript 复制代码
function render(vnode, container) {
    console.log("vnode", vnode); //虚拟 DOM 对象
    // vnode _> node
    const node = createNode(vnode, container);
    container.appendChild(node); 
}
    // 创建真实DOM节点
function createNode(vnode, parentNode) {
    let node = null;
    const {type, props} = vnode;
    if (type === TEXT) {
        node = document.createTextNode("");
    } else if (typeof type === "string") {
        node = document.createElement(type); 
    } else if (typeof type === "function") {
        node = type.isReactComponent? updateClassComponent(vnode, parentNode) : updateFunctionComponent(vnode, parentNode); 
    } else {
        node = document.createDocumentFragment(); 
    }
    reconcileChildren(props.children, node);
    updateNode(node, props);
    return node;
 }
    //遍历下子 vnode 然后把子vnode-> 真实DOM 再插入到父node中
function reconcileChildren(children, node) {
    for (let i = 0; i < children.length; i++) {
        let child = children[i];
        if (Array.isArray(child)) {
            for (let j = 0; j < child.length; j++) {
                render(child[j], node); 
            } 
        } else {
            render(child, node); 
        } 
    } 
}
function updateNode(node, nextVal) {
    Object.keys(nextVal) .filter(k => k !== "children") .forEach(k => {
        if (k.slice(0, 2) === "on") { 
            node.addEventListener(eventName, nextVal[k]); 
        } else {
                node[k] = nextVal[k]; 
        } 
    }); 
}
//返回真实 dom节点
// 执行函数
function updateFunctionComponent(vnode, parentNode) {
    const {type, props} = vnode;
    let vvnode = type(props);
    const node = createNode(vvnode, parentNode);
    return node; 
}
            // dom
            // 先实例化,再执行render函数
function updateClassComponent(vnode, parentNode) {
    const {type, props} = vnode;
    let cmp = new type(props);
    const vvnode = cmp.render();
    const node = createNode(vvnode, parentNode);
    return node; 
}
export default {
    render
};

3. 总结

渲染流程如下所示:

  • 使用React.createElement或JSX编写React组件,实际上所有的JSX代码最后都会转换成React.createElement(...),Babel帮助我们完成了这个转换的过程。
  • createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
  • ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
相关推荐
To_OC10 小时前
从一头雾水到跑通全流程:我用一个周末啃透了JWT登录鉴权
前端·后端·http
excel11 小时前
研究 Vue 3 源码的收获
前端·vue.js
可乐鸡翅yeah_13 小时前
业务中 M3U8 水印相关坑,硬水印和动态水印区别
前端·网络·数据库·ffmpeg·m3u8在线
lerhxx13 小时前
AI 应用如何高效优雅地恢复中断?—— "连接解耦 + 状态持久化"
前端·javascript
计算机魔术师14 小时前
METR 演示 AI 智能体如何篡改 Inspect 评估记录以掩盖不当行为
前端
前端snow14 小时前
ai agent --- 异步处理之 Rabbit MQ
前端
念何架构之路14 小时前
zap扩展生态与总结
java·前端·数据库
独孤九剑打醒他14 小时前
【原创开源】【概念设计】源 - 栅 - 漏 - 栅 - 源 横向双栅 MOS,低压交流多值逻辑芯片探索
前端·其他·架构·开源·硬件工程
mantou13215 小时前
我给 AI Agent 做了个「油猴」:让 Claude Code / Codex 直接用你已登录的浏览器
前端·javascript·后端
默_笙15 小时前
🍕 一个主编、三个工种、两本手册:搭一支 AI 调研队
前端·javascript