学习 redux 源码整体架构,深入理解 redux 及其中间件原理

如果觉得内容不错,可以设为星标置顶我的公众号

1. 前言

你好,我是若川。这是学习源码整体架构系列第八篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。

要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
5.学习 vuex 源码整体架构,打造属于自己的状态管理库
6.学习 axios 源码整体架构,打造属于自己的请求库
7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

感兴趣的读者可以点击阅读。
其他源码计划中的有:expressvue-rotuerredux、  react-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

阅读本文你将学到:

  1. git subtree 管理子仓库

  2. 如何学习 redux 源码

  3. redux 中间件原理

  4. redux 各个API的实现

  5. vuexredux  的对比

  6. 等等

1.1 本文阅读最佳方式

把我的redux源码仓库 git clone https://github.com/lxchuan12/redux-analysis.git克隆下来,顺便star一下我的redux源码学习仓库^_^。跟着文章节奏调试和示例代码调试,用chrome动手调试印象更加深刻。文章长段代码不用细看,可以调试时再细看。看这类源码文章百遍,可能不如自己多调试几遍。也欢迎加我微信交流ruochuan12

2. git subtree 管理子仓库

写了很多源码文章,vuexaxioskoa等都是使用新的仓库克隆一份源码在自己仓库中。虽然电脑可以拉取最新代码,看到原作者的git信息。但上传到github后。读者却看不到原仓库作者的git信息了。于是我找到了git submodules 方案,但并不是很适合。再后来发现了git subtree

简单说下 npm packagegit subtree的区别。npm package是单向的。git subtree则是双向的。

具体可以查看这篇文章@德来(原有赞大佬):用 Git Subtree 在多个 Git 项目间双向同步子项目,附简明使用手册

学会了git subtree后,我新建了redux-analysis项目后,把redux源码4.x(截止至2020年06月13日,4.x分支最新版本是4.0.5master分支是ts,文章中暂不想让一些不熟悉ts的读者看不懂)分支克隆到了我的项目里的一个子项目,得以保留git信息。

对应命令则是:

git subtree add --prefix=redux https://github.com/reduxjs/redux.git 4.x

3. 调试 redux 源码准备工作

之前,我在知乎回答了一个问题若川:一年内的前端看不懂前端框架源码怎么办?推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:

1.借助调试
2.搜索查阅相关高赞文章
3.把不懂的地方记录下来,查阅相关文档
4.总结

看源码调试很重要,所以我的每篇源码文章都详细描述(也许有人看来是比较啰嗦...)如何调试源码。

断点调试要领:
赋值语句可以一步按F10跳过,看返回值即可,后续详细再看。
函数执行需要断点按F11跟着看,也可以结合注释和上下文倒推这个函数做了什么。
有些不需要细看的,直接按F8走向下一个断点
刷新重新调试按F5

调试源码前,先简单看看 redux 的工作流程,有个大概印象。

redux 工作流程

3.1 rollup 生成 sourcemap 便于调试

修改rollup.config.js文件,output输出的配置生成sourcemap

// redux/rollup.config.js 有些省略
const sourcemap = {sourcemap: true,
};output: {// ......sourcemap,
}

安装依赖

git clone http://github.com/lxchuan12/redux-analysis.git
cd redux-analysi/redux
npm i
npm run build
# 编译结束后会生成 sourcemap .map格式的文件到 dist、es、lib 目录下。

仔细看看redux/examples目录和redux/README

这时我在根路径下,新建文件夹examples,把原生js写的计数器redux/examples/counter-vanilla/index.html,复制到examples/index.html。同时把打包后的包含sourcemapredux/dist目录,复制到examples/dist目录。

修改index.htmlscriptredux.js文件为dist中的路径

为了便于区分和调试后续html文件,我把index.html重命名为index.1.redux.getState.dispatch.html

# redux-analysis 根目录
# 安装启动服务的npm包
npm i -g http-server
cd examples
hs -p 5000

就可以开心的调试啦。可以直接克隆我的项目git clone http://github.com/lxchuan12/redux-analysis.git。本地调试,动手实践,容易消化吸收。

4. 通过调试计数器例子的学习 redux 源码

接着我们来看examples/index.1.redux.getState.dispatch.html文件。先看html部分。只是写了几个 button,比较简单。

<div><p>Clicked: <span id="value">0</span> times<button id="increment">+</button><button id="decrement">-</button><button id="incrementIfOdd">Increment if odd</button><button id="incrementAsync">Increment async</button></p>
</div>

js部分,也比较简单。声明了一个counter函数,传递给Redux.createStore(counter),得到结果store,而store是个对象。render方法渲染数字到页面。用store.subscribe(render)订阅的render方法。还有store.dispatch({type: 'INCREMENT' })方法,调用store.dispatch时会触发render方法。这样就实现了一个计数器。

function counter(state, action) {if (typeof state === 'undefined') {return 0}switch (action.type) {case 'INCREMENT':return state + 1case 'DECREMENT':return state - 1default:return state}
}var store = Redux.createStore(counter)
var valueEl = document.getElementById('value')function render() {valueEl.innerHTML = store.getState().toString()
}
render()
store.subscribe(render)document.getElementById('increment')
.addEventListener('click', function () {store.dispatch({ type: 'INCREMENT' })
})// 省略部分暂时无效代码...

思考:看了这段代码,你会在哪打断点来调试呢。

// 四处可以断点来看
// 1.
var store = Redux.createStore(counter)
// 2.
function render() {
valueEl.innerHTML = store.getState().toString()
}
render()
// 3.
store.subscribe(render)
// 4.
store.dispatch({ type: 'INCREMENT' })
redux debugger图

图中的右边Scope,有时需要关注下,会显示闭包、全局环境、当前环境等变量,还可以显示函数等具体代码位置,能帮助自己理解代码。

断点调试,按F5刷新页面后,按F8,把鼠标放在Reduxstore上。

可以看到Redux上有好几个方法。分别是:

  • __DO_NOT_USE__ActionTypes: {INIT: "@@redux/INITu.v.d.u.6.r", REPLACE: "@@redux/REPLACEg.u.u.7.c", PROBE_UNKNOWN_ACTION: ƒ}

  • applyMiddleware: ƒ applyMiddleware() 函数是一个增强器,组合多个中间件,最终增强store.dispatch函数,dispatch时,可以串联执行所有中间件。

  • bindActionCreators: ƒ bindActionCreators(actionCreators, dispatch) 生成actions,主要用于其他库,比如react-redux

  • combineReducers: ƒ combineReducers(reducers) 组合多个reducers,返回一个总的reducer函数。

  • compose: ƒ compose() 组合多个函数,从右到左,比如:compose(f, g, h) 最终得到这个结果 (...args) => f(g(h(...args))).

  • createStore: ƒ createStore(reducer, preloadedState, enhancer) 生成 store 对象

再看store也有几个方法。分别是:

  • dispatch: ƒ dispatch(action)  派发动作,也就是把subscribe收集的函数,依次遍历执行

  • subscribe: ƒ subscribe(listener) 订阅收集函数存在数组中,等待触发dispatch依次执行。返回一个取消订阅的函数,可以取消订阅监听。

  • getState: ƒ getState() 获取存在createStore函数内部闭包的对象。

  • replaceReducer: ƒ replaceReducer(nextReducer) 主要用于redux开发者工具,对比当前和上一次操作的异同。有点类似时间穿梭功能。

  • Symbol(observable): ƒ observable()

也就是官方文档redux.org.js上的 API

暂时不去深究每一个API的实现。重新按F5刷新页面,断点到var store = Redux.createStore(counter)。一直按F11,先走一遍主流程。

4.1 Redux.createSotre

createStore 函数结构是这样的,是不是看起来很简单,最终返回对象store,包含dispatchsubscribegetStatereplaceReducer等方法。

// 省略了若干代码
export default function createStore(reducer, preloadedState, enhancer) {// 省略参数校验和替换// 当前的 reducer 函数let currentReducer = reducer// 当前statelet currentState = preloadedState// 当前的监听数组函数let currentListeners = []// 下一个监听数组函数let nextListeners = currentListeners// 是否正在dispatch中let isDispatching = falsefunction ensureCanMutateNextListeners() {if (nextListeners === currentListeners) {nextListeners = currentListeners.slice()}}function getState() {return currentState}function subscribe(listener) {}function dispatch(action) {}function replaceReducer(nextReducer) {}function observable() {}// ActionTypes.INIT @@redux/INITu.v.d.u.6.rdispatch({ type: ActionTypes.INIT })return {dispatch,subscribe,getState,replaceReducer,[$$observable]: observable}
}

4.2 store.dispatch(action)

function dispatch(action) {// 判断action是否是对象,不是则报错if (!isPlainObject(action)) {throw new Error('Actions must be plain objects. ' +'Use custom middleware for async actions.')}// 判断action.type 是否存在,没有则报错if (typeof action.type === 'undefined') {throw new Error('Actions may not have an undefined "type" property. ' +'Have you misspelled a constant?')}// 不是则报错if (isDispatching) {throw new Error('Reducers may not dispatch actions.')}try {isDispatching = truecurrentState = currentReducer(currentState, action)} finally {// 调用完后置为 falseisDispatching = false}//  把 收集的函数拿出来依次调用const listeners = (currentListeners = nextListeners)for (let i = 0; i < listeners.length; i++) {const listener = listeners[i]listener()}// 最终返回 actionreturn action}
var store = Redux.createStore(counter)

上文调试完了这句。

继续按F11调试。

function render() {valueEl.innerHTML = store.getState().toString()
}
render()

4.3 store.getState()

getState函数实现比较简单。

function getState() {// 判断正在dispatch中,则报错if (isDispatching) {throw new Error('You may not call store.getState() while the reducer is executing. ' +'The reducer has already received the state as an argument. ' +'Pass it down from the top reducer instead of reading it from the store.')}// 返回当前的statereturn currentState
}

4.4 store.subscribe(listener)

订阅监听函数,存放在数组中,store.dispatch(action)时遍历执行。

function subscribe(listener) {// 订阅参数校验不是函数报错if (typeof listener !== 'function') {throw new Error('Expected the listener to be a function.')}// 正在dispatch中,报错if (isDispatching) {throw new Error('You may not call store.subscribe() while the reducer is executing. ' +'If you would like to be notified after the store has been updated, subscribe from a ' +'component and invoke store.getState() in the callback to access the latest state. ' +'See https://redux.js.org/api-reference/store#subscribelistener for more details.')}// 订阅为 truelet isSubscribed = trueensureCanMutateNextListeners()nextListeners.push(listener)// 返回一个取消订阅的函数return function unsubscribe() {if (!isSubscribed) {return}// 正在dispatch中,则报错if (isDispatching) {throw new Error('You may not unsubscribe from a store listener while the reducer is executing. ' +'See https://redux.js.org/api-reference/store#subscribelistener for more details.')}// 订阅为 falseisSubscribed = falseensureCanMutateNextListeners()//   找到当前监听函数const index = nextListeners.indexOf(listener)//   在数组中删除nextListeners.splice(index, 1)currentListeners = null}}

到这里,我们就调试学习完了Redux.createSotrestore.dispatchstore.getStatestore.subscribe的源码。

接下来,我们写个中间件例子,来调试中间件相关源码。

5. Redux 中间件相关源码

中间件是重点,面试官也经常问这类问题。

5.1 Redux.applyMiddleware(...middlewares)

5.1.1 准备 logger 例子调试

为了调试Redux.applyMiddleware(...middlewares),我在examples/js/middlewares.logger.example.js写一个简单的logger例子。分别有三个logger1logger2logger3函数。由于都是类似,所以我在这里只展示logger1函数。

// examples/js/middlewares.logger.example.js
function logger1({ getState }) {return next => action => {console.log('will dispatch--1--next, action:', next, action)// Call the next dispatch method in the middleware chain.const returnValue = next(action)console.log('state after dispatch--1', getState())// This will likely be the action itself, unless// a middleware further in chain changed it.return returnValue}
}
// 省略 logger2、logger3

logger中间件函数做的事情也比较简单,返回两层函数,next就是下一个中间件函数,调用返回结果。为了让读者能看懂,我把logger1用箭头函数、logger2则用普通函数。

写好例子后,我们接着来看怎么调试Redux.applyMiddleware(...middlewares))源码。

cd redux-analysis && hs -p 5000
# 上文说过npm i -g http-server

打开http://localhost:5000/examples/index.2.redux.applyMiddleware.compose.html,按F12打开控制台,

先点击加号操作+1,把结果展示出来。

redux 中间件调试图

从图中可以看出,next则是下一个函数。先1-2-3,再3-2-1这样的顺序。

这种也就是我们常说的中间件,面向切面编程(AOP)。

中间件图解

接下来调试,在以下语句打上断点和一些你觉得重要的地方打上断点。

// examples/index.2.redux.applyMiddleware.compose.html
var store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))

5.1.2 Redux.applyMiddleware(...middlewares) 源码

// redux/src/applyMiddleware.js
/*** ...* @param {...Function} middlewares The middleware chain to be applied.* @returns {Function} A store enhancer applying the middleware.*/
export default function applyMiddleware(...middlewares) {return createStore => (...args) => {const store = createStore(...args)let dispatch = () => {throw new Error('Dispatching while constructing your middleware is not allowed. ' +'Other middleware would not be applied to this dispatch.')}const middlewareAPI = {getState: store.getState,dispatch: (...args) => dispatch(...args)}const chain = middlewares.map(middleware => middleware(middlewareAPI))dispatch = compose(...chain)(store.dispatch)return {...store,dispatch}}
}
// redux/src/createStore.js
export default function createStore(reducer, preloadedState, enhancer) {// 省略参数校验// 如果第二个参数`preloadedState`是函数,并且第三个参数`enhancer`是undefined,把它们互换一下。if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {enhancer = preloadedStatepreloadedState = undefined}if (typeof enhancer !== 'undefined') {if (typeof enhancer !== 'function') {throw new Error('Expected the enhancer to be a function.')}// enhancer 也就是`Redux.applyMiddleware`返回的函数// createStore 的 args 则是 `reducer, preloadedState`/*** createStore => (...args) => {const store = createStore(...args)return {...store,dispatch,}}** /// 最终返回增强的store对象。return enhancer(createStore)(reducer, preloadedState)}// 省略后续代码
}

把接收的中间件函数logger1, logger2, logger3放入到 了middlewares数组中。Redux.applyMiddleware最后返回两层函数。把中间件函数都混入了参数getStatedispatch

// examples/index.2.redux.applyMiddleware.compose.html
var store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))

最后这句其实是返回一个增强了dispatchstore对象。

而增强的dispatch函数,则是用Redux.compose(...functions)进行串联起来执行的。

5.2 Redux.compose(...functions)

export default function compose(...funcs) {if (funcs.length === 0) {return arg => arg}if (funcs.length === 1) {return funcs[0]}return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
// applyMiddleware.js
dispatch = compose(...chain)(store.dispatch)
// compose
funcs.reduce((a, b) => (...args) => a(b(...args)))

这两句可能不是那么好理解,可以断点多调试几次。我把箭头函数转换成普通函数。

funcs.reduce(function(a, b){return function(...args){return a(b(...args));};
});

其实redux源码中注释很清晰了,这个compose函数上方有一堆注释,其中有一句:组合多个函数,从右到左,比如:compose(f, g, h) 最终得到这个结果 (...args) => f(g(h(...args))).

5.2.1 compose 函数演化

Redux.compose(...functions)函数源码后,还是不明白,不要急不要慌,吃完鸡蛋还有汤。仔细来看如何演化而来,先来简单看下如下需求。

传入一个数值,计算数值乘以10再加上10,再减去2。

实现起来很简单。

const calc = (num) => num * 10 + 10 - 2;
calc(10); // 108

但这样写有个问题,不好扩展,比如我想乘以10时就打印出结果。为了便于扩展,我们分开写成三个函数。

const multiply = (x) => {const result = x * 10;console.log(result);return result;
};
const add = (y) => y + 10;
const minus = (z) => z - 2;// 计算结果
console.log(minus(add(multiply(10))));
// 100
// 108
// 这样我们就把三个函数计算结果出来了。

再来实现一个相对通用的函数,计算这三个函数的结果。

const compose = (f, g, h) => {return function(x){return f(g(h(x)));}
}
const calc = compose(minus, add, multiply);
console.log(calc(10));
// 100
// 108

这样还是有问题,只支持三个函数。我想支持多个函数。我们了解到数组的reduce方法就能实现这样的功能。前一个函数

// 我们常用reduce来计算数值数组的总和
[1,2,3,4,5].reduce((pre, item, index, arr) => {console.log('(pre, item, index, arr)', pre, item, index, arr);// (pre, item, index, arr) 1 2 1 (5) [1, 2, 3, 4, 5]// (pre, item, index, arr) 3 3 2 (5) [1, 2, 3, 4, 5]// (pre, item, index, arr) 6 4 3 (5) [1, 2, 3, 4, 5]// (pre, item, index, arr) 10 5 4 (5) [1, 2, 3, 4, 5]return pre + item;
});
// 15

pre 是上一次返回值,在这里是数值1,3,6,10。在下一个例子中则是匿名函数。

function(x){return a(b(x));
}

item2,3,4,5,在下一个例子中是minus、add、multiply

const compose = (...funcs) => {return funcs.reduce((a, b) => {return function(x){return a(b(x));}})
}
const calc = compose(minus, add, multiply);
console.log(calc(10));
// 100
// 108

Redux.compose(...functions)其实就是这样,只不过中间件是返回双层函数罢了。

所以返回的是next函数,他们串起来执行了,形成了中间件的洋葱模型。人们都说一图胜千言。我画了一个相对简单的redux中间件原理图。

redux中间件原理图

如果还不是很明白,建议按照我给出的例子,多调试。

cd redux-analysis && hs -p 5000
# 上文说过npm i -g http-server

打开http://localhost:5000/examples/index.3.html,按F12打开控制台调试。

5.2.2 前端框架的 compose 函数的实现

lodash源码中 compose函数的实现,也是类似于数组的reduce,只不过是内部实现的arrayReduce

引用自我的文章:学习lodash源码整体架构

// lodash源码
function baseWrapperValue(value, actions) {var result = value;// 如果是lazyWrapper的实例,则调用LazyWrapper.prototype.value 方法,也就是 lazyValue 方法if (result instanceof LazyWrapper) {result = result.value();}// 类似 [].reduce(),把上一个函数返回结果作为参数传递给下一个函数return arrayReduce(actions, function(result, action) {return action.func.apply(action.thisArg, arrayPush([result], action.args));}, result);
}

koa-compose源码也有compose函数的实现。实现是循环加promise。由于代码比较长我就省略了,具体看链接若川:学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理小节 koa-compose 源码(洋葱模型实现)

6. Redux.combineReducers(reducers)

打开http://localhost:5000/examples/index.4.html,按F12打开控制台,按照给出的例子,调试接下来的Redux.combineReducers(reducers)Redux.bindActionCreators(actionCreators, dispatch)具体实现。由于文章已经很长了,这两个函数就不那么详细解释了。

combineReducers函数简单来说就是合并多个reducer为一个函数combination

export default function combineReducers(reducers) {const reducerKeys = Object.keys(reducers)const finalReducers = {}for (let i = 0; i < reducerKeys.length; i++) {const key = reducerKeys[i]// 省略一些开发环境判断的代码...if (typeof reducers[key] === 'function') {finalReducers[key] = reducers[key]}}// 经过一些处理后得到最后的finalReducerKeysconst finalReducerKeys = Object.keys(finalReducers)// 省略一些开发环境判断的代码...return function combination(state = {}, action) {// ... 省略开发环境的一些判断// 用 hasChanged变量 记录前后 state 是否已经修改let hasChanged = false// 声明对象来存储下一次的stateconst nextState = {}//遍历 finalReducerKeysfor (let i = 0; i < finalReducerKeys.length; i++) {const key = finalReducerKeys[i]const reducer = finalReducers[key]const previousStateForKey = state[key]// 执行 reducerconst nextStateForKey = reducer(previousStateForKey, action)// 省略容错代码 ...nextState[key] = nextStateForKey// 两次 key 对比 不相等则发生改变hasChanged = hasChanged || nextStateForKey !== previousStateForKey}// 最后的 keys 数组对比 不相等则发生改变hasChanged =hasChanged || finalReducerKeys.length !== Object.keys(state).lengthreturn hasChanged ? nextState : state}
}

7. Redux.bindActionCreators(actionCreators, dispatch)

如果第一个参数是一个函数,那就直接返回一个函数。如果是一个对象,则遍历赋值,最终生成boundActionCreators对象。

function bindActionCreator(actionCreator, dispatch) {return function() {return dispatch(actionCreator.apply(this, arguments))}
}export default function bindActionCreators(actionCreators, dispatch) {if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, dispatch)}// ... 省略一些容错判断const boundActionCreators = {}for (const key in actionCreators) {const actionCreator = actionCreators[key]if (typeof actionCreator === 'function') {boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)}}return boundActionCreators
}

redux所提供的的API 除了store.replaceReducer(nextReducer)没分析,其他都分析了。

8. vuex 和 redux 简单对比

8.1 源码实现形式

从源码实现上来看,vuex源码主要使用了构造函数,而redux则是多用函数式编程、闭包。

8.2 耦合度

vuexvue 强耦合,脱离了vue则无法使用。而reduxreact没有关系,所以它可以使用于小程序或者jQuery等。如果需要和react使用,还需要结合react-redux库。

8.3 扩展

// logger 插件,具体实现省略
function logger (store) {console.log('store', store);
}
// 作为数组传入
new Vuex.Store({state,getters,actions,mutations,plugins: process.env.NODE_ENV !== 'production'? [logger]: []
})
// vuex 源码 插件执行部分
class Store{constructor(){// 把vuex的实例对象 store整个对象传递给插件使用plugins.forEach(plugin => plugin(this))}
}

vuex实现扩展则是使用插件形式,而redux是中间件的形式。redux的中间件则是AOP(面向切面编程),reduxRedux.applyMiddleware()其实也是一个增强函数,所以也可以用户来实现增强器,所以redux生态比较繁荣。

8.4 上手难易度

相对来说,vuex上手相对简单,redux相对难一些,redux涉及到一些函数式编程、高阶函数、纯函数等概念。

9. 总结

文章主要通过一步步调试的方式循序渐进地讲述redux源码的具体实现。旨在教会读者调试源码,不惧怕源码。

面试官经常喜欢考写一个redux中间件,说说redux中间件的原理。

function logger1({ getState }) {return next => action => {const returnValue = next(action)return returnValue}
}
const compose = (...funcs) => {if (funcs.length === 0) {return arg => arg}if (funcs.length === 1) {return funcs[0]}// 箭头函数// return funcs.reduce((a, b) => (...args) => a(b(...args)))return funcs.reduce((a, b) => {return function(x){return a(b(x));}})
}
const enhancerStore = Redux.create(reducer, Redux.applyMiddleware(logger1, ...))
enhancerStore.dispatch(action)

用户触发enhancerStore.dispatch(action)是增强后的,其实就是第一个中间件函数,中间的next是下一个中间件函数,最后next是没有增强的store.dispatch(action)

最后再来看张redux工作流程图

是不是就更理解些了呢。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对你有些许帮助,可以点赞、评论、转发分享,也是对我的一种支持,非常感谢呀。要是有人说到怎么读源码,正在读文章的你能推荐我的源码系列文章,那真是太好了

一般人都看不到文章末尾,看到这里你已经超越90%的人了。

  1. 觉得文章不错,可以点个在看呀^_^另外欢迎留言交流~

  2. 加我(若川)微信ruochuan12,拉你进交流群,长期交流学习

  3. 关注我的公众号若川视野,回复pdf领取前端优质书籍pdf

  4. 我的博客地址:https://lxchuan12.cn 欢迎收藏

小提醒:若川视野公众号原创文章合集在菜单栏中间【原创精选】按钮,欢迎点击阅读。

由于公众号限制外链,点击阅读原文,或许阅读体验更佳

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/276456.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pdf安装包_有么有pdf控件,不需要用户安装任何安装包直接打印的?

如果开发一个软件&#xff0c;需要用到PDF功能&#xff0c;您的选择是基于Adobe PDF吗&#xff1f; 如果是基于Adobe PDF&#xff0c;需要用户安装一个几十M的Adobe的安装包&#xff0c;这显然是不友好的。即使目前也有了一些其它的阅读器&#xff0c;大小也还好。但是&#xf…

[转] C#异步操作

Title 通过委托实现异步调用中BeginInvoke及回调函数的使用 通过委托实现异步调用的步骤&#xff1a; 1.定义委托。 2.将要进行异步调用的方法“实例化”到定义的委托。 3.在委托上调用BeginInvoke方法。其中&#xff0c;BeginInvoke的参数由三个部分构成。第一部分&#xff1…

HTTP Server Error 500 内部服务器错误

问题&#xff1a;HTTP500错误 或 Server Application Error ------------------------------------Server Application ErrorThe server has encountered an error while loading an application during the processing of your request. Please refer to the event log for mo…

使用 ohmyzsh 打造 windows、ubuntu、mac 系统高效终端命令行工具

如果觉得内容不错&#xff0c;可以设为星标置顶我的公众号原标题名&#xff1a;oh my zsh 和 windows git bash 设置别名提高效率写于2018年06月03日在我的微信交流群中听闻很多前端开发比较贫穷&#xff0c;没有买mac电脑&#xff08;比如我&#xff09;&#xff0c;也没有用过…

request获取mac地址_【Go】获取用户真实的ip地址

原文链接&#xff1a;https://blog.thinkeridea.com/201903/go/get_client_ip.html用户请求到达提供服务的服务器中间有很多的环节&#xff0c;导致服务获取用户真实的 ip 非常困难&#xff0c;大多数的框架及工具库都会封装各种获取用户真实 ip 的方法&#xff0c;在 exnet 包…

iPhone开发四剑客之《Objective-C基础教程》

iPhone 开发四剑客之《Objective-C 基础教程》 Objective-C 语言是 C 语言的一个扩展集&#xff0c;许多&#xff08;可能是大多数&#xff09;具备 Mac OS X 外观的应用程序都是使用该语言开发的。它以 C 语言为基础&#xff0c;添加了一些微妙但意义重大的特性。 苹果公司为…

keras训练完以后怎么预测_还在使用“龟速”的单显卡训练模型?动动手,让TPU节省你的时间...

点击上方关注&#xff0c;All in AI中国本文将介绍如何使用Keras和Google CoLaboratory与TPU一起训练LSTM模型&#xff0c;与本地计算机上的GPU相比&#xff0c;这样训练能大大缩短训练时间。很长一段时间以来&#xff0c;我都在单张GTX 1070显卡上训练我的模型&#xff0c;它的…

手把手教你写个小程序定时器管理库

背景凹凸曼是个小程序开发者&#xff0c;他要在小程序实现秒杀倒计时。于是他不假思索&#xff0c;写了以下代码&#xff1a;Page({init: function () {clearInterval(this.timer)this.timer setInterval(() > {// 倒计时计算逻辑console.log(setInterval)})}, })可是&…

[New Portal]Windows Azure Virtual Machine (14) 在本地制作数据文件VHD并上传至Azure(1)

《Windows Azure Platform 系列文章目录》 之前的内容里&#xff0c;我介绍了如何将本地的Server 2012中文版 VHD上传至Windows Azure&#xff0c;并创建基于该Server 2012 VHD的虚拟机。 我们知道&#xff0c;VHD不仅仅可以保存操作系统&#xff0c;而且可以保存数据文件。 如…

python 退出程序_Python:用Ctrl+C解决终止多线程程序的问题!(建议收藏)

前言&#xff1a;今天为大家带来的内容是Python:用CtrlC解决终止多线程程序的问题&#xff01;文章中的代码具有不错的参考意义&#xff0c;希望在此能够帮助到各位&#xff01;(多数代码用图片的方式呈现出来&#xff0c;方便各位观看与收藏)出发点&#xff1a;前段时间&#…

若川知乎高赞:有哪些必看的 JS 库?

欢迎星标我的公众号&#xff0c;回复加群&#xff0c;长期交流学习我的知乎回答目前2w阅读量&#xff0c;270赞&#xff0c;现在发到公众号声明原创。必看的js库&#xff1f;只有当前阶段值不值看。我从去年7月起看一些前端库的源码&#xff0c;历时一年才写了八篇《学习源码整…

基于EasyUI的Web应用程序及过去一年的总结

前言 一个多月之前已经提交了离职申请&#xff0c;好在领导都已经批准了&#xff0c;过几天就办理手续了&#xff0c;在此感谢领导的栽培与挽留&#xff0c;感谢各位同事在工作中的给我的帮助&#xff0c;离开这个团队确实有一些不舍&#xff0c;不为别的&#xff0c;只因为这个…

快速使用Vue3最新的15个常用API

之前我写了一篇博客介绍了Vue3的新特性&#xff0c;简单了解了一下Vue3都有哪些特色&#xff0c;并且在文末带大家稍微体验了一下Vue3中 Compsition API 的简单使用上一篇文章地址&#xff1a;紧跟尤大的脚步提前体验Vue3新特性&#xff0c;你不会还没了解过Vue3吧因为这个月的…

超级马里奥代码_任天堂的源码泄露,揭示超级马里奥的前世之生

被黑客盯上的任天堂任天堂遭到了史上最大规模的黑客攻击&#xff0c;Wii 完整源码、设计以及《宝可梦》多部作品的信息遭到泄露&#xff0c;而此次泄露事件的后续影响似乎也爆发了出来。《马里奥赛车》和《超级马里奥世界2》(耀西岛)的早期原型视频&#xff0c;以及《超级马里奥…

漫画 | 前端发展史的江湖恩怨情仇

时间总是过得很快&#xff0c; 似乎快得让人忘记了昨天&#xff0c;前端WEB领域的发展更是如此&#xff0c;转眼间已是近30年&#xff0c;时光荏苒&#xff0c;初心不变&#xff0c;在一代又一代前端人的努力下&#xff0c;前端已经是互联网不可或缺的一部分。然而很多前端打工…

10 个你可能还不知道 VS Code 使用技巧

经常帮一些同学 One-on-One 地解决问题&#xff0c;在看部分同学使用 VS Code 的时候&#xff0c;有些蹩脚&#xff0c;实际上一些有用的技巧能够提高我们的日常工作效率。NO.1一、重构代码VS Code 提供了一些快速重构代码的操作&#xff0c;例如&#xff1a;将一整段代码提取为…

构建安全的Xml Web Service系列之如何察看SoapMessage

上一篇文章地址&#xff1a;构建安全的Xml Web Service系列一之初探使用Soap头 (5-22 12:53) 要分析Xml Web Service的安全性&#xff0c;首先要解决的问题是我们能了解和清楚Soap消息的格式和内容&#xff0c;如果获得不了SoapMessage&#xff0c;分析如何能构建安全Xml w…

前端高效开发必备的 js 库梳理

之前有很多人问学好前端需要学习哪些 js 库, 主流框架应该学 vue 还是 react ? 针对这些问题, 笔者来说说自己的看法和学习总结.首先我觉得在学习任何知识之前必须要有一个明确的学习目标, 知道自己为什么要学它, 而不是看网上说的一股脑的给你灌输各种知识, 让你学习各种库, …

交叉报表crosstab隐藏列名显示_SAP软件 报表查询之 输出格式设置

SAP不仅是功能强大、逻辑严谨的ERP软件&#xff0c;还提供了强大的报表查询功能。SAP的ALV报表展示功能是SAP的一大特点&#xff0c;实现了类似于EXCEL的功能。使用好ALV报表功能可以方便用户从SAP中取到想要的数据&#xff0c;尤其是财务用户。大家在使用SAP报表时&#xff0c…

seo每日一贴_白杨SEO:我看ZAC的外贸SEO应该怎样做?(策略篇)

前言&#xff1a;这是白杨SEO公众号更新第64篇。本该写写头条SEO啥的&#xff0c;最近在师徒培训讲站内SEO时有旁听同学提到后面讲讲谷歌SEO怎么样&#xff0c;因为谷歌全世界搜索市场占有率&#xff0c;所以外贸SEO最主要还是做谷歌SEO。以白杨特意又去了前辈ZAC的SEO每日一贴…