主流思想
不直接操作DOM,改为“数据驱动思想”
操作DOM思想
- 操作DOM比较消耗性能:主要原因是可能导致DOM重排(回流)/重绘
- 操作起来相对麻烦
数据驱动思想
- 不直接操作DOM
- 直接操作数据,当数据修改后,框架会按照相关的数据,让页面重新渲染
- 框架底层实现试图的渲染,也是基于操作DOM完成的
- 构建了一套 虚拟DOM -> 真实DOM 的渲染体系
- 有效避免了DOM的重排/重绘
- 开发效率更高,最后的性能也相对较好
模式
React框架采用的是MVC体系;Vue框架采用的是MVVM体系
MVC模式
model数据层 + view视图层 + controller控制层
- 我们需要按照专业的语法去构建视图(页面):React中是基于jsx语法来构建视图的
- 构建数据层:但凡在视图中需要“动态”处理的(需要变化的,不论是样式还是内容),我们都要有对应的数据模型
- 控制层:当我们在视图中(或根据业务需求)进行某些操作的时候,都是去修改相关的数据,然后React框架会按照最新的数据,重新渲染视图,以此让用户看到最新的效果
- 数据驱动视图的渲染
- 视图中的表单内容改变,想要修改数据,需要开发者自己写代码实现
- “单项驱动”
MVVM模式
model数据层 + view视图层 + ViewModel数据/视图监听层
- 数据驱动视图的渲染:监听修改数据,让视图重新渲染
- 视图驱动数据的修改:监听页面中表单元素内容改变,自动去修改相关的数据
- “双向驱动“
JSX
JSX:JavaScript and xml(html)把js和html标签混合在了一起【并不是之前使用的字符串拼接】
语法使用
import React from 'react'; // React语法核心
import ReactDOM from 'react-dom/client'; // 构建HTML(WebApp)的核心
// 获取页面中 #root 的容器,作为“根”容器
const root = ReactDOM.createRoot(document.getElementById('root'));
// 基于render方法渲染所编写的视图,把渲染后的内容,全部插入到 #root 中进行渲染
let str = 'hello,react'
root.render(
<div>{str}</div>
);
在ReactDOM.createRoot()的时候,不能直接把HTML/BODY作为根容器,需要指定一个额外的盒子【例如: #root】
React提供了一个特殊的节点(标签):React.Fragment 空文档标记标签
<></>既保证了可以只有一个根节点,又不新增一个HTML层级结构
具体应用
JSX中不同类型值的具体表现
number/string:值是什么,渲染出来就是什么boolean/numm/undefined/Symbol/BigInt:渲染的内容是空- 除数组对象外,其余对象一般都不支持在
{}中进行渲染,但是也有特殊情况:JSX虚拟DOM对象- 给元素设置
style行内样式,要求必须写成一个对象格式
- 数组对象:把数组的每一项都分别拿出来渲染,并不是变为字符串渲染,中间没有逗号
- 函数对象:不支持在
{}中渲染,但是可以作为函数组件,用<Component/>方式渲染
设置样式
root.render(<div style={{ color: 'red', fontSize: '16px' }}>{str}</div>);
命名规则:
小驼峰(camelCase):首字母小写,其余每一个有意义单词首字母大写
大驼峰(PascaCase):首字母都要大写
kabab-case 写法:例如 personal-box
- 案例一
- 案例二
底层渲染机制
- 第一步:把编写的JSX语法,便以为虚拟DOM对象「virtualDOM」
虚拟DOM对象:款加自己内部构建的一套对象体系(对象的相关成员都是React内部规定的),基于这些属性描述出,我们所构建视图中的,DOM节点的相关特征
- 把构建的virtualDOM渲染为真实DOM
真实DOM:浏览器页面中,最后渲染出来,让用户看见的DOM元素
- 第一次渲染页面是直接从virtualDOM -> 真实DOM;但是后期视图更新的时候,需要经过一个DOM-DIFF的对比,计算出补丁包PATCH(两次视图差异的部分),把PATCH补丁包进行渲染
底层渲染机制【创建虚拟DOM】
- 基于babel-preset-react-app把JSX编译为React.createElement(...)这种格式
- 只要是元素节点,必然会基于createElement进行处理
- React.createElement(ele,props,...childrem)
- ele:元素标签名或组件
- props:元素的属性集合(对象),如果没有设置过任何的属性,则此值是null
- children:第三个及以后的参数,都是当前元素的子节点
createElement:创建虚拟DOM对象
export const createElement = function createElement(type, props, ...params) {
console.log("my-creatElement");
let virtualDOM = {
$$typeof: Symbol("react.element"),
props: {},
type,
ref: null
};
//处理ref
if (props.hasOwnProperty("ref")) {
virtualDOM.ref = props.ref;
delete props.ref
}
//处理传递的属性
if (props) { //如果它存在不是null
virtualDOM.props = {
...props
}
}
//处理传递的子节点
if (params.length > 0) {
virtualDOM.props.children = params.length === 1 ? params[0] : params;
//判断第三个参数及以后的参数有几个 如果只有一个把第一给children,如果有多个就把params这个数组给children
}
return virtualDOM
};
- 再把 createElement 方法执行,创建出 virtualDOM虚拟DOM对象,也称之为:JSX元素、JSX对象、ReactChild对象
virtualDOM = {
$$typeof: Symbol(react.element),
ref: null,
key: null,
type: 标签名(组件名),
// 存储了元素的相关属性 && 子节点信息
props: {
// 元素的相关属性
children: 子节点信息,没有子节点则没有这个属性,属性可能是一个值,也可能是一个数组
}
}
React的JSX语法中不能直接渲染对象?
错误。JSX语法中不能直接渲染对象,但可以渲染
数组对象、给元素的style设置样式属性的时候要求样式属性值必须是一个对象、直接在JSX语法中渲染通过createElement创建出来的virtualDOM虚拟DOM对象
底层渲染机制【创建真实DOM】
获取对象所有的私有属性「私有的、不论是否可枚举、不论类型」
- 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);
封装each获取虚拟Dom属性的方法
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);
});
};
render:把虚拟DOM变为真实DOM
export const render = function render(virtualDOM, container) {
console.log("my-render");
let { type, props, ref } = virtualDOM, { children } = props;
delete props.children; //我们已经把children获取到赋值给了其他变量了,此时就可以props中移除它
if (typeof type === "string") {
//要创建的是html标签
let elem = document.createElement(type);
//@1 给元素设置属性
Reflect.ownKeys(props).forEach(key => {
//拿到所有的私有属性
let value = props[key]; //属性名
if (key === "className") {
elem.setAttribute("class", value);
return
};
if (key === "style") {
//JSX语法要求sytle是一个对象,我们需要迭代对象中的每一项,分别给真实的DOM设置行内样式
//value={color:"red",fontSize:"16px"...}
Reflect.ownKeys(value).forEach(item => {
elem.style[item] = value[item]
})
return;
}
elem.setAttribute(key, value);
});
//@2 给元素设置子节点
if (children) {
//确保不论几个节点,children都是一个数组
children = !Array.isArray(children) ? [children] : children;
children.forEach(child => {
//如果其中的一个子节点是一个文本节点,直接把其放入elem容器中
if (/^(string|number)$/.test(typeof child)) {
let textNode = document.createTextNode(child); //创建一个文本节点
elem.appendChild(textNode)
return
}
//如果是元素子节点(也就是一个新的虚拟DOM),需要基于递归,重新调用render方法,只不过容器是elem
render(child, elem)
})
}
//@3 处理ref的属性值
if (ref) {
if (typeof ref === "string") {
//假设:this->实例
this.refs[ref] = elem
}
if (typeof ref === "function") {
ref.call(this, elem)
}
if (ref.current) {
ref.current = elem
}
}
container.appendChild(elem);
return
}
/* 调用的是组件,我们还需要区分函数组件和类组件
@1 如果是函数组件
+ 把其作为普通函数执行
+ 把props(包含children)传递给函数[对象是冻结的]
+ 接受函数的返回值[virtualDOM],再进行渲染
@2 如果是类组件
+ 把其作为构造函数执行,创建一个实例
+ .....
*/
}
import React from 'react';
import ReactDOM from 'react-dom';
import { createElement, render } from './renderJSX'
// import Tc from './test'
// // let Tc = require("./test")
// console.log(Tc);
render(
createElement(
"div",
{ className: "box" },
createElement(
"h2",
{ className: "title", style: { color: "red" } },
"1111"),
createElement(
"ul",
{ className: "news" },
createElement("li", null, "10"),
createElement("li", null, "20"),
createElement("li", null, "30"))),
document.getElementById("root")
)
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(
// <div className='box'>
// <h2 className='title'>1111</h2>
// <ul className='news'>
// <li>10</li>
// <li>20</li>
// <li>30</li>
// </ul>
// </div>
// )