上一篇文章咱们手写了一个 Redux,然而单纯的 Redux 只是一个状态机,是没有 UI 出现的,所以个别咱们应用的时候都会配合一个 UI 库,比方在 React 中应用 Redux 就会用到 React-Redux
这个库。这个库的作用是将 Redux 的状态机和 React 的 UI 出现绑定在一起,当你 dispatch action
扭转 state
的时候,会自动更新页面。本文还是从它的根本应用动手来本人写一个React-Redux
,而后替换官网的 NPM 库,并放弃性能统一。
本文全副代码曾经上传 GitHub,大家能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux
根本用法
上面这个简略的例子是一个计数器,跑起来成果如下:
要实现这个性能,首先咱们要在我的项目外面增加 react-redux
库,而后用它提供的 Provider
包裹整个React
App 的根组件:
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux'
import store from './store'
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
下面代码能够看到咱们还给 Provider
提供了一个参数 store
,这个参数就是 Redux 的createStore
生成的 store
,咱们须要调一下这个办法,而后将返回的store
传进去:
import {createStore} from 'redux';
import reducer from './reducer';
let store = createStore(reducer);
export default store;
下面代码中 createStore
的参数是一个reducer
,所以咱们还要写个reducer
:
const initState = {count: 0};
function reducer(state = initState, action) {switch (action.type) {
case 'INCREMENT':
return {...state, count: state.count + 1};
case 'DECREMENT':
return {...state, count: state.count - 1};
case 'RESET':
return {...state, count: 0};
default:
return state;
}
}
export default reducer;
这里的 reduce
会有一个初始 state
,外面的count
是0
,同时他还能解决三个 action
,这三个action
对应的是 UI 上的三个按钮,能够对 state
外面的计数进行加减和重置。到这里其实咱们 React-Redux
的接入和 Redux
数据的组织其实曾经实现了,前面如果要用 Redux
外面的数据的话,只须要用 connect
API 将对应的state
和办法连贯到组件外面就行了,比方咱们的计数器组件须要 count
这个状态和加一,减一,重置这三个 action
,咱们用connect
将它连贯进去就是这样:
import React from 'react';
import {connect} from 'react-redux';
import {increment, decrement, reset} from './actions';
function Counter(props) {
const {
count,
incrementHandler,
decrementHandler,
resetHandler
} = props;
return (
<>
<h3>Count: {count}</h3>
<button onClick={incrementHandler}> 计数 +1</button>
<button onClick={decrementHandler}> 计数 -1</button>
<button onClick={resetHandler}> 重置 </button>
</>
);
}
const mapStateToProps = (state) => {
return {count: state.count}
}
const mapDispatchToProps = (dispatch) => {
return {incrementHandler: () => dispatch(increment()),
decrementHandler: () => dispatch(decrement()),
resetHandler: () => dispatch(reset()),
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
下面代码能够看到 connect
是一个高阶函数,他的第一阶会接管 mapStateToProps
和mapDispatchToProps
两个参数,这两个参数都是函数。mapStateToProps
能够自定义须要将哪些 state
连贯到以后组件,这些自定义的 state
能够在组件外面通过 props
拿到。mapDispatchToProps
办法会传入 dispatch
函数,咱们能够自定义一些办法,这些办法能够调用 dispatch
去dispatch action
,从而触发 state
的更新,这些自定义的办法也能够通过组件的 props
拿到,connect
的第二阶接管的参数是一个组件,咱们能够猜想这个函数的作用就是将后面自定义的 state
和办法注入到这个组件外面,同时要返回一个新的组件给内部调用,所以 connect
其实也是一个高阶组件。
到这里咱们汇总来看下咱们都用到了哪些 API,这些 API 就是咱们前面要手写的指标:
Provider
: 用来包裹根组件的组件,作用是注入Redux
的store
。
createStore
:Redux
用来创立store
的外围办法,[咱们另一篇文章曾经手写过了]()。
connect
:用来将state
和dispatch
注入给须要的组件,返回一个新组件,他其实是个高阶组件。
所以 React-Redux
外围其实就两个 API,而且两个都是组件,作用还很相似,都是往组件外面注入参数,Provider
是往根组件注入 store
,connect
是往须要的组件注入 state
和dispatch
。
在手写之前咱们先来思考下,为什么 React-Redux
要设计这两个 API,如果没有这两个 API,只用 Redux
能够吗?当然是能够的!其实咱们用 Redux
的目标不就是心愿用它将整个利用的状态都保留下来,每次操作只用 dispatch action
去更新状态,而后 UI 就自动更新了吗?那我从根组件开始,每一级都把 store
传下去不就行了吗?每个子组件须要读取状态的时候,间接用 store.getState()
就行了,更新状态的时候就 store.dispatch
,这样其实也能达到目标。然而,如果这样写,子组件如果嵌套层数很多,每一级都须要手动传入store
,比拟俊俏,开发也比拟繁琐,而且如果某个新同学忘了传store
,那前面就是一连串的谬误了。所以最好有个货色可能将store
全局的注入组件树,而不须要一层层作为 props
传递,这个货色就是 Provider
!而且如果每个组件都独立依赖Redux
会毁坏 React
的数据流向,这个咱们前面会讲到。
React 的 Context API
React 其实提供了一个全局注入变量的 API,这就是 context api。如果我当初有一个需要是要给咱们所有组件传一个文字色彩的配置,咱们的色彩配置在最顶级的组件上,当这个色彩扭转的时候,上面所有组件都要主动利用这个色彩。那咱们能够应用 context api 注入这个配置:
先应用 React.createContext
创立一个 context
// 咱们应用一个独自的文件来调用 createContext
// 因为这个返回值会被 Provider 和 Consumer 在不同的中央援用
import React from 'react';
const TestContext = React.createContext();
export default TestContext;
应用 Context.Provider
包裹根组件
创立好了 context,如果咱们要传递变量给某些组件,咱们须要在他们的根组件上加上 TestContext.Provider
,而后将变量作为value
参数传给TestContext.Provider
:
import TestContext from './TestContext';
const setting = {color: '#d89151'}
ReactDOM.render(<TestContext.Provider value={setting}>
<App />
</TestContext.Provider>,
document.getElementById('root')
);
应用 Context.Consumer
接管参数
下面咱们应用 Context.Provider
将参数传递进去了,这样被 Context.Provider
包裹的所有子组件都能够拿到这个变量,只是拿这个变量的时候须要应用 Context.Consumer
包裹,比方咱们后面的 Counter
组件就能够拿到这个色彩了,只须要将它返回的 JSX
用Context.Consumer
包裹一下就行:
// 留神要引入同一个 Context
import TestContext from './TestContext';
// ... 两头省略 n 行代码 ...
// 返回的 JSX 用 Context.Consumer 包裹起来
// 留神 Context.Consumer 外面是一个办法,这个办法就能够拜访到 context 参数
// 这里的 context 也就是后面 Provider 传进来的 setting,咱们能够拿到下面的 color 变量
return (
<TestContext.Consumer>
{context =>
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}> 计数 +1</button>
<button onClick={decrementHandler}> 计数 -1</button>
<button onClick={resetHandler}> 重置 </button>
</>
}
</TestContext.Consumer>
);
下面代码咱们通过 context
传递了一个全局配置,能够看到咱们文字色彩曾经变了:
应用 useContext
接管参数
除了下面的 Context.Consumer
能够用来接管 context
参数,新版 React 还有 useContext
这个 hook 能够接管 context 参数,应用起来更简略,比方下面代码能够这样写:
const context = useContext(TestContext);
return (
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}> 计数 +1</button>
<button onClick={decrementHandler}> 计数 -1</button>
<button onClick={resetHandler}> 重置 </button>
</>
);
所以咱们齐全能够用 context api
来传递 redux store
,当初咱们也能够猜想React-Redux
的Provider
其实就是包装了 Context.Provider
,而传递的参数就是redux store
,而React-Redux
的connect
HOC 其实就是包装的 Context.Consumer
或者 useContext
。咱们能够依照这个思路来本人实现下React-Redux
了。
手写Provider
下面说了 Provider
用了 context api
,所以咱们要先建一个context
文件,导出须要用的context
:
// Context.js
import React from 'react';
const ReactReduxContext = React.createContext();
export default ReactReduxContext;
这个文件很简略,新建一个 context
再导出就行了,对应的源码看这里。
而后将这个 context
利用到咱们的 Provider
组件外面:
import React from 'react';
import ReactReduxContext from './Context';
function Provider(props) {const {store, children} = props;
// 这是要传递的 context
const contextValue = {store};
// 返回 ReactReduxContext 包裹的组件,传入 contextValue
// 外面的内容就间接是 children,咱们不动他
return (<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
Provider
的组件代码也不难,间接将传进来的 store
放到 context
上,而后间接渲染 children
就行,对应的源码看这里。
手写connect
基本功能
其实 connect
才是 React-Redux 中最难的局部,外面性能简单,思考的因素很多,想要把它搞明确咱们须要一层一层的来看,首先咱们实现一个只具备基本功能的connect
。
import React, {useContext} from 'react';
import ReactReduxContext from './Context';
// 第一层函数接管 mapStateToProps 和 mapDispatchToProps
function connect(mapStateToProps, mapDispatchToProps) {
// 第二层函数是个高阶组件,外面获取 context
// 而后执行 mapStateToProps 和 mapDispatchToProps
// 再将这个后果组合用户的参数作为最终参数渲染 WrappedComponent
// WrappedComponent 就是咱们应用 connext 包裹的本人的组件
return function connectHOC(WrappedComponent) {function ConnectFunction(props) {
// 复制一份 props 到 wrapperProps
const {...wrapperProps} = props;
// 获取 context 的值
const context = useContext(ReactReduxContext);
const {store} = context; // 解构出 store
const state = store.getState(); // 拿到 state
// 执行 mapStateToProps 和 mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
// 组装最终的 props
const actualChildProps = Object.assign({}, stateProps, dispatchProps, wrapperProps);
// 渲染 WrappedComponent
return <WrappedComponent {...actualChildProps}></WrappedComponent>
}
return ConnectFunction;
}
}
export default connect;
触发更新
用下面的 Provider
和connect
替换官网的 react-redux
其实曾经能够渲染出页面了,然而点击按钮还不会有反馈,因为咱们尽管通过 dispatch
扭转了 store
中的 state
,然而这种扭转并没有触发咱们组件的更新。之前 Redux 那篇文章讲过,能够用store.subscribe
来监听 state
的变动并执行回调,咱们这里须要注册的回调是查看咱们最终给 WrappedComponent
的props
有没有变动,如果有变动就从新渲染ConnectFunction
,所以这里咱们须要解决两个问题:
- 当咱们
state
变动的时候查看最终给到ConnectFunction
的参数有没有变动- 如果这个参数有变动,咱们须要从新渲染
ConnectFunction
查看参数变动
要查看参数的变动,咱们须要晓得上次渲染的参数和本地渲染的参数,而后拿过去比一下就晓得了。为了晓得上次渲染的参数,咱们能够间接在 ConnectFunction
外面应用 useRef
将上次渲染的参数记录下来:
// 记录上次渲染参数
const lastChildProps = useRef();
useLayoutEffect(() => {lastChildProps.current = actualChildProps;}, []);
留神 lastChildProps.current
是在第一次渲染完结后赋值,而且须要应用 useLayoutEffect
来保障渲染后立刻同步执行。
因为咱们检测参数变动是须要从新计算actualChildProps
,计算的逻辑其实都是一样的,咱们将这块计算逻辑抽出来,成为一个独自的办法childPropsSelector
:
function childPropsSelector(store, wrapperProps) {const state = store.getState(); // 拿到 state
// 执行 mapStateToProps 和 mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
而后就是注册 store
的回调,在外面来检测参数是否变了,如果变了就强制更新以后组件,比照两个对象是否相等,React-Redux
外面是采纳的 shallowEqual
,也就是浅比拟,也就是只比照一层,如果你mapStateToProps
返回了好几层构造,比方这样:
{
stateA: {value: 1}
}
你去改了 stateA.value
是不会触发从新渲染的,React-Redux
这样设计我想是出于性能思考,如果是深比拟,比方递归去比拟,比拟节约性能,而且如果有循环援用还可能造成死循环。采纳浅比拟就须要用户遵循这种范式,不要传入多层构造,这点在官网文档中也有阐明。咱们这里间接抄一个它的浅比拟:
// shallowEqual.js
function is(x, y) {if (x === y) {return x !== 0 || y !== 0 || 1 / x === 1 / y} else {return x !== x && y !== y}
}
export default function shallowEqual(objA, objB) {if (is(objA, objB)) return true
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {return false}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {return false}
}
return true
}
在回调外面检测参数变动:
// 注册回调
store.subscribe(() => {const newChildProps = childPropsSelector(store, wrapperProps);
// 如果参数变了,记录新的值到 lastChildProps 上
// 并且强制更新以后组件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 须要一个 API 来强制更新以后组件
}
});
强制更新
要强制更新以后组件的办法不止一个,如果你是用的 Class
组件,你能够间接 this.setState({})
,老版的React-Redux
就是这么干的。然而新版 React-Redux
用 hook 重写了,那咱们能够用 React 提供的 useReducer
或者 useState
hook,React-Redux
源码用了useReducer
,为了跟他保持一致,我也应用useReducer
:
function storeStateUpdatesReducer(count) {return count + 1;}
// ConnectFunction 外面
function ConnectFunction(props) {
// ... 后面省略 n 行代码 ...
// 应用 useReducer 触发强制更新
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0);
// 注册回调
store.subscribe(() => {const newChildProps = childPropsSelector(store, wrapperProps);
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
forceComponentUpdateDispatch();}
});
// ... 前面省略 n 行代码 ...
}
connect
这块代码次要对应的是源码中 connectAdvanced
这个类,基本原理和构造跟咱们这个都是一样的,只是他写的更灵便,反对用户传入自定义的 childPropsSelector
和合并 stateProps, dispatchProps, wrapperProps
的办法。有趣味的敌人能够去看看他的源码:https://github.com/reduxjs/react-redux/blob/master/src/components/connectAdvanced.js
到这里其实曾经能够用咱们本人的 React-Redux
替换官网的了,计数器的性能也是反对了。然而上面还想讲一下 React-Redux
是怎么保障组件的更新程序的,因为源码中很多代码都是在解决这个。
保障组件更新程序
后面咱们的 Counter
组件应用 connect
连贯了 redux store
,如果他上面还有个子组件也连贯到了redux store
,咱们就要思考他们的回调的执行程序的问题了。咱们晓得 React 是单向数据流的,参数都是由父组件传给子组件的,当初引入了Redux
,即便父组件和子组件都援用了同一个变量count
,然而子组件齐全能够不从父组件拿这个参数,而是间接从Redux
拿,这样就突破了 React
原本的数据流向。在 父 -> 子
这种单向数据流中,如果他们的一个专用变量变动了,必定是父组件先更新,而后参数传给子组件再更新,然而在 Redux
里,数据变成了 Redux -> 父,Redux -> 子
, 父
与 子
齐全能够依据 Redux
的数据进行独立更新,而不能齐全保障父级先更新,子级再更新的流程。所以 React-Redux
花了不少功夫来手动保障这个更新程序,React-Redux
保障这个更新程序的计划是在 redux store
外,再独自创立一个监听者类Subscription
:
Subscription
负责解决所有的state
变动的回调- 如果以后连贯
redux
的组件是第一个连贯redux
的组件,也就是说他是连贯redux
的根组件,他的state
回调间接注册到redux store
;同时新建一个Subscription
实例subscription
通过context
传递给子级。- 如果以后连贯
redux
的组件不是连贯redux
的根组件,也就是说他下面有组件曾经注册到redux store
了,那么他能够拿到下面通过context
传下来的subscription
,源码外面这个变量叫parentSub
,那以后组件的更新回调就注册到parentSub
上。同时再新建一个Subscription
实例,代替context
上的subscription
,持续往下传,也就是说他的子组件的回调会注册到以后subscription
上。- 当
state
变动了,根组件注册到redux store
上的回调会执行更新根组件,同时根组件须要手动执行子组件的回调,子组件回调执行会触发子组件更新,而后子组件再执行本人subscription
上注册的回调,触发孙子组件更新,孙子组件再调用注册到本人subscription
上的回调。。。这样就实现了从根组件开始,一层一层更新子组件的目标,保障了父 -> 子
这样的更新程序。
Subscription
类
所以咱们先新建一个 Subscription
类:
export default class Subscription {constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.listeners = []; // 源码 listeners 是用链表实现的,我这里简略解决,间接数组了
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
// 子组件注册回调到 Subscription 上
addNestedSub(listener) {this.listeners.push(listener)
}
// 执行子组件的回调
notifyNestedSubs() {
const length = this.listeners.length;
for(let i = 0; i < length; i++) {const callback = this.listeners[i];
callback();}
}
// 回调函数的包装
handleChangeWrapper() {if (this.onStateChange) {this.onStateChange()
}
}
// 注册回调的函数
// 如果 parentSub 有值,就将回调注册到 parentSub 上
// 如果 parentSub 没值,那以后组件就是根组件,回调注册到 redux store 上
trySubscribe() {
this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
}
}
Subscription
对应的源码看这里。
革新Provider
而后在咱们后面本人实现的 React-Redux
外面,咱们的根组件始终是 Provider
,所以Provider
须要实例化一个 Subscription
并放到 context
上,而且每次 state
更新的时候须要手动调用子组件回调,代码革新如下:
import React, {useMemo, useEffect} from 'react';
import ReactReduxContext from './Context';
import Subscription from './Subscription';
function Provider(props) {const {store, children} = props;
// 这是要传递的 context
// 外面放入 store 和 subscription 实例
const contextValue = useMemo(() => {const subscription = new Subscription(store)
// 注册回调为告诉子组件,这样就能够开始层级告诉了
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
}
}, [store])
// 拿到之前的 state 值
const previousState = useMemo(() => store.getState(), [store])
// 每次 contextValue 或者 previousState 变动的时候
// 用 notifyNestedSubs 告诉子组件
useEffect(() => {const { subscription} = contextValue;
subscription.trySubscribe()
if (previousState !== store.getState()) {subscription.notifyNestedSubs()
}
}, [contextValue, previousState])
// 返回 ReactReduxContext 包裹的组件,传入 contextValue
// 外面的内容就间接是 children,咱们不动他
return (<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
export default Provider;
革新connect
有了 Subscription
类,connect
就不能间接注册到 store
了,而是应该注册到父级 subscription
上,更新的时候除了更新本人还要告诉子组件更新。在渲染包裹的组件时,也不能间接渲染了,而是应该再次应用 Context.Provider
包裹下,传入批改过的 contextValue
,这个contextValue
外面的 subscription
应该替换为本人的。革新后代码如下:
import React, {useContext, useRef, useLayoutEffect, useReducer} from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription';
function storeStateUpdatesReducer(count) {return count + 1;}
function connect(mapStateToProps = () => {},
mapDispatchToProps = () => {}
) {function childPropsSelector(store, wrapperProps) {const state = store.getState(); // 拿到 state
// 执行 mapStateToProps 和 mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
return function connectHOC(WrappedComponent) {function ConnectFunction(props) {const { ...wrapperProps} = props;
const contextValue = useContext(ReactReduxContext);
const {store, subscription: parentSub} = contextValue; // 解构出 store 和 parentSub
const actualChildProps = childPropsSelector(store, wrapperProps);
const lastChildProps = useRef();
useLayoutEffect(() => {lastChildProps.current = actualChildProps;}, [actualChildProps]);
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0)
// 新建一个 subscription 实例
const subscription = new Subscription(store, parentSub);
// state 回调抽出来成为一个办法
const checkForUpdates = () => {const newChildProps = childPropsSelector(store, wrapperProps);
// 如果参数变了,记录新的值到 lastChildProps 上
// 并且强制更新以后组件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 须要一个 API 来强制更新以后组件
forceComponentUpdateDispatch();
// 而后告诉子级更新
subscription.notifyNestedSubs();}
};
// 应用 subscription 注册回调
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe();
// 批改传给子级的 context
// 将 subscription 替换为本人的
const overriddenContextValue = {
...contextValue,
subscription
}
// 渲染 WrappedComponent
// 再次应用 ReactReduxContext 包裹,传入批改过的 context
return (<ReactReduxContext.Provider value={overriddenContextValue}>
<WrappedComponent {...actualChildProps} />
</ReactReduxContext.Provider>
)
}
return ConnectFunction;
}
}
export default connect;
到这里咱们的 React-Redux
就实现了,跑起来的成果跟官网的成果一样,残缺代码曾经上传 GitHub 了:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux
上面咱们再来总结下 React-Redux
的外围原理。
总结
-
React-Redux
是连贯React
和Redux
的库,同时应用了React
和Redux
的 API。 -
React-Redux
次要是应用了React
的context api
来传递Redux
的store
。 -
Provider
的作用是接管Redux store
并将它放到context
上传递上来。 -
connect
的作用是从Redux store
中选取须要的属性传递给包裹的组件。 -
connect
会本人判断是否须要更新,判断的根据是须要的state
是否曾经变动了。 -
connect
在判断是否变动的时候应用的是浅比拟,也就是只比拟一层,所以在mapStateToProps
和mapDispatchToProps
中不要反回多层嵌套的对象。 - 为了解决父组件和子组件各自独立依赖
Redux
,毁坏了React
的父级 -> 子级
的更新流程,React-Redux
应用Subscription
类本人治理了一套告诉流程。 - 只有连贯到
Redux
最顶级的组件才会间接注册到Redux store
,其余子组件都会注册到最近父组件的subscription
实例上。 - 告诉的时候从根组件开始顺次告诉本人的子组件,子组件接管到告诉的时候,先更新本人再告诉本人的子组件。
参考资料
官网文档:https://react-redux.js.org/
GitHub 源码:https://github.com/reduxjs/react-redux/
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。
作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges