React
React
也是现如今最流行的前端框架,也是很多大厂面试必备。React
与 Vue
虽有不同,但同样作为一款 UI
框架,虽然实现可能不一样,但在一些理念上还是有相似的,例如数据驱动、组件化、虚拟 dom
等。这里就主要列举一些 React 中独有的概念。
React16 提出了 Fiber 结构,其能够将任务分片,划分优先级,同时能够实现类似于操作系统中对线程的抢占式调度,非常强大。
React 的核心流程可以分为两个部分:
要了解 Fiber,我们首先来看为什么需要它?
问题: 随着应用变得越来越庞大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿和掉帧的情况。而关键点,便是 同步阻塞。在之前的调度算法中,React 需要实例化每个类组件,生成一颗组件树,使用 同步递归 的方式进行遍历渲染,而这个过程最大的问题就是无法 暂停和恢复。
解决方案: 解决同步阻塞的方法,通常有两种: 异步 与 任务分割。而 React Fiber 便是为了实现任务分割而诞生的。
简述:
核心:
class Fiber {constructor(instance) {this.instance = instance;// 指向第一个 child 节点this.child = child;// 指向父节点this.return = parent;// 指向第一个兄弟节点this.sibling = previous;}}
链表树遍历算法: 通过 节点保存与映射,便能够随时地进行 停止和重启,这样便能达到实现任务分割的基本前提;
任务分割,React 中的渲染更新可以分成两个阶段:
分散执行: 任务分割后,就可以把小任务单元分散到浏览器的空闲期间去排队执行,而实现的关键是两个新 API: requestIdleCallback
与 requestAnimationFrame
requestIdleCallback
处理,这是个浏览器提供的事件循环空闲期的回调函数,需要 pollyfill,而且拥有 deadline 参数,限制执行事件,以继续切分任务;requestAnimationFrame
处理;// 类似于这样的方式requestIdleCallback((deadline) => {// 当有空闲时间时,我们执行一个组件渲染;// 把任务塞到一个个碎片时间中去;while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&nextComponent) {nextComponent = performWork(nextComponent);}});
Tips:
Fiber 其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。当遇到进程阻塞的问题时,任务分割、异步调用 和 缓存策略 是三个显著的解决思路。
虽然DOM是由JavaScript实现的,但是在浏览器中都是把DOM和JavaScript分开来实现的,比如IE中,JavaScript的实现名为JScript,放在jscript.dll文件中,而DOM则放在另一个叫做mshtml.dll的库中。在Safari中,DOM和渲染是使用Webkit中的WebCore实现,而JavaScript是由独立的JavaScriptCore引擎实现,同样在Chrome中,同样是使用WebCore来实现渲染,而JavaScript引擎则是他们自己研发的V8引擎。
由于DOM和JavaScript是被分开独立实现的,因此,每一次在通过js操作DOM的时候,就需要先去连接js和DOM,我们可以这样理解:把DOM和JavaScript比作两个岛,他们之间通过一个收费的桥连接着,每一次访问DOM的时候,就需要经过这座桥,并且给“过路费”,访问的次数越多,路费就会越高,并且访问到DOM后,操作具体的DOM还需要给“操作费”,由于浏览器访问DOM的操作很多,因此,“路费”和“操作费”自然会增加,这就是为什么操作DOM会很慢的原因
在新版本中,React 官方对生命周期有了新的 变动建议:
getDerivedStateFromProps
替换componentWillMount
;getSnapshotBeforeUpdate
替换componentWillUpdate
;componentWillReceiveProps
;其实该变动的原因,正是由于上述提到的 Fiber。首先,从上面我们知道 React 可以分成 reconciliation 与 commit 两个阶段,对应的生命周期如下:
reconciliation:
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
commit:
componentDidMount
componentDidUpdate
componentWillUnmount
在 Fiber 中,reconciliation 阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致 reconciliation 中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误。
新版的建议生命周期如下:
class Component extends React.Component {// 替换 `componentWillReceiveProps` ,// 初始化和 update 时被调用// 静态函数,无法使用 thisstatic getDerivedStateFromProps(nextProps, prevState) {}// 判断是否需要更新组件// 可以用于组件性能优化shouldComponentUpdate(nextProps, nextState) {}// 组件被挂载后触发componentDidMount() {}// 替换 componentWillUpdate// 可以在更新之前获取最新 dom 数据getSnapshotBeforeUpdate() {}// 组件更新后调用componentDidUpdate() {}// 组件即将销毁componentWillUnmount() {}// 组件已销毁componentDidUnMount() {}}
使用建议:
constructor
初始化 state;componentDidMount
中进行事件监听,并在componentWillUnmount
中解绑事件;componentDidMount
中进行数据的请求,而不是在componentWillMount
;getDerivedStateFromProps(nextProps, prevState)
;public static getDerivedStateFromProps(nextProps, prevState) {// 当新 props 中的 data 发生变化时,同步更新到 state 上if (nextProps.data !== prevState.data) {return {data: nextProps.data}} else {return null1}}
componentDidUpdate
监听 props 或者 state 的变化,例如:componentDidUpdate(prevProps) {// 当 id 发生变化时,重新获取数据if (this.props.id !== prevProps.id) {this.fetchData(this.props.id);}}
componentDidUpdate
使用setState
时,必须加条件,否则将进入死循环;getSnapshotBeforeUpdate(prevProps, prevState)
可以在更新之前获取最新的渲染数据,它的调用是在 render 之后, mounted 之前;shouldComponentUpdate
: 默认每次调用setState
,一定会最终走到 diff 阶段,但可以通过shouldComponentUpdate
的生命钩子返回false
来直接阻止后面的逻辑执行,通常是用于做条件渲染,优化渲染的性能。key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确, 更快的拿到 oldVnode 中对应的 vnode 节点。
更准确 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快 利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map 会比遍历更快。)
在了解setState
之前,我们先来简单了解下 React 一个包装结构: Transaction:
setState
: React 中用于修改状态,更新视图。它具有以下特点:
异步与同步: setState
并不是单纯的异步或同步,这其实与调用时的环境相关:
setState
是"异步"的;setState
的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents
队列中等待执行;否则,开始执行batchedUpdates
队列更新;componentDidUpdate
是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;setState
后马上从this.state
上获取更新后的值。setState
其实是可以传入第二个参数的。setState(updater, callback)
,在回调中即可获取最新值;setState
是同步的,可以马上获取更新后的值;setTimeout
是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;批量更新: 在 合成事件 和 生命周期钩子 中,setState
更新队列时,存储的是 合并状态(Object.assign
)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;
函数式: 由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn)
,在fn
中返回新的state
对象即可,例如this.state((state, props) => newState);
setState
的批量更新的逻辑,传入的函数将会被 顺序调用;注意事项:
setState
,React 会报错警告,通常有两种解决办法:componentWillUnmount
中标记为 true,在setState
前进行判断;强制React组件渲染的方法有多种,但本质上是相同的。 首先是使用this.forceUpdate() ,它会跳过shouldComponentUpdate
someMethod() {// Force a render without state change...this.forceUpdate();}
首先回顾以下原生事件的两个方法:event.stopImmediatePropagation
和 event.stopPropagation
,前者可以阻止同一 dom 上的后续事件的执行以及阻止冒泡,后者仅能阻止冒泡。
在 react 内通过 onClick 绑定的事件,实际上并没有把点击事件绑定到对应的元素上,而是统一放到了 document 上处理。点击一个元素,首先触发这个元素的原生绑定事件(这里不讨论捕获),接着事件冒泡,到了 document 上,先触发 dispatch,即分发 react 的合成事件,这个触发过程也是和冒泡类似,从里向外的。
dispatch 结束后,触发 document 上剩余的原生事件,也就是说可以认为 dispatch 是 document 上的第一个原生绑定事件,这个事件内进行 react 合成事件的分发。
原生绑定的回调事件中获取到的是原生事件。通过 react 在 jsx 内 onClick 绑定的回调事件中获取到的是合成事件。
如果你有试过输出 react 事件中的 event,你就会发现这个 event 好像和我们看到的 dom 事件中的 event 不太一样,那是因为 react 在进行 dom 事件绑定时,不是直接绑定事件的,而是通过所谓的合成事件(SyntheticEvent)进行委托管理的,它是原生事件进行封装后的结果,你可以通过 nativeEvent 获取原生事件。
onClick = { this.clickHandle.bind(this,[other params]) }
在 JSX 中可以直接使用 onClick = { this.clickHandle }
绑定事件
constructor(props){super(props);this.clickHandle = this.handleClick.bind(this);render() {return <button onClick = { this.handleClick }>click</button>;}}
箭头函数的特点是, 能够自动绑定定义此函数作用域的 this 。
箭头函数脱离不了函数这个范围,因此其 this 指向依旧是最终调用该函数的作用域 this。(这句话是否正确或者合理目前个人并不是很清楚)
所以也可以通过这个特性实现绑定。
onClick={ () => this.handleClick() }
HOC(Higher Order Componennt) 是在 React 机制下社区形成的一种组件模式,在很多第三方开源库中表现强大。
简述:
用法:
属性代理 (Props Proxy): 返回出一个组件,它基于被包裹组件进行 功能增强;
function proxyHoc(Comp) {return class extends React.Component {render() {const newProps = {name: "tayde",age: 1,};return <Comp {...this.props} {...newProps} />;}};}
function withOnChange(Comp) {return class extends React.Component {constructor(props) {super(props);this.state = {name: "",};}onChangeName = () => {this.setState({name: "dongdong",});};render() {const newProps = {value: this.state.name,onChange: this.onChangeName,};return <Comp {...this.props} {...newProps} />;}};}
使用姿势如下,这样就能非常快速的将一个 Input
组件转化成受控组件。
const NameInput = (props) => <input name="name" {...props} />;export default withOnChange(NameInput);
function withMask(Comp) {return class extends React.Component {render() {return (<div><Comp {...this.props} /><div style={{width: '100%',height: '100%',backgroundColor: 'rgba(0, 0, 0, .6)',}}</div>)}}}
反向继承 (Inheritance Inversion): 返回出一个组件,继承于被包裹组件,常用于以下操作:
function IIHoc(Comp) {return class extends Comp {render() {return super.render();}};}
渲染劫持 (Render Highjacking)
function withLoading(Comp) {return class extends Comp {render() {if (this.props.isLoading) {return <Loading />;} else {return super.render();}}};}
操作状态 (Operate State): 可以直接通过 this.state
获取到被包裹组件的状态,并进行操作。但这样的操作容易使 state 变得难以追踪,不易维护,谨慎使用。
应用场景:
function withAdminAuth(WrappedComponent) {return class extends React.Component {constructor(props) {super(props);this.state = {isAdmin: false,};}async componentWillMount() {const currentRole = await getCurrentUserRole();this.setState({isAdmin: currentRole === "Admin",});}render() {if (this.state.isAdmin) {return <Comp {...this.props} />;} else {return <div>您没有权限查看该页面,请联系管理员!</div>;}}};}
function withTiming(Comp) {return class extends Comp {constructor(props) {super(props);this.start = Date.now();this.end = 0;}componentDidMount() {super.componentDidMount && super.componentDidMount();this.end = Date.now();console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);}render() {return super.render();}};}
使用注意:
React.forwardRef
;在调用组件时,引入一个函数类型的 prop,这个 prop 定义了组件的渲染方式。
而 render props 本质实际上是使用到了回调的方式来通信。只不过在传统的 js 回调是在构造函数中进行初始化(使用回调函数作为参数),而在 react 中,现在可以通过 props 传入该回调函数,就是我们所介绍的 render prop。
回调的目的是渲染子组件,而渲染的外部细节需要通过父组件注入,实现了控制反转。
组件间进行消息传递(通信),组件间通信大体有下面几种情况:
简单的父子,兄弟之间的通信用Props
传值,用Callback
进行回调赋值
作为中间介子进行事件汇总及发布,参考发布订阅模式
Context
通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props
属性。Redux就是通过这个方式注入
// React.createContext:创建一个上下文的容器(组件), defaultValue可以设置共享的默认数据const {Provider, Consumer} = React.createContext(defaultValue);// Provider(生产者): 用于生产共享数据的地方。value:放置共享的数据<Provider value={/*共享的数据*/}>/*里面可以渲染对应的内容*/</Provider>// Consumer(消费者): 消费供应商(Provider 上面提到的)产生数据。Consumer需要嵌套在生产者下面。才能通过回调的方式拿到共享的数据源。当然也可以单独使用,那就只能消费到上文提到的defaultValue<Consumer>{value => /*根据上下文 进行渲染相应内容*/}</Consumer>
React
中通常使用 类定义 或者 函数定义 创建组件:
在类定义中,我们可以使用到许多 React
特性,例如 state
、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8
版本推出了一个新功能 (React Hooks)
,通过它,可以更好的在函数定义组件中使用 React
特性。
render props
/ HOC
也是为了复用,相比于它们,Hooks
作为官方的底层 API
,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;this
的指向问题;useEffect
中使用useState
,React 会报错提示;useState
)用于定义组件的 State,其到类定义中this.state
的功能;
// useState 只接受一个参数: 初始状态// 返回的是组件名和更改该组件对应的函数const [flag, setFlag] = useState(true);// 修改状态setFlag(false);// 上面的代码映射到类定义中:this.state = {flag: true,};const flag = this.state.flag;const setFlag = (bool) => {this.setState({flag: bool,});};
useEffect
)类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect
),这里可以看做componentDidMount
、componentDidUpdate
和componentWillUnmount
的结合。
useEffect(callback, [source])
接受两个参数callback
: 钩子回调函数;source
: 设置触发条件,仅当 source 发生改变时才会触发;useEffect
钩子在没有传入[source]
参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;useEffect(() => {// 组件挂载后执行事件绑定console.log("on");addEventListener();// 组件 update 时会执行事件解绑return () => {console.log("off");removeEventListener();};}, [source]);// 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):// --- DidMount ---// 'on'// --- DidUpdate ---// 'off'// 'on'// --- DidUpdate ---// 'off'// 'on'// --- WillUnmount ---// 'off'
通过第二个参数,我们便可模拟出几个常用的生命周期:
componentDidMount
: 传入[]
时,就只会在初始化时调用一次;const useMount = (fn) => useEffect(fn, []);
componentWillUnmount
: 传入[]
,回调中的返回的函数也只会被最终执行一次;const useUnmount = (fn) => useEffect(() => fn, []);
mounted
: 可以使用 useState 封装成一个高度可复用的 mounted 状态;const useMounted = () => {const [mounted, setMounted] = useState(false);useEffect(() => {!mounted && setMounted(true);return () => setMounted(false);}, []);return mounted;};
componentDidUpdate
: useEffect
每次均会执行,其实就是排除了 DidMount 后即可;const mounted = useMounted();useEffect(() => {mounted && fn();});
useContext
: 获取 context 对象import React, { useContext } from "react";const colorContext = React.createContext("gray");function Bar() {const color = useContext(colorContext);return <div>{color}</div>;}function Foo() {return <Bar />;}function App() {return (<colorContext.Provider value={"red"} /><Foo /></colorContext.Provider>);}
useReducer
: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:
useContext
的全局性,可以完成一个轻量级的 Redux;(easy-peasy)const initialState = {count: 0};function reducer(state, action) {switch (action.type) {case 'increment':return {count: state.count + 1};case 'decrement':return {count: state.count - 1};default:throw new Error();}}function Counter() {const [state, dispatch] = useReducer(reducer, initialState);return (<>Count: {state.count}<button onClick={() => dispatch({type: 'decrement'})}>-</button><button onClick={() => dispatch({type: 'increment'})}>+</button></>);}
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;const memoizedCallback = useCallback(() => {doSomething(a, b);},[a, b],);
useMemo
: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useRef
: 获取组件的真实节点;useLayoutEffect
:useEffect
类似,只是区别于执行时间点的不同。useEffect
属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect
则会真正渲染后才触发;useXxxxx
)基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的useMounted
。又例如,我们需要每个页面自定义标题:
function useTitle(title) {useEffect(() => {document.title = title;});}// 使用:function Home() {const title = "我是首页";useTitle(title);return <div>{title}</div>;}
SSR,俗称 服务端渲染 (Server Side Render),讲人话就是: 直接在服务端层获取数据,渲染出完成的 HTML 文件,直接返回给用户浏览器访问。
前后端分离: 前端与服务端隔离,前端动态获取数据,渲染页面。
痛点:
首屏渲染性能瓶颈:
SEO 问题: 由于页面初始状态为空,因此爬虫无法获取页面中任何有效数据,因此对搜索引擎不友好。
最初的服务端渲染,便没有这些问题。但我们不能返璞归真,既要保证现有的前端独立的开发模式,又要由服务端渲染,因此我们使用 React SSR。
原理:
条件: Node 中间层、 React / Vue 等框架。 结构大概如下:
开发流程: (此处以 React + Router + Redux + Koa 为例)
1、在同个项目中,搭建 前后端部分,常规结构:
build
public
src
2、server 中使用 Koa 路由监听 页面访问:
import * as Router from "koa-router";const router = new Router();// 如果中间也提供 Api 层router.use("/api/home", async () => {// 返回数据});router.get("*", async (ctx) => {// 返回 HTML});
// 前端页面路由import { pages } from "../../client/app";import { matchPath } from "react-router-dom";// 使用 react-router 库提供的一个匹配方法const matchPage = matchPath(ctx.req.url, page);
4、通过页面路由的配置进行 数据获取。通常可以在页面路由中增加 SSR 相关的静态配置,用于抽象逻辑,可以保证服务端逻辑的通用性,如:
class HomePage extends React.Component{public static ssrConfig = {cache: true,fetch() {// 请求获取数据}}}
获取数据通常有两种情况:
const data = await matchPage.ssrConfig.fetch();
// 页面路由class HomePage extends React.Component{public static ssrConfig = {fetch: {url: '/api/home',}}}// 根据规则匹配出对应的数据获取方法// 这里的规则可以自由,只要能匹配出正确的方法即可const controller = matchController(ssrConfig.fetch.url)// 获取数据const data = await controller(ctx)
5、创建 Redux store,并将数据dispatch
到里面:
import { createStore } from "redux";// 获取 Clinet层 reducer// 必须复用前端层的逻辑,才能保证一致性;import { reducers } from "../../client/store";// 创建 storeconst store = createStore(reducers);// 获取配置好的 Actionconst action = ssrConfig.action;// 存储数据store.dispatch(createAction(action)(data));
renderToString
将 React Virtual Dom 渲染成 字符串:import * as ReactDOMServer from "react-dom/server";import { Provider } from "react-redux";// 获取 Clinet 层根组件import { App } from "../../client/app";const AppString = ReactDOMServer.renderToString(<Provider store={store}><StaticRouter location={ctx.req.url} context={{}}><App /></StaticRouter></Provider>);
7、将 AppString 包装成完整的 html 文件格式;
8、此时,已经能生成完整的 HTML 文件。但只是个纯静态的页面,没有样式没有交互。接下来我们就是要插入 JS 与 CSS。我们可以通过访问前端打包后生成的asset-manifest.json
文件来获取相应的文件路径,并同样注入到 Html 中引用。
const html = `<html lang="zh"><head></head><link href="${cssPath}" rel="stylesheet" /><body><div id="App">${AppString}</div><script src="${scriptPath}"></script></body></html>`;
import serialize from "serialize-javascript";// 获取数据const initState = store.getState();const html = `<!DOCTYPE html><html lang="zh"><head></head><body><div id="App"></div><script type="application/json" id="SSR_HYDRATED_DATA">${serialize(initState)}</script></body></html>`;ctx.status = 200;ctx.body = html;
Tips:
这里比较特别的有两点:
使用了
serialize-javascript
序列化 store, 替代了JSON.stringify
,保证数据的安全性,避免代码注入和 XSS 攻击;使用 json 进行传输,可以获得更快的加载速度;
const hydratedEl = document.getElementById("SSR_HYDRATED_DATA");const hydrateData = JSON.parse(hydratedEl.textContent);// 使用初始 state 创建 Redux storeconst store = createStore(reducer, hydrateData);
history是一个独立的第三方js库,可以用来兼容在不同浏览器、不同环境下对历史记录的管理,拥有统一的API。具体来说里面的history分为三类
实现URL与UI界面的同步。其中在react-router中,URL对应Location对象,而UI是由react components来决定的,这样就转变成location与components之间的同步问题
在react-router中最主要的component是Router RouterContext Link,history库起到了中间桥梁的作用 以browserHistory(一种history类型:一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象)为例 : browserHistory进行路由state管理,主要通过sessionStorage
//保存 路由state(router state)function saveState(key, state) {...window.sessionStorage.setItem(createKey(key), JSON.stringify(state));}//读取路由statefunction readState(key) {...json = window.sessionStorage.getItem(createKey(key));return JSON.parse(json);}
参考:从路由原理出发,深入阅读理解react-router 4.0的源码React Router原理
函数式编程是一种 编程范式,你可以理解为一种软件架构的思维模式。它有着独立一套理论基础与边界法则,追求的是 更简洁、可预测、高复用、易测试。其实在现有的众多知名库中,都蕴含着丰富的函数式编程思想,如 React / Redux 等。
常见的编程范式:
函数式编程的理念:
纯函数(确定性函数): 是函数式编程的基础,可以使程序变得灵活,高度可拓展,可维护;
优势:
条件:
new Date()
或者Math.randon()
等这种不可控因素;我们常用到的许多 API 或者工具函数,它们都具有着纯函数的特点, 如split / join / map
;
函数复合: 将多个函数进行组合后调用,可以实现将一个个函数单元进行组合,达成最后的目标;
扁平化嵌套: 首先,我们一定能想到组合函数最简单的操作就是 包裹,因为在 JS 中,函数也可以当做参数:
f(g(k(x)))
: 嵌套地狱,可读性低,当函数复杂后,容易让人一脸懵逼;xxx(f, g, k)(x)
结果传递: 如果想实现上面的方式,那也就是xxx
函数要实现的便是: 执行结果在各个函数之间的执行传递;
reduce
,它可以按数组的顺序依次执行,传递执行结果;pipe
,用于函数组合:// ...fs: 将函数组合成数组;// Array.prototype.reduce 进行组合;// p: 初始参数;const pipe = (...fs) => (p) => fs.reduce((v, f) => f(v), p);
使用: 实现一个 驼峰命名 转 中划线命名 的功能:
// 'Guo DongDong' --> 'guo-dongdong'// 函数组合式写法const toLowerCase = (str) => str.toLowerCase();const join = curry((str, arr) => arr.join(str));const split = curry((splitOn, str) => str.split(splitOn));const toSlug = pipe(toLowerCase, split(" "), join("_"), encodeURIComponent);console.log(toSlug("Guo DongDong"));
好处:
const log = curry((label, x) => {console.log(`${label}: ${x}`);return x;});const toSlug = pipe(toLowerCase,log("toLowerCase output"),split(" "),log("split output"),join("_"),log("join output"),encodeURIComponent);
Tips:
一些工具纯函数可直接引用
lodash/fp
,例如curry/map/split
等,并不需要像我们上面这样自己实现;
数据不可变性(immutable): 这是一种数据理念,也是函数式编程中的核心理念之一:
const
。使用const
创建一个对象后,它的属性仍然可以被修改;Object.freeze
: 冻结对象,但freeze
仍无法保证深层的属性不被串改;immutable.js
: js 中的数据不可变库,它保证了数据不可变,在 React 生态中被广泛应用,大大提升了性能与稳定性;trie
数据结构:避免不同函数之间的 状态共享,数据的传递使用复制或全新对象,遵守数据不可变原则;
避免从函数内部 改变外部状态,例如改变了全局作用域或父级作用域上的变量值,可能会导致其它单位错误;
避免在单元函数内部执行一些 副作用,应该将这些操作抽离成更独立的工具单元;
高阶函数: 是指 以函数为参数,返回一个新的增强函数 的一类函数,它通常用于:
函数式编程的好处:
总结:
Tips:
其实我们很难也不需要在面试过程中去完美地阐述出整套思想,这里也只是浅尝辄止,一些个人理解而已。博主也是初级小菜鸟,停留在表面而已,只求对大家能有所帮助,轻喷 🤣;
我个人觉得: 这些编程范式之间,其实并不矛盾,各有各的 优劣势。
理解和学习它们的理念与优势,合理地 设计融合,将优秀的软件编程思想用于提升我们应用;
所有设计思想,最终的目标一定是使我们的应用更加 解耦颗粒化、易拓展、易测试、高复用,开发更为高效和安全;
有一些库能让大家很快地接触和运用函数思想:
Underscore.js
/Lodash/fp
/Rxjs
等。
在最新的React16
版本中,我们可以直接在url后加上?react_pref
,就可以在chrome浏览器的performance
,我们可以查看User Timeing
来查看组件的加载时间。
render
里面尽量减少新建变量和bind
函数,传递参数是尽量减少传递参数的数量。React.PureComponent
,会对数据进行浅层比较shouldComponentUpdate
函数Immutable.js
进行数据管理函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。
区别 | 函数组件 | 类组件 |
---|---|---|
是否有 this | 没有 | 有 |
是否有生命周期 | 没有 | 有 |
是否有状态 state | 没有 | 有 |