React教程 - 模式与JSX

163 阅读7分钟

主流思想

不直接操作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

  • 案例一

QQ截图20230227170952.png

  • 案例二

QQ截图20230227172606.png

底层渲染机制

  • 第一步:把编写的JSX语法,便以为虚拟DOM对象「virtualDOM」

虚拟DOM对象:款加自己内部构建的一套对象体系(对象的相关成员都是React内部规定的),基于这些属性描述出,我们所构建视图中的,DOM节点的相关特征

  • 把构建的virtualDOM渲染为真实DOM

真实DOM:浏览器页面中,最后渲染出来,让用户看见的DOM元素

  • 第一次渲染页面是直接从virtualDOM -> 真实DOM;但是后期视图更新的时候,需要经过一个DOM-DIFF的对比,计算出补丁包PATCH(两次视图差异的部分),把PATCH补丁包进行渲染

底层渲染机制【创建虚拟DOM】

  1. 基于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
};
  1. 再把 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>
// )