3. MVC 模式和 MVVM 模式
React 是 Web 前端框架
目前市面上比较主流的前端框架有:
- React
- Angular
- Vue
主流的思想:不再直接操作 DOM,而是改为“数据驱动思想”。
操作 DOM 思想:
- 操作 DOM 比较消耗性能,主要原因是:可能导致 DOM 重排(回流)/ 重绘
- 操作起来也相对麻烦一些
数据驱动思想:
-
操作数据,框架会按照相关的数据,让页面重新渲染
-
框架底层构建从虚拟 DOM(Virtual DOM)到真实DOM的渲染体系,有效避免 DOM 的重排和重绘
-
相比真实 DOM,虚拟 DOM 更为轻量级,效率更高
-
优点:
- 开发效率高
- 性能高
React 框架采用的是 MVC 体系;Vue 采用的是 MVVM 体系。
MVC = Model 数据层 + View 视图层 + Controler 控制层
-
单向驱动(视图 -> 数据需要开发者自行写代码实现)
-
需要按照专业的语法去构建视图(页面):React 中是基于 jsx 语法来构建视图的
-
构建数据层:但凡在视图中,需要“动态”处理的(需要变化的,不论是样式还是内容),都要有对应的数据模型
-
控制层:当在视图中(或者根据业务需求)进行某些操作时,都是去修改相关的数据,然后 React 框架会按照最新的数据,重新渲染视图,以此让用户看到最新的效果
MVVM = Model 数据层 + View 视图层 + ViewModel 数据视图监听层
- 双向驱动
- 数据驱动视图的渲染:监听数据的更新,让视图重新渲染
- 视图驱动数据的更改:监听页面中表单元素的内容改变,自动去修改相关的数据
4. JSX 语法使用上的细节
JSX:JavaScript and XML(HTML),把 JS 和 HTML 标签混合在一起
import React from 'react'; // React 语法核心
import ReactDOM from 'react-dom/client'; // 构建 HTML(WebApp) 的核心
// 获取页面中的 #root 容器,作为根容器,不能将 html、body 元素作为根容器
const root = ReactDOM.createRoot(document.getElementById("root"));
// 基于 render 方法渲染编写的视图,把渲染后的内容,全部插入到 #root 元素中
// 每一个构建的视图是能有一个根节点
root.render(
<>
{/* 空文档标记标签 React.Fragment,不会增加层结构,既保证了只有一个根节点,又不增加一个 HTML 层级结构 */}
<div>珠峰培训</div>
</>
);
可以通过 {} 嵌入 JS 表达式来渲染:
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById("root"));
let text = "珠峰培训";
root.render(
<div>
{text}
</div>
);
常见的 JS 表达式有:
- 变量 / 值
- 数学运算
- 三目表达式
- 借助于数组迭代方法的循环,如
map - 有返回值的函数调用
5. JSX 的具体应用
{} 语法中嵌入不同的值,所呈现出来的特点如下:
-
number / string:值是什么,就渲染出来什么
-
bool / null / undefined / Symbol / Bigint:渲染内容是空
-
普通对象:不支持渲染
-
数组对象:把每一项拿出来,分别渲染(并不是变为字符串渲染,中间没有逗号,如果数组中有不支持渲染的元素,如普通对象,也会报错)
-
正则对象、时间对象、包装类对象:不支持渲染
-
函数对象:不支持在
{}中渲染,但是可以作为函数组件,作为组件<componment/>渲染
除数组对象之外,其余对象一般都不支持在
{}中渲染,但也有特殊情况:
- JSX 虚拟 DOM 对象
- 给元素设置 style 样式,要求写成一个对象格式
5.1 元素设置样式
行内样式,需要基于对象的格式处理,直接写成字符串会报错。
<h2 style={{color: 'red', fontSize: '18px'}}>Learn React</h2>
样式属性要基于小驼峰命名法。
设置样式类名:要把 class 替换为 className:
<h2 className="box"></h2>
需求一:基于数据的值,来判断元素的显示隐藏
<div>
{/* 控制元素是否显示,不论显示还是隐藏,元素本身已经渲染出来了 */}
<div style={{
display: this.flag ? "block" : "none"
}}>显示
</div>
{/* 控制元素是否渲染 */}
{this.flag ? <button>渲染/不渲染</button> : null}
</div>
需求二:从服务器获取了一组列表数据,循环动态绑定相关的内容
const root = ReactDOM.createRoot(document.getElementById('root'))
let data = [
{
id: 1,
title: '新闻一'
},
{
id: 2,
title: '新闻二'
},
{
id: 3,
title: '新闻三'
}
]
root.render(
<>
<h2 className="title">今日新闻</h2>
<ul className="news-box">
{
data.map( (item, index) => {
return <li key={item.id}>
<em>{item.id}</em>
<span>{item.title}</span>
</li>
} )
}
</ul>
<br />
{/* 扩展需求:没有数组,就是想单独循环 5 ci */}
{
new Array(5).fill(null).map( (_, index) => {
return <button key={index}>
按钮{index + 1}
</button>
})
}
</>
)
对于
Array()函数,如果参数仅传入一个数值,则该参数表示长度,即:new Array(5) // 返回数组长度为 5 的稀疏数组,其每一项都是 empty使用数组的迭代方法(
forEach或map),它们不会去迭代稀疏数组,例如:let arr = new Array(5) arr.forEach( () => { console.log('OK') // 不打印任何输出 } )可以基于数组的
fill方法,将稀疏数组进行填充,变为密集数组,就可以使用数组的迭代方法了。let arr2 = arr.fill(null) // arr2 = [null, null, null, null, null] arr2.forEach( () => { console.log('OK') // 输出 5 次 'ok' } )
6. JSX 底层渲染机制
关于 JSX 底层处理机制:
-
第一步:把 JSX 语法编译为虚拟 DOM 对象(virtual DOM)
虚拟 DOM 是框架自己内部构建的一套对象体系(对象的相关成员都是 React 内部规定的),基于这些属性描述出我们所构建视图中的 DOM 节点的相关特征。
-
基于 babel-prest-react-app 把 JSX 编译为
React.createElement(…)这种格式(只要是元素节点,必然基于createElement进行处理)React.createElement(ele, props, ...children) // ele:元素标签名「或组件」 // props:元素的属性集合「如果没有设置过任何属性,则为 null」 // children:第三个及以后的参数,都是当前元素的子节点 -
再执行
createElement方法,创建出 virtual DOM 对象(也有称之为:JSX 元素、JSX 对象、ReactChild 对象)vitrualDOM = { $$typeof: Symbol(react.element), ref: null, key: null, type: 标签名「或组件」, // 存储了元素的相关属性 && 子节点信息 props: { 元素的相关属性, children: 子节点信息「没有子节点则没有这个信息、属性可能是一个值、也可能是一个数组」 } }
-
-
第二步:把构建的 virtual DOM 渲染为真实 DOM
真实 DOM 是浏览器页面中,最后渲染出来,让用户看见的 DOM 元素。
基于
ReactDOM中render方法处理:{/* V16 */} ReactDOM.render( <>...</>, document.getElementById('root') ); {/* V18 */} const root = ReactDOM.createRoot(document.getElementById('root')); root.render{ <>...</> };
补充说明:第一次渲染页面是直接从 virtual DOM -> 真实 DOM;但是后期视图更新的时候需要经过一个 DOM-diff 的对比,计算出补丁包 PATCH(两次视图差异的部分),把 PATCH 补丁包进行渲染。
手写一个 createElement 方法:
// createElement:创建虚拟 DOM 对象
export function createElement(ele, props, ...children) {
let virtualDOM = {
$$typeof: Symbol('react.element'),
ref: null,
key: null,
type: null,
props: {}
}
virtualDOM.type = ele;
if (props !== null) {
virtualDOM.props = {
...props
}
}
let len = children.length
if(len === 1) virtualDOM.props.children = children[0]
if(len > 1) virtualDOM.props.children = children
}
手写一个 render 方法:
// 封装一个对象迭代的方法
// 如果基于传统的 for/in 循环,会存在一些弊端
// 性能较差:既可以迭代私有的,也可以迭代公有的
// 只能迭代可枚举、非 Symbol 类型的属性
// ...
// 解决思路:获取对象所有的私有属性(私有的、不论是否可枚举、不论类型)
// 使用 Object.getOwnPropertyNames(obj) -> 获取对象非 Symbol 类型的私有属性(无关是否可枚举)
// 使用 Object.getOwnPropertySymbols(obj) -> 获取 Symbol 类型的私有属性
// 如果不考虑兼容性,可以直接使用 Reflect.ownKeys(obj) -- 不兼容 IE
const each = function each(obj, callback) {
if(obj === null || typeof obj !== "object") throw new('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)
})
}
export function render(virtualDOM, container) {
let { type, props } = virtualDOM
if(typeof type === 'string') { // 存储的是标签名:动态创建该标签
let ele = document.createElement(type)
// 为标签设置相关的属性和子节点
each(props, (value, key) => {
// className 的处理
if(key === 'className') {
ele.className = value
return
}
// style 的处理
if(key === 'style') {
each(value, (var, attr) => {
ele.style[attr] = val
})
return
}
// 子节点的处理
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
}
// 子节点又是一个 virtual DOM,递归处理
render(child, ele)
})
return
}
ele.setAttribute(key, value)
})
// 把新增的标签增加到指定容器中
container.appendChild(ele)
}
}
为元素设置(自定义、内置)属性,有两种方式:
元素.属性 = 属性值原理:对于内置属性,是设置在元素的标签上;对于自定义属性,是给对象的堆内存空间中新增成员,不会设置在标签上
获取:
元素.属性删除:
delete 元素.属性
元素.setAttribute(属性, 属性值)原理:直接写在元素的标签上
获取:
getAttribute删除:
removeAttribute二者不能混淆!!!