前言
-
细阅此文章大概需要左右
-
本篇中讲述了:
- 从编写JSX到页面渲染出来都发生了什么?
- 创建虚拟DOM
- 渲染真实DOM
-
欢迎在评论区探讨、留言,如果认为有任何错误都还请您不吝赐教,万分感谢。希望今后能和大家共同学习、进步。
-
下一篇会尽快更新,已经写好的文章也会在今后进行不定期的修订、更新。
-
如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!
-
欢迎转载,注明出处即可。
从编写JSX到页面渲染出来都发生了什么?
-
将我们编写的JSX语法,编译成
虚拟DOM对象(virtualDOM)
虚拟DOM对象(virtualDOM)
:框架自己内部自己构建的一套对象体系【对象的相关成员都是react内部规定的】,基于这些属性描述出,我们所构建视图中的DOM节点相关的特征 -
把构建的虚拟DOM渲染为
真实DOM
真实DOM
:浏览器页面中,最后渲染出来用户能够看到的DOM元素 -
首次渲染
是直接将虚拟DOM对象渲染为真实DOM,但后期视图更新时,需要经过DOM-Diff的对比,计算出“补丁包(patch)”,实际上就是两次要渲染视图的差异部分,然后将补丁包进行差异渲染
首次渲染
:JSX语法->虚拟DOM对象->真实DOM差异渲染
:按照最新的数据将JSX语法全部重新编译为->新的虚拟DOM对象->与旧的虚拟DOM对象DOM-DIFF生成patch(补丁包)->将Patch部分渲染为真实DOM(只重新渲染补丁包)
知道了大体流程,我们来看一看react具体是怎么来做的这些事。
JSX底层渲染机制【创建虚拟DOM】
1. JSX是如何编译为虚拟DOM的?
-
首先webpack打包时基于
babel-preset-react-app
将jsx
代码编译为React.createElement...
这种形式React.createElement(ele, props, ...children)
的参数 【只要是元素节点,必然会基于React.createElement进行处理】
ele
:【节点类型】元素标签名或组件 如(react.fragment、’div’、’h2’等)props
:【元素的属性集合(对象)】如果没有设置过任何属性,则此时为null 如(null、{classname:’aaa’, style:…..}等)children
:第三个及以后的参数都是当前元素的子节点 如(子节点:React.createElement…、’字符串’等)
-
再将编译后生成的这些
React.createElement(...)
方法执行,创建出虚拟DOM对象
(也称jsx元素
、jsx对象
…)// React.createElement(...)方法执行后创建出的虚拟DOM对象 virtualDOM={ $$typeof【固定值】: symbol(React.element), ref:null, key:null, type:标签名【或组件】, // 存储了元素的相关属性&&子节点信息 props【肯定是一个对象,哪怕是空的】:{ ...元素相关属性, children【子节点信息,如果没有子节点就没有这个属性。】:可能是一个值,也可能是一个数组。 }
-
实现一个简单的createElement方法
我们可以模拟react的做法来实现一个简单的createElement方法,来看一下虚拟DOM对象的核心创建流程。
export function createElement(ele, props, ...children){ // 声明一个初始的虚拟dom对象 let virtualDOM={ $$typeof: symbol(React.element), ref:null, key:null, type:null, props:{} } let len = children.length virtualDOM.type = ele // 判读props属性并赋值 if(props!==null){ virtualDOM.props = { ...props } } // 判断子元素是否长度,并赋值 if(len===1) virtualDOM.props.children = children[0] if(len>0) virtualDOM.props.children = children return virtualDOM }
-
其他
胡子语法中不能直接嵌入除数组外的其他对象,但是有一个对象是可以直接嵌入的,那就是虚拟dom对象【JSX元素对象】
<div> {OBJ} // ERROR </div> <div> {[<COMPONENTS/>,....]} // 被支持的写法 </div> <div> {{ $$typeof: symbol(React.element), ref:null, key:null, type:null, props:{...children} }} // 被支持的写法 </div>
同理我们也可以基于createElement语法来构建视图【写起来太麻烦,基本不会这么搞,但是可行】
<div> {React.createElement(ele, props, ...children)} // 被支持的写法 </div>
jsx语法实际上也是会编译成createElement语法来生成虚拟dom对象
JSX底层渲染机制【渲染真实DOM】
1. React是如何把虚拟DOM渲染成真实DOM的
不管是React16版本还是React18版本都是基于ReactDOM
中的render
方法处理的,只不过稍有区别:
v16:
// v16
ReactDOM.render(
<>.....</>,
document.getElementById('root')
)
v18:
// v18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<>.....</>
)
接下来从render方法来看看react到底是怎么将虚拟DOM渲染为真实DOM的
在上一节当中,我们知道虚拟DOM对象就是React框架内部自己构建的一套对象体系,作用就是将JSX编译为虚拟DOM对象后,能够基于这些对象当中所存储的属性描述出我们所构建视图中DOM节点的相关特征。
而React当中的render
方法,就是结合虚拟对象中所描述的这些信息,基于JS当中操作DOM的那些API来创建、编排DOM节点,最终将虚拟DOM渲染为真实DOM。
如:
document.createElement
[ELEMENT].appendChild
[ELEMENT].setAttribute
- …
封装一个遍历对象私有属性的方法
首先在虚拟DOM里对于节点的描述中,肯定会有一个属性props
,其属性值是该节点的属性集合(对象),当我们想要将这个虚拟DOM渲染为真实DOM时,肯定需要给这个DOM节点添加相应的属性。那我们可以先封装一个遍历对象私有属性的方法,便于我们后续遍历Props的这个操作。
基于传统的for/in
循环,会存在一些弊端:
- 性能较差(既可以迭代私有的,也可以迭代公有的)
- 只能迭代“可枚举、非Symbol类型的”属性
于是我们需要自己来封装一个遍历对象私有属性的方法
首先我们要做的事是:获取对象所有的私有属性【私有的、不论是否可枚举、不论类型】
Object.getOwnPropertyNames(arr)
:获取对象非Symbol类型的私有属性(无关是否可枚举)Object,getOwnPropertySymbols(arr)
:获取Symbol类型的私有属性
结合上面的两个方法我们可以获取对象的所有的私有属性名:
// 获取对象的所有的私有属性名
let keys = object.getownPropertyNames(arr).concat(object.getownPropertySymbols(arr));
而在ES6当中我们可以通过Reflect.ownKeys
代替上述操作(弊端:不兼容IE)
// 获取对象的所有的私有属性名
let keys = Reflect.ownKeys(arr);
完整方法:
// 遍历对象私有属性的方法
const each = function each(obj, callback){
if (obj === null || typeof obj !== "object") throw new TypeError('obj is not a object')
if (typeof callback !== "function") throw new TypeError('callback is not a function')
let keys = Reflect.ownKeys(obj)
keys.forEach(key => {
let value = obj[key]
callback(value, key) // 每次循环都将回调执行
}
}
2. 实现一个简易的render方法
接下来通过一个简易的render来了解一下,React当中的render核心都做了哪些事:
/*render:把虚拟DOM渲染为真实DOM*/
export function render(virtualDOM,container){ // 方法接收两个参数,一个是虚拟DOM对象,一个是挂载的容器
let { type, props } = virtualDOM // 先从虚拟DOM对象中解构出节点类型和节点的相关属性
if (typeof type === "string"){ //类型是字符串,存储的是标签名:动态创建这样一个标签
let ele = document.createElement(type)
each(props,(value,key)=>{ // 刚才封装的遍历对象私有属性的方法,为标签设置相关的属性&子节点
//【className的处理】:value存储的是样式类名
if (key ==='className'){
ele.className = value
return
}
//【style的处理】:value:存储的是样式对象
if (key ==='style'){
each(value,(val, attr)=>{ // 遍历styles,为节点设置样式
ele.style = val
})
return
}
//【子节点的处理】:value存储的children属性值
if (key ==='children'){
let children = value
if(!Array.isArray(children)) children = [children] // 处理只有一个子节点的情况
children.forEach(child=>{
// 判断子节点类型:文本节点直接插入即可
if(/^(string|number)$/.test(typeof child)){ // 文本和数字都属于文本节点
ele.appendChild(document.createTextNode(child))
return
}
// 判断子节点类型:子节点依然是一个虚拟DOM【递归处理】
render(child, ele)
})
return
}
// 其他属性先简写
ele.setAttribute(key,value);
});
// 把新增的标签,增加到指定容器中
centainer.appendChild(ele);
}
测试通过我们封装的方法来生成虚拟DOM并渲染真实DOM
import React from 'react'
import ReactDOM from 'react-dom/client'
import { createElement, render } from './myJsxHandle.js'
const root = ReactDOM.createRoot(document.getElementById('root')) //根节点
let styles = {//...样式}
let x = 10
let y = 20
// JSX形式
// root.render(
// <div className='container'>
// <h1 className='title' style={styles}>Title</h1>
// <div className='box'>
// <span>{x}</span>
// <span>{y}</span>
// </div>
// </div>
// )
// 基于我们封装的createElement语法来构建视图
let myVirtualDOM = createElement(
'div',
{className: 'container'},
createElement(
'h1',
{className: 'title', style: styles},
'Title'
),
createElement(
'div',
{className: 'box'},
createElement('span', null, x),
createElement('span', null, y)
)
)
// 基于我们封装的render,使用虚拟DOM渲染真实DOM
render(myVirtualDOM, document.getElementById('root'))
其他
在生成真实DOM的过程中,需要给元素设置属性。通常设置属性的方式有两种,我们选择通过内置api设置的方式
为元素设置属性有两种方式:【自定义、内置】,使用时需要注意他们的区别
元素.属性 = 属性
自定义属性的方式实际上是给对象的堆内存空间中新增成员,不会设置到元素标签上
获取:
元素.属性
删除:
delete 元素.属性
元素.setAttribute(属性,属性值)
- 内置属性,是直接写在元素的标签上
- 获取:
getAttribute
- 删除:
removeAttribute
原文链接:https://juejin.cn/post/7341394494900453413 作者:Hypnotize