React 框架分析

1. 基础语法

根标签

根标签必须为单节点,如有多个节点可用<React. Fragment></React. Fragment>或者<></>

插值表达式

在类型:
boolean / null / undefined / symbol / bigint渲染结果为空

Object普通对象不支持直接渲染

数组对象分别把每项进行渲染, 并不是直接toString(), [1, 2] => 12

循环五次的方法

{
    new Array(5).fill(null).map((_, index) => < div > {
            {
                index
            }
        } < /div>);
    }

2.jsx 处理机制

第一次渲染时候将虚拟 dom 直接转化为真实 dom, 后面根据 vdom 的 diff 算法计算出差异的部分 Patch, 再渲染 patch

2.1 将 jsx 转 AST 语法树

使用babel-preset-react-appopen in new window将 jsx 语法转化为 AST 语法树,

例如以下 jsx 语法

< div >
    <
    h1 className = "main-title" > 我的标题 < /h1> <
div style = {
    {
        color: "red"
    }
} > content < /div> <
h2 > message < /h2> < /
    div >

React17 以及以前转化为 React.createElement

/* classic转化方法
  React.createElement(element,props,children)
  element:元素或者是组件
  props:属性,中间包括id
  children:子节点
*/
import React from "react";

React.createElement(
    "div",
    null,
    React.createElement(
        "h1", {
            className: "main-title",
        },
        "\u6211\u7684\u6807\u9898"
    ),
    React.createElement(
        "div", {
            style: {
                color: "red",
            },
        },
        "content"
    ),
    React.createElement("h2", null, "message")
);

React18 转化 jsx 函数

import {
    jsx as _jsx
} from "react/jsx-runtime";
import {
    jsxs as _jsxs
} from "react/jsx-runtime";
_jsxs("div", {
    children: [
        _jsx("h1", {
            className: "main-title",
            children: "\u6211\u7684\u6807\u9898",
        }),
        _jsx("div", {
            style: {
                color: "red",
            },
            children: "content",
        }),
        _jsx("h2", {
            children: "message",
        }),
    ],
});

2.2 生成虚拟 dom

使用 React.createElement(react 17)或者 jsx(react 18)函数生成虚拟 dom

virtualDom = {
    $$typeof: Symbol(React.element),
    ref: null,
    key: null
    props: {
        children: []
    },
    type: 'div'
}

简单实现 createElement

// mini-react/min-react-dom.js
export function createElement(element, props, ...children) {
    let vitualDom = {
        $$typeof: Symbol("React.element"),
        ref: null,
        key: null,
        props: {
            children: [],
        },
        type: "div",
    };

    let len = children.length;

    vitualDom.type = element;
    if (props !== null) {
        vitualDom.props = {
            ...props
        };
    }

    if (len === 1) {
        vitualDom.props.children = children[0];
    }

    if (len >= 1) {
        vitualDom.props.children = children;
    }
    return vitualDom;
}

2.3 虚拟 dom 转化为真实 dom

简单实现 render, 将虚拟 dom 转化为真实 dom, render 方法

// mini-react/min-react-dom.js

// 遍历所有的属性
function each(obj, callback) {
    if (obj === null || obj === undefined || typeof obj !== "object") {
        throw new TypeError("obj is not a object");
    }
    Reflect.ownKeys(obj).forEach((key) => callback(key, obj[key]));
}

export function render(vitualDom, container) {
    let {
        type,
        props
    } = vitualDom;

    // 如果是函数组件的话,直接执行,将参数传入
    if (typeof type === "function") {
        const componentFn = type;
        const componentVm = componentFn(props);
        render(componentVm, container);
        return;
    }

    if (typeof type === "string") {
        let ele = document.createElement(type);
        each(props, (key, value) => {
            //className直接设置 ele.className='box'
            if (key === "className") {
                ele.className = value;
                return;
            }

            //style属性需要深层遍历,ele.style.color='red'
            if (key === "style") {
                each(value, (attr, val) => {
                    ele.style[attr] = val;
                });
                return;
            }

            // 子节点,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;
                    }

                    // 子节点是个vitualDom
                    render(child, ele);
                });
                return;
            }

            // 普通属性直接设置 如id等
            ele.setAttribute(key, value);
        });

        container.appendChild(ele);
    }
}

2.4 函数组件渲染机制

  1. 基于babel-preset-react-appopen in new window将 jsx 语法转化为 AST 语法树

2.4.1 基于babel-preset-react-appopen in new window将 jsx 语法转化为 AST 语法树

React.createElement(TestComponent, {
    title: "TestComponent",
    data: [1, 2],
    className: "test-box",
    style: {
        color: "red",
    },
});
  1. AST 语法树转化为虚拟 dom

2.4.2 AST 语法树转化为虚拟 dom

virtualDom = {
    $$typeof: Symbol(React.element),
    ref: null,
    key: null
    props: {
        children: [],
        title: "TestComponent",
        data: [1, 2],
        className: "test-box",
        style: {
            color: "red"
        }
    },
    type: TestComponent
}

2.4.3 render 过程

render 过程:

  1. 执行函数组件 TestComponent

  2. 把 props 传给函数 TestComponent(props)

import {
    createElement,
    render
} from "../mini-react/min-react-dom";

/**
 * 
  function TestComponent (props){
    return (
      <div className={props.className} style={props.style}>
        <div>{props.title}</div>
        <div>{props.data}</div>  
      </div>
    );
  }
 */
function TestComponent(props) {
    return createElement(
        "div", {
            className: props.className,
            style: props.style,
        },
        createElement("div", null, props.title),
        createElement("div", null, props.data)
    );
}

/**
  <TestComponent
    title="TestComponent"
    data={1}
    className="test-box"
    style={{
      color: "red"
    }}
  />
 */

const jsxObj = createElement(TestComponent, {
    title: "TestComponent",
    data: 1,
    className: "test-box",
    style: {
        color: "red",
    },
});

render(jsxObj, document.getElementById("root"));

render 函数(/mini-react/min-react-dom.js)中增加对组件(函数)的判断

// 如果是函数组件的话,直接执行,将参数传入
if (typeof type === "function") {
    const componentFn = type;
    const componentVm = componentFn(props);
    render(componentVm, container);
    return;
}

2.4.4 关于函数 props 属性

props 属性只读
  • 调用组件,传进来的属性为只读属性,已经被冻结了 Object.freeze(props);
  • 父组件调用子组件时候,可以把属性传给子组件,不同的组件收到不同的属性,展示出组件的复用性

4. hooks 函数组件

函数组件每次渲染或者更新,都是函数重新执行一次,产生一个全新的私有上下文

内部的代码都需要重新执行一次

// 示例
import React, {
    useState
} from "react";

export default function TestHookUseState() {
    const [count, setCount] = useState(0);

    function addCount() {
        setCount(count + 1);
    }
    return ( 
      <div>
        <div> count: {count} </div> 
        <div>
          <button onClick = {addCount}> 数字加1 </button> 
        </div>
      </div>
    );
}

4.1 执行过程

1. 第一次渲染组件, 把函数执行【 传递属性】

产生私有上下文EC1
私有变量:
    count = 0;
setCount = 修改状态的函数
addCountClick = 函数
开始编译jsx函数, 创建virtualDom, 最后渲染为真实的dom

点击addCountClick函数, 执行addCountClick
产生私有上下文EC1 - addCountClick
setCount(count + 1) 变为setCount(10);

修改状态
控制视图更新

4.2 useState 实现示例

4.2.1 示例实现代码
let _state;
export function useState(initVal) {
    if (typeof _state === "undefined") {
        _state = initVal;
    }
    const setState = (newValue) => {
        _state = newValue;
        console.log("render");
    };

    return [_state, setState];
}
4.2.2 示例实现代码

在 react18 中,基于 useState 创建出来的修改状态的方法,执行过程也是异步的

基于异步操作与更新队列,实现更新状态的批处理

更新队列:
    [setState1, setState2, , setState3, .....];
4.2.3 惰性化初始值

将初始值放在 useState 的初始化的值里面,最开始只是执行一次,

以后组件更新不会执行

import React, {
    useState
} from "react";

export default function TestHookUseState() {
    const [timeStamp, setTimeStamp] = useState(() => {
        return Date.now();
    });

    return ( 
      <div>
        <div> timeStamp: {timeStamp} </div> 
      </div>
    );
}

4.3 flushSync

flushSync 会立即执行一次更新

import React, {
    useState
} from "react";
// import {useState} from './useState';
import {
    flushSync
} from "react-dom";

export default function TestHookUseState() {
    console.log("render TestHookUseState");
    const [count, setCount] = useState(0);
    const [count2, setCount2] = useState(0);
    const [count3, setCount3] = useState(0);

    function add() {
        setCount(count + 1);
        flushSync(() => {
            setCount2(count2 + 1);
        });
        // count已放队列,count2放队列之后执行一次更新,之后再count3放入更新队列执行一次更新
        setCount3(count3 + 1);
    }

    return ( 
      <div>
        <div> count: {count} </div> 
        <div> count2: {count2} </div> 
        <div>
          <button onClick = {add}> 数字加1 </button> 
        </div> 
      </div>
    );
}

4.4 useEffect 实现示例

4.4.1

useEffect(callback):

  • 第一次渲染完毕之后执行。callback(等于 componentDidMount)

  • 组件每次更新完毕之后执行 callback(等于 componentDidUpdate)

useEffect(callback, [dep, dep2]):

  • 第一次渲染完毕之后执行

  • 当依赖的状态值发生改变的时候,也会执行callback

  • 当依赖的状态值未发生改变的时候,就算组件更新,也不会执行callback

useEffect(callback, []):

callback返回的函数在组件释放的时候执行

import React, {
    useEffect,
    useState
} from "react";

export default function TestHookUseEffect() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log("useEffect callback TestHookUseEffect");
    });

    useEffect(() => {
        console.log("useEffect count callback TestHookUseEffect");
    }, [count]);

    function add() {
        setCount(count + 1);
    }

    return ( 
      <div>
        <div>count:{count}</div> 
        <div>
          <button onClick = {add}> 数字加1 </button> 
        </div > 
      </div>
    );
}
4.4.2

useEffect执行过程

  1. 第一次渲染产生executeContext(执行上下文)
  • 其中的变量count初始化
  • MountEffect方法把callback的依赖项加入effect链表
// 示例
@1 useEffect callback 依赖项
@2 useEffect callback 依赖项
@3 useEffect callback 依赖项 返回函数

基于UpdateEffect方法通知effect链表中的callback按照要求执行

  • 基于依赖项
4.4.3 useLayoutEffect与useEffect区别

useEffectuseLayoutEffect 都是React的hooks函数,都可以用来处理副作用。但是它们执行的时间是不一样的。

useEffect 会在渲染完毕后异步执行,而 useLayoutEffect 会在渲染前同步执行。这就是它们的主要区别。

因此,如果你有需要在渲染前执行的事件, useLayoutEffect 是更好的选择。

另一个区别是在服务端渲染(SSR)时的表现。 useEffect 不会阻塞渲染,而 useLayoutEffect 会在渲染前执行,可能会阻塞渲染。

以下是一些使用建议:

  • 如果你的事件不需要操作DOM,可以使用 useEffect
  • 如果你的事件需要操作DOM,并且希望在渲染前执行,可以使用 useLayoutEffect
  • 如果你使用了服务端渲染(SSR),并且你的事件需要在渲染前执行,并且需要操作DOM,可以使用 useLayoutEffect ,但要注意可能会阻塞渲染。

4.5 useRef与React.createRef

  • 在类组件与函数组件都可以使用createRef
  • 在函数组件中只能使用useRef
  • useRef在每次组件更新时候使用的是一个ref,不会创建新的ref对象
  • React.createRef在每一次组件更新时候,会创建一个全新的ref对象,比较浪费性能,在类组件中不会出现每次都创建ref对象
  • 所以React.createRef适合在类组件中使用
  • useRef适合在函数组件中使用
const form = useRef();
// dom对象
form.current


const form1 = React.createRef();
// dom对象
form1.current

4.6 React.forwardRef与useImperativeHandle

  • 实现元素转发
const Child =React.forwardRef(function Child(props,ref){

  const [text,setText] = useState('');

  const onOk = ()=>{
    console.log('onOk')
  }

  useImperativeHandle(ref,()=>{
    return {
      text,
      onOk
    }
  })

  return (
    <div>
      <div ref={ref}>childText</div> 
    </div> 
  )
})

const Parent = function (){
  let childEleRef = useRef(null);

  useEffect(()=>{
    // childEleRef.current拿到内嵌的对象 {text,onOk}
    console.log(childEleRef.current)
  },[])
  
  return <div>
    <Child ref={childEleRef} />
  </div>  
}

4.7 useMemo

  • useMemo比较类型与Vue的computed
const result =  useMemo(callback,[dep]);
function Calculate(){

  const [a,setA]=useState(0);
  const [b,setB]=useState(0);

  const sum = useMemo(()=>{
    const num = a+b;
    return num;
  },[a,b])


  return <div>
      <div>a:{a}</div>
      <div>b:{a}</div>
      <div>sum:{sum}</div>
      <button onClick={()=>setA(a+1)}> add a</button>  
      <button onClick={()=>setB(b+1)}> add b</button>  
  </div>    
}

Contributors: masecho, --