React

前言

本篇主要对React的状态进行一个介绍。

声明式 UI 与命令式 UI 的比较

当你设计 UI 交互时,可能会去思考 UI 如何根据用户的操作而响应变化。想象一个允许用户提交一个答案的表单:

  • 当你向表单输入数据时,“提交”按钮会随之变成可用状态

  • 当你点击“提交”后,表单和提交按钮都会随之变成不可用状态,并且会加载动画会随之出现

  • 如果网络请求成功,表单会随之隐藏,同时“提交成功”的信息会随之出现

  • 如果网络请求失败,错误信息会随之出现,同时表单又变为可用状态

在 命令式编程 中,以上的过程直接告诉你如何去实现交互。你必须去根据要发生的事情写一些明确的命令去操作 UI。

而在Vuejs渐进式框架和React库里面都是采用的声明式编程。

声明式地考虑 UI

你已经从上面的例子看到如何去实现一个表单了,为了更好地理解如何在 React 中思考,一般情况下要用 React 重新实现这个 UI就要经过这几个一般步骤:

  1. 定位你的组件中不同的视图状态

  2. 确定是什么触发了这些 state 的改变

  3. 表示内存中的 state(需要使用 useState)

  4. 删除任何不必要的 state 变量

  5. 连接事件处理函数去设置 state

React的状态管理

在React中,状态管理是一个非常重要的概念,React的状态管理是通过state来实现的。

state

state是React组件中的一个内置对象,用于存储组件内部的状态。state是一个普通的JavaScript对象,我们可以通过this.state来访问它。

React 会为 UI 中的组件结构构建 渲染树。

当向一个组件添加状态时,那么可能会认为状态“存在”在组件内。但实际上,状态是由 React 保存的。React 通过组件在渲染树中的位置将它保存的每个状态与正确的组件关联起来。

这边与Vuejs的区别是,Vue的响应式系统是与组件进行隔离开来的,而React的状态是与组件进行绑定的。

我们将state嵌入进React的组件里面,作为组件vnode的一部分信息,当React的state更新时,也意味着组件的信息更新,会触发组件的重新渲染。

如果理解了这一点,那么我们就可以很轻易的理解React的响应式是怎么工作的了,React的状态是与组件绑定的,当状态更新时,组件也会更新,这也是React的响应式系统。

我们来具体给一个例子:

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

下面是它们的树形结构的样子:

vue-react

这是两个独立的 counter,因为它们在树中被渲染在了各自的位置。 一般情况下你不用去考虑这些位置来使用 React,但知道它们是如何工作会很有用。

而他们有各自的状态,因为每个组件实例都有自己的 state。 你可以在一个组件中使用多个 state 变量。当我更新了state之后,会触发组件的重新渲染,这边我们可以看到,两个counter的状态是独立的,互不影响。

相同位置的相同组件会使得 state 被保留下来

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        使用好看的样式
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

vue-react

那么相同位置的不同组件会使 state 重置,这一点应该大家也能够知道为什么了。

再举一个例子:将一个<Counter> 替换为一个 <p>

vue-react

当 Counter 变为 p 时,Counter 会被移除,同时 p 被添加。

vue-react

当切换回来时,p 会被删除,而 Counter 会被添加

参考的React文档在这里:状态与渲染树中的位置相关

提取状态逻辑到 reducer 中

对于那些需要更新多个状态的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以在组件外部将所有状态更新逻辑合并到一个称为 “reducer” 的函数中。这样,事件处理程序就会变得简洁,因为它们只需要指定用户的 “actions”。在文件的底部,reducer 函数指定状态应该如何更新以响应每个 action

useReducer 是 React 中的一个 Hook,用于管理组件的状态逻辑。它提供了一种更复杂状态逻辑的组织方式,适用于那些包含多个子值或需要根据先前的状态来更新的状态。 useReducer 的使用方式类似于 Redux 中的 reducer 概念。

下面是 useReducer 的基本用法:

import React, { useReducer } from 'react';

// reducer函数,接受当前state和action,返回新的state
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const initialState = { count: 0 };

const Counter = () => {
  // 使用useReducer,传入reducer和初始state
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
};

export default Counter;

主要的步骤包括:

  1. 定义 reducer 函数: 这个函数接收当前状态 state 和一个描述动作的 action,然后根据 action 的类型执行相应的逻辑,并返回新的状态。

  2. 初始化状态: 通过调用 useReducer(reducer, initialState) 来创建状态和 dispatch 函数。

  3. 在组件中使用状态和 dispatch: 通过 state 访问当前状态值,通过 dispatch 触发 reducer 执行。

使用 Context 进行深层数据传递

通常,你会通过 props 将信息从父组件传递给子组件。 但是,如果要在组件树中深入传递一些 prop,或者树里的许多组件需要使用相同的 prop,那么传递 prop 可能会变得很麻烦。Context 允许父组件将一些信息提供给它下层的任何组件,不管该组件多深层也无需通过 props 逐层透传。

import React, { createContext, useContext, useState } from 'react';

// 创建一个Context
const MyContext = createContext();

// 创建一个包含Provider和Consumer的组件
const MyProvider = ({ children }) => {
  const [data, setData] = useState('Hello from Context');

  return (
    <MyContext.Provider value={{ data, setData }}>
      {children}
    </MyContext.Provider>
  );
};

// 使用 useContext 获取Context中的值
const ChildComponent = () => {
  const { data, setData } = useContext(MyContext);

  return (
    <div>
      <p>{data}</p>
      <button onClick={() => setData('Updated Context')}>Update Context</button>
    </div>
  );
};

// 在应用中使用Provider包裹需要共享数据的组件
const App = () => {
  return (
    <MyProvider>
      <ChildComponent />
    </MyProvider>
  );
};

export default App;

使用 Reducer 和 Context 进行状态扩展

Reducer 帮助你合并组件的状态更新逻辑同时Context 帮助你将信息深入传递给其他组件。

你可以将 reducers 和 context 组合在一起使用,以管理复杂应用的状态。

基于这种想法,使用 reducer 来管理一个具有复杂状态的父组件。组件树中任何深度的其他组件都可以通过 context 读取其状态。还可以 dispatch 一些 action 来更新状态。

合理的使用 Reducer 和 Context 可以帮助你更好的管理状态,同时也可以让你的代码更加简洁。

相比于Vuejs基于Proxy的响应式系统,使用代理的方式,以及provide和inject的方式,Vuejs的状态管理是在一个单独的WeekMap里面进行管理的,当变量的状态进行改变,可以激活effect副作用函数,并进行值的更新。而React的状态管理是与组件绑定的,当状态更新时,组件也会更新,但是当我们要进行一个全局状态的管理时,就得进行在父组件里面定义。

能基于具体的情况进行React状态的定义,才能更好的进行状态的管理。

前言

这一篇主要来聊一聊React的JSX如何被编译成VNode的。

Vue的render函数

在此之前,我们都知道,Vue是使用Vue模板方法的方式进行组件的声明。我们先来回顾一下Vue是怎么处理的。

Vue模板经过Vue的编译器编译成render函数,最后render函数返回VNode,然后再通过patch函数将VNode渲染成真实DOM。

具体的renderer细节在以前的章节里面有提到过,这边就不再赘述了。

React的JSX

react通过将组件编写的JSX映射到屏幕,以及组件中的状态发生了变化之后 React会将这些「变化」更新到屏幕上

JSX通过babel最终转化成React.createElement这种形式,至于babel是具体怎么将JSX转化成React.createElement的,我们可以通过babel的在线编译器来查看。

<div>
  < img src="avatar.png" className="profile" />
  <Hello />
</div>

会被bebel转化成如下:

React.createElement(
  "div",
  null,
  React.createElement("img", {
    src: "avatar.png",
    className: "profile"
  }),
  React.createElement(Hello, null)
);

Babel提供一个REPL在线编译器,可以在线将ES6代码转为ES5代码。转换后的代码,可以直接作为ES5代码插入网页运行,这边贴一个链接:https://babeljs.io/

vue-react

在转化过程中,babel在编译时会判断 JSX 中组件的首字母:

当首字母为小写时,其被认定为原生 DOM 标签,createElement 的第一个变量被编译为字符串

当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象

最终都会通过RenderDOM.render(...)方法进行挂载,如下:

ReactDOM.render(<App />,  document.getElementById("root"));

React的JSX转换具体是怎么做的呢

在react中,节点大致可以分成四个类别:

  • 原生标签节点
  • 文本节点
  • 函数组件
  • 类组件

如下所示:

class ClassComponent extends Component {
  static defaultProps = {
    color: "pink"
  };
  render() {
    return (
      <div className="border">
        <h3>ClassComponent</h3>
        <p className={this.props.color}>{this.props.name}</p >
      </div>
    );
  }
}

function FunctionComponent(props) {
  return (
    <div className="border">
      FunctionComponent
      <p>{props.name}</p >
    </div>
  );
}

const jsx = (
  <div className="border">
    <p>xx</p >
    < a href=" ">xxx</ a>
    <FunctionComponent name="函数组件" />
    <ClassComponent name="类组件" color="red" />
  </div>
);

这些类别最终都会被转化成React.createElement这种形式

vue-react

React.createElement其被调用时会传⼊标签类型type,标签属性props及若干子元素children,作用是生成一个虚拟Dom对象,如下所示:

function createElement(type, config, ...children) {
    if (config) {
        delete config.__self;
        delete config.__source;
    }
    // ! 源码中做了详细处理,⽐如过滤掉key、ref等
    const props = {
        ...config,
        children: children.map(child =>
   typeof child === "object" ? child : createTextNode(child)
  )
    };
    return {
        type,
        props
    };
}
function createTextNode(text) {
    return {
        type: TEXT,
        props: {
            children: [],
            nodeValue: text
        }
    };
}
export default {
    createElement
};

createElement会根据传入的节点信息进行一个判断:

  • 如果是原生标签节点, type 是字符串,如div、span
  • 如果是文本节点, type就没有,这里是 TEXT
  • 如果是函数组件,type 是函数名
  • 如果是类组件,type 是类名
  • 虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,使用方法如下:
ReactDOM.render(element, container[, callback])

当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 diff算法进行高效的更新

如果提供了可选的回调函数callback,该回调将在组件被渲染或更新之后被执行
render大致实现方法如下:

function render(vnode, container) {
    console.log("vnode", vnode); // 虚拟DOM对象
    // vnode _> node
    const node = createNode(vnode, container);
    container.appendChild(node);
}

// 创建真实DOM节点
function createNode(vnode, parentNode) {
    let node = null;
    const {type, props} = vnode;
    if (type === TEXT) {
        node = document.createTextNode("");
    } else if (typeof type === "string") {
        node = document.createElement(type);
    } else if (typeof type === "function") {
        node = type.isReactComponent
            ? updateClassComponent(vnode, parentNode)
        : updateFunctionComponent(vnode, parentNode);
    } else {
        node = document.createDocumentFragment();
    }
    reconcileChildren(props.children, node);
    updateNode(node, props);
    return node;
}

// 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node中
function reconcileChildren(children, node) {
    for (let i = 0; i < children.length; i++) {
        let child = children[i];
        if (Array.isArray(child)) {
            for (let j = 0; j < child.length; j++) {
                render(child[j], node);
            }
        } else {
            render(child, node);
        }
    }
}
function updateNode(node, nextVal) {
    Object.keys(nextVal)
        .filter(k => k !== "children")
        .forEach(k => {
        if (k.slice(0, 2) === "on") {
            let eventName = k.slice(2).toLocaleLowerCase();
            node.addEventListener(eventName, nextVal[k]);
        } else {
            node[k] = nextVal[k];
        }
    });
}

// 返回真实dom节点
// 执行函数
function updateFunctionComponent(vnode, parentNode) {
    const {type, props} = vnode;
    let vvnode = type(props);
    const node = createNode(vvnode, parentNode);
    return node;
}

// 返回真实dom节点
// 先实例化,再执行render函数
function updateClassComponent(vnode, parentNode) {
    const {type, props} = vnode;
    let cmp = new type(props);
    const vvnode = cmp.render();
    const node = createNode(vvnode, parentNode);
    return node;
}
export default {
    render
};

在react源码中,虚拟Dom转化成真实Dom整体流程如下图所示:

vue-react

其渲染流程如下所示:

  • 使用React.createElementJSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...) Babel帮助我们完成了这个转换的过程。

  • createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象

  • ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM

前言

最近面了得物春季青训营,项目要求掌握React。之前写过一段时间Next.js,对jsx的理解还算是有些片面,作为Vuejs转React的一个开发者,打算先将Vuejs和React的功能特性做一个对比,然后再根据项目入手React。

Vuejs和React的相同点

vue-react

Vue和React相同点非常多:

  • 都使用Virtural DOM
  • 都使用组件化思想,流程基本一致
  • 都是响应式,推崇单向数据流(Vue的v-model指令允许在表单元素上进行双向数据绑定。)
  • 都有成熟的社区,都支持服务端渲染

Vue和React实现原理和流程基本一致,都是使用Virtual DOM + Diff算法。

不管是Vue的template模板 + options api写法(即使用SFC体系),还是React的Class或者Function(js 的class写法也是function函数的一种)的jsx写法,底层最终都是为了生成render函数。

render函数执行返回VNode(虚拟DOM树)。 当每一次UI更新时,总会根据render重新生成最新的VNode,然后跟以前缓存起来老的VNode进行比对,再使用Diff算法(框架核心)去真正更新真实DOM(虚拟DOM是JS对象结构,同样在JS引擎中,而真实DOM在浏览器渲染引擎中,所以操作虚拟DOM比操作真实DOM开销要小的多)。

Vue和React通用流程:

vue template/react jsx -> render函数 -> 生成VNode -> 当有变化时,新老VNode diff -> diff算法对比,并真正去更新真实DOM。

核心还是Virtual DOM,VNode的好处不用多说:

  • 减少直接操作DOM。框架给我们提供了屏蔽底层dom书写的方式,减少频繁的整更新dom,同时也使得数据驱动视图

  • 为函数式UI编程提供可能(React核心思想)

  • 可以跨平台,渲染到DOM(web)之外的平台。比如ReactNative,Weex

vue-react

Vue说自己是框架,是因为官方提供了从声明式渲染到构建工具一整套东西。

React说自己是库,是因为官方只提供了React.js这个核心库,路由、状态管理这些都是社区第三方提供了,最终整合成了一套方案。

Vue和React的区别

组件实现不同

Vuejs实现

Vue源码实现是把options挂载到Vue核心类上,然后再new Vue({options})拿到实例(vue组件的script导出的是一个挂满options的纯对象而已)。

所以options api中的this指向内部Vue实例,对用户是不透明的,所以需要文档去说明this.$slot、this.$xxx这些api。另外Vue插件都是基于Vue原型类基础之上建立的,这也是Vue插件使用Vue.install的原因,因为要确保第三方库的Vue和当前应用的Vue对象是同一个。

React实现

React内部实现比较简单,直接定义render函数以生成VNode ,而React内部使用了四大组件类包装VNode,不同类型的VNode使用相应的组件类处理,职责划分清晰明了(后面的Diff算法也非常清晰)。React类组件都是继承自React.Component类,其this指向用户自定义的类,对用户来说是透明的。

响应式原理不同

Vuejs实现

  • Vue依赖收集,自动优化,数据可变。
  • Vue递归监听data的所有属性,直接修改。
  • 当数据改变时,自动找到引用组件重新渲染。

React实现

  • React基于状态机,手动优化,数据不可变,需要setState驱动新的State替换老的State。
  • 当数据改变时,以组件为根目录,默认全部重新渲染

diff算法不同

Vue基于snabbdom库,它有较好的速度以及模块机制。Vue Diff使用双向链表,边对比,边更新DOM。

React主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。

事件机制不同

Vuejs实现

  • Vue原生事件使用标准Web事件
  • Vue组件自定义事件机制,是父子组件通信基础
  • Vue合理利用了snabbdom库的模块插件

React实现

  • React原生事件被包装,所有事件都冒泡到顶层document监听,然后在这里合成事件下发。基于这套,可以跨端使用事件机制,而不是和Web DOM强绑定。
  • React组件上无事件,父子组件通信使用props