这是我参与「第四届青训营 」笔记创作活动的第10天
响应式系统与React
一.React 的历史与应用
1.应用场景:
- 前端应用开发,如Facebook,Instagram
- 移动原生应用开发,如Instagram,Discord
- 结合 Electron,进行桌面应用开发
- React 语法书写3D图形应用,以及VR AR
2.历史:
-
2010 年 Facebook 在其 php 生态中,引入了 xhp 框架,首次引入了组合式组件的思想,启发了后来的 React 的设计
-
2011 年 Jordan Walke 创造了 FaxJS,也就是后来的 React 原型
-
2012 年 在 Facebook 收购 Instagram 后,该 FaxJS 项目在内部得到使用,Jordan Walke 基于 FaxJS 的经验,创造了 React
-
2013 年 React 正式开源,在2013 JSConf 上 Jordan Walke 介绍了这款全选的框架
有人反对,说这是一种倒退,在之前 Java 写网页也是用这种组合式来写的,所以说这是一种倒退
-
2014 年 - 今天 生态大爆发,各种围绕 React 的新工具/新框架开始涌现
二.React 的设计思路
1.UI 编程的痛点:
-
购物案例,每单用户点击不同的产品参数时,UI上的价格也要同时变化,导致出现许多的 onClick 事件
- 状态更新,UI 不会自动更新,需要手动地调用 DOM 进行更新
- 欠缺基本的代码层面的封装和隔离,代码层面没有组件化。
- UI 之间的数据依赖关系,需要手动维护,如果依赖链路长,则会遇到“Callback Hell”
转换式系统:如编译系统,是一个 输入 - 求解 - 输出 的过程
前端代码:发生某个事件,就会发生一连续的变化,所以Javascript是异步编程.。 [事件 -> 执行既定的回调 -> 状态变更 -> UI变更]
响应式系统:如监控系统,在火灾发生时,触发报警响应等;如人烧开水,水开了我再关电,在这个等待水烧开期间,我们可以去干别的事,这就叫异步编程。 [事件 -> 执行既定的回调 -> 状态变更]
所以我们希望:
- 状态更新,UI 自动更新
- 前端代码组件化,可复用,可封装
- 状态之间的相互依赖关系,只需声明即可
2.组件化:
-
一个简单的结构,用树状表示出来,注意:这不叫DOM树,这仅仅是自己划分的
总结:
- 组件是 组件的组合/原子组件
- 组件内拥有状态,外部不可见
- 父组件可将状态传入组件内部
思考:【当前价格】 状态属于谁?
答:【当前价格】 属于 Root 节点!因为只有父组件才能控制子组件,当两个组件要共享一个状态时,要把状态上移,如果把状态都上移到 Root ,这就违背了组件复用的初衷了。大部分情况下,状态具有局部性,如颜色只需要要放在一个组件内。(伏笔:五.状态管理库)
结论:如果想要两个组件共享一个状态,则这个状态归属于两个节点向上寻找到最近的祖宗节点。
思考:【当前价格】怎么改变 ,它属于Root 节点,但触发改变时却属于 型号 和 颜色 节点。
答:将 onChangeValue()函数 从 Root 节点 向下传递。
思考:
- React 是单向数据流,还是双向数据流?
- 如何解决状态不合理上升的问题?
- 组件的状态改变后,如何更新 DOM?
答:
- 单向数据流,永远都是父组件往子组件传递,但父组件可以将一个函数传递给子组件,让子组件执行这个函数来改变父组件。
- 见后
- 见后
总结:
- 组件声明了状态和 UI 的映射。(数学上的函数关系,输入什么状态,传出什么UI)
- 组件有 Props(内部私有) / State(外部传入) 两种状态。
- “组件”可由其他组件拼装而成。
思考:组件代码是什么样子?
答:
- 组件内部拥有私有状态 State
- 组件接受外部的 Props 状态提供复用性
- 根据当前的 State / Props ,返回一个 UI
示例:
3.生命周期
- 挂载到实际的DOM上,即 Mounting
- 状态发生改变时,重新执行 render 函数
- 更新到 DOM 上,即 React updates DOM and refs
- 挂载 - Mounting
- 卸载 - Unmounting
- 状态改变时 - Updating
三.React (hooks)的写法
示例代码:
-
import React, { useState } from 'react'; function Example() { //声明一个新状态 //useState传入初始值 const [count, setCount] = useState(0); //count 状态本身,setCount 改变方式 return ( <div> <p> You clicked {count} times </p> <button onClick={()=> setCount(count + 1)}> Click me </button> </div> ) } -
import React, {useState, useEffect} from 'react'; function Example() { count [count, setCount] = useState(0); //改变标题,副作用都用 useEffect ,都有个明确的执行时期 useEffect(() => { docunment.title = `You clicked {count} times`; }) return ( <div> <p> You clicked {count} times </p> <button onClick={()=> setCount(count + 1)}> Click me </button> </div> ) }副作用:一个组件内单独执行一个语句,执行简单的语句不会影响外部,但如发起请求等就会影响到外部。
useState:
传入一个初始值,返回一个状态,和set该状态的函数,用户可以通过调用该函数,来实现状态的修改。
useEffect:
传入一个函数,和一个数组,数组是状态的数组,称作依赖项,该函数在 mount 时,和依赖性被 set 的时候会执行。
有“副作用”的函数,要传入 useEffect 来执行。副作用代表除了单纯的计算之外,还要做其它的一些事情,比如网络请求,更新DOM,localStorage存储数据等。
Hook 使用法则:
- 不要在循环条件或嵌套函数中调用 hook
四.React 的实现
1.Problems
- JSX 不符合 JS 标准语法
- 返回的 JSX 发生改变时,如何更新 DOM
- State/Props 更新时,要重新触发 render 函数
1.1Problem1
通过转译
1.2Problem2
代码返回的类似于DOM片断,状态改变时DOM也改变。但是重新删除原来的DOM后,再添加新的DOM很消耗性能。
要实现diff的更新,能使DOM消耗低,但是diff的计算又不能太耗时。那么怎样平衡呢?
Virtual DOM (虚拟 DOM) :
Virtual DOM (虚拟 DOM)是一种用于和真实DOM同步,而在 JS 内存中维护的一个对象,它具有和DOM类似的树状结构,并和DOM可以建立一一对应的关系。
它赋予了 React 声明式的 API:您告诉 React 希望让 UI 是什么状态,React 就确保 DOM 匹配该状态。这使您可以从属性操作、事件处理和手动 DOM 更新这些在构建应用程序时必要的操作中解放出来。
指令式编程:如C语言,一步一步告诉程序应该怎么做。如烧水,第一步装水,然后插电,接着...
声明式编程:告诉它,烧水
响应式编程:声明式的一个类别,但又可以根据某个状态改变时,又能自动更新UI等。
问:既然所有的前端框架都是声明式的,为什么不把声明式的能力接入到浏览器里?
答:作为应用平台,则不能提供太高层的内容,仅仅提供底层,毕竟如果有人就是喜欢指令式编程呢?
性能问题:位置很高的父组件发生改变时,要影响的子组件过于多时,如何保证性能?
2.How to Diff ?
TeadeOff(权衡):
- 更新次数少
- 计算速度快
完美的最小 Diff 算法,想要)O(n^3)的复杂度。
牺牲理论最小 Diff,换取时间,得到O(n)复杂度的算法。
Heuristic O(n) Algorithm
| 元素种类 | 实现方式 |
|---|---|
| 不同类型的元素 | 替换 |
| 同类型的DOM元素 | 更新 |
| 同类型的组件元素 | 递归 |
五.React 状态管理库
1.核心思想
降低复用性,组件内却依赖于外部的Store中
2.推荐
-
redux
- 用的较多
- 最近的设计有些问题
-
xstate
- 状态集
-
mobx
-
recoil
将状态抽离到UI外部进行统一管理
3.状态机
4.Modern.js/Reduck
六.应用级框架科普
-
NEXT.js
- 硅谷明星创业公司Vercel的React开发框架,稳定,开发体验好,支持Unbundled Dev, SWC等,其同样有Serverless 一键部署平台帮助开发者快速完成部署。口号是"Let's Make Web Faster"
-
Modern.js
- 字节跳动Web Infra团队研发的全栈开发框架,内置了很多开箱即用的能力与最佳实践,可以减少很多调研选择工具的时间。
-
Blitz
- 无API思想的全栈开发框架,开发过程中无需写API调用与CRUD逻辑,适合前后端紧密结合的小团队项目。