B站2023年最新珠峰React全家桶(二)

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>
                        &nbsp;&nbsp;
                        <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

使用数组的迭代方法(forEachmap),它们不会去迭代稀疏数组,例如:

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 元素。

    基于 ReactDOMrender 方法处理:

    {/* 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)
    }
}

为元素设置(自定义、内置)属性,有两种方式:

  1. 元素.属性 = 属性值

    原理:对于内置属性,是设置在元素的标签上;对于自定义属性,是给对象的堆内存空间中新增成员,不会设置在标签上

    获取:元素.属性

    删除:delete 元素.属性

  2. 元素.setAttribute(属性, 属性值)

    原理:直接写在元素的标签上

    获取:getAttribute

    删除:removeAttribute

二者不能混淆!!!