欢迎来到我的博客,在这里,我会写一些文章分享个人所学习的知识,如果写的问题,可以提 issues 给我指正,谢谢~
hankzhuo / blog Goto Github PK
View Code? Open in Web Editor NEW📖 like to share tech-knowledge
📖 like to share tech-knowledge
欢迎来到我的博客,在这里,我会写一些文章分享个人所学习的知识,如果写的问题,可以提 issues 给我指正,谢谢~
第一次看 redux 文档时候,说实话我是看的比较抽象的,比如 Action、 dipatch、Reducer、Store
等概念,对于刚入 React 全家桶,这些概念会让人觉得有点绕。
但它太棒了,它是 React 全家桶里很重要的组成部分,如果你项目中使用 React 技术栈(当然 React 与 Redux 不是绑定关系),那么 Redux **是必须要了解下,当你使用过一段时间时候,真的会被其**感染,只能说这些库的作者是真的强,给他们点赞。
偶然间,在国外技术论坛看到一些文章,把 Redux 解释的非常通俗易懂,把 Redux 与现实中例子结合起来,构成了非常清晰的、完整的一套逻辑。
为啥要用到 Redux?因为在一个 app 项目中,如果涉及到 state
复杂交互,在 React 组件内部对于 state
的管理是不方便,不便于维护,使用统一管理 state
的库是比较合适的。当然,如果你的项目比较简单,完全可以不需要 redux。
那么 Redux 是什么?官方文档解释:
Redux is a predictable state container for JavaScript apps.
如果你了解过 React,那么肯定知道 state
概念,那么 Redux 可以理解为 state 的外部状态管理容器,是不是还很有点意思了?接下来我会给你一步步解释每个概念。把 Redux 与 “去银行取钱的过程”结合起来,更好地理解它们之间的关系。
首先,你的钱都储存在银行金库里,银行保存着你所有的钱,相对应的,把银行金库比作 Redux 里的 Store
,而 Store
储存着你 app 中的所有 State
。
先记住这个对应关系:
金库 <==>
Store
你的钱 <==>
State
现在,你要去银行取钱,这个想法或者目的,相对应的,Redux 里的Action
。
取钱想法 <==>
Action
去取钱,还是存钱?这个对应的 Redux 里是 Action
里的 Type
:
取钱还是存钱 <==>
Type
action
形式简单表示如下:
{ type: "WITHDRAW_MONEY", amount: "¥1,000" }
把卡给收银员或者 ATM 取钱,然后把钱给你,相对应的,就是 Redux 里的,发起一个
action,传递给
Reducer返回一个新的
State`。
收银员 / ATM <==>
Reducer
本篇内容主要讲 Redux 的理论,把 Redux 比喻成去银行取钱,能更好理解 Redux。
Redux 与实际的对应关系:
Store
State
Action
Type
Reducer
流程:
去银行取钱,把卡给收银员(或 ATM 机),取多少钱,然后收银员把钱取出来给你的过程。
在 app 中,更新 state
的过程:dipatch
一个 action
给 reducer
(收银员),reducer
(收营员) 返回一个新的 state
(你银行还剩下的钱) 。
理论与代码结合,实践动手,才能更好理解 Redux。
老样子,学一门技术,都是从简单的应用开始的。
首先,为了节省时间和以学习 Redux 的目的,使用 React 脚手架 create-react-app 安装 React 环境。
安装好环境后,按照提示把项目跑起来,修改 src/App.js
文件,
src/App.js
import React, { Component } from "react";
import HelloWorld from "./HelloWorld";
class App extends Component {
constructor(props) {
super(props)
this.state = {
tech: 'react'
}
}
render() {
return <HelloWorld tech={this.state.tech}/>
}
}
export default App;
在 src
目录下创建一个 React 无状态组件 <HelloWorld/>
:
import React from 'react'
const HelloWorld = ({tech}) => {
return <div>
Hello World <span>{tech}!</span>
</div>
}
export default HelloWorld
可以看到,默认情况下是, React 内部通过 this.state.tech
,传递给子组件。
接下来,把内部传递 state
修改成 Redux 传递的 state
。
先 npm install redux --save
安装 Redux 库,安装成功后,在 src/App.js
中添加 redux
进来,如下:
import { createStore } from 'redux'
const store = createStore()
通过 createStore()
是生成 store
,createStore
函数接受一些参数,第一个参数是 reducer
,如下:
const store = createStore(reducer)
现在把 reducer
传进来,新建一个 reducer
,在 src/reducers
目录下新建一个 index.js
文件。reducer
返回一个 state
(这里先简单地返回一个 state
),index.js
文件如下:
export default (state) => {
return state
}
createStore
还可以接受第二个参数 initialState
,initialState
是初始状态,修改后如下:
const initialState = { tech: "Redux"};
const store = createStore(reducer, initialState);
还记得上一篇文章,state
(你的钱)是从 store
(收营员) 返回的吗?使用 store.getState().tech
返回新的 state
,src/App.js
完整代码如下:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import HelloWorld from './HelloWorld'
import { createStore } from 'redux'
import reducer from './reducers'
const initialState = { tech: 'Redux'}
const store = createStore(reducer, initialState)
class App extends Component {
render() {
return <HelloWorld tech={store.getState().tech}/>
}
}
export default App
现在为止,项目从组件内部 state
传递,现在从 redux
传递 state
了。
以上就是简单的 Hello Redux 入门程序。
这一篇文章主要讲了,从 React 中内部更新 state 到 Redux 获取 state。
下面总结下知识点:
createStore
是一个工厂函数,用于生成 store
createStore
第一个参数 reducer
,是必须传递的,第二个参数是初始 状态 initialState
reducer
是一个纯函数,接受两个参数,一个是初始化 state
,另一个是 action
state
store.getState()
将会返回当前的 state
这是分析 redux 源码系列第一篇。
下面是 Redux 版本4.0.1 源码的解读。
createStore
createStore(reducer, [preloadedState], enhancer)
createStore
方法接受三个参数,第一个参数为 reducer
,第二个参数是 preloadedState
(可选),第三个是参数是 enhancer
。返回一个 对象 store
。store
中包含方法 dispatch
、getState
、subscribe
、replaceReducer
、[$$observable]
。
参数 reducer
有可能是一个 reducer
,如果有个多个 reducer
,则通过 combineReducer
函数执行后返回函数作为 reducer
。
参数 preloadedState
代表初始状态,很多时候都不传,不传该参数时候且 enhancer
是函数情况,createStore
函数内部会把 enhancer
,作为第二个参数使用,源码如下:
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
参数 enhancer
是 store
增强器,是一个函数, createStore
作为 enhancer
函数的参数,这里用到了函数式编程,返回函数又传递 reducer
和 preloadState
参数,执行最终返回一个增强的 store
。enhancer
常有的是 applyMiddleware()
返回值,react-devrools()
等工具,源码如下:
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
介绍方法之前,下面是一些变量:
let currentReducer = reducer // 当前的 reducer
let currentState = preloadedState // 当前的 state
let currentListeners = [] // 当前的监听者
let nextListeners = currentListeners // 监听者的备份
let isDispatching = false // 是否正在执行 dispatch 动作
dispatch
dispatch(action)
dispatch
方法,接受一个参数action
,action
是一个对象,该对象必须包括 type
属性,否则会报错。dispatch
函数内部,主要是执行了 reducer()
函数,得到最新 state
,赋值给 currentState
,执行订阅者,dispatch
函数返回一个 action
对象。源码如下:
function dispatch(action) {
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) { // 如果正在 dispatch,再次触发 dispatch,则报错
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true // 表示正在执行 `dispatch`
currentState = currentReducer(currentState, action) // 执行最新的 Reducer,返回最新的 state
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners) // 每次 dispatch 执行,订阅者都会执行
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
getState
getState()
获取最新的 state,就是返回 currentState。源码如下:
function getState() {
return currentState
}
subscribe
添加事件订阅者,每次触发 dispatch(action)
时候,订阅者都会执行。返回取消订阅方法 unSubscribe
。这里采用了观察者模式/发布-订阅者模式。源码如下:
注意:订阅者必须是函数,否则报错。
function subscribe(listener) {
if (typeof listener !== 'function') { // 订阅者必须是函数
throw new Error('Expected the listener to be a function.')
}
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#subscribe(listener) for more details.'
)
}
let isSubscribed = true
nextListeners.push(listener) // 订阅消息,每次 dispatch(action) 时候,订阅者都会接受到消息
return function unsubscribe() { // 返回取消订阅方法
if (!isSubscribed) {
return
}
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#subscribe(listener) for more details.'
)
}
isSubscribed = false
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1) // 取消订阅者
}
}
observable
让一个对象可被观察。被观察对象必须是对象,否则报错。这里作者用到了第三方库 symbol-observable
,对 currentState
进行观察。源码如下:
ECMAScript Observables
目前只是草案,还没正式使用。
function observable() {
const outerSubscribe = subscribe
return {
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}
function observeState() {
if (observer.next) {
observer.next(getState()) // 对 currentState 进行监听
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
}
}
}
replaceReducer
替换 reducer
,一般用不到。源码也简单,把 nextReducer
赋值给 currentReducer
。源码如下:
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
}
上面 dispatch
、getState
、subscribe
、replaceReducer
、[$$observable]
这些方法使用了闭包,一直保持对 currentState
、currentReducer
、currentListeners
、nextListeners
、isDispatching
等变量的引用。比如,dispatch
改变 currentState
,相应的其他方法中,currentState
也会跟着变,所以这些方法之间是存在联系的。
applyMiddleware
执行中间件的方法,中间件可以有很多,有自定义的如:打印日志,也有比较知名的 如:redux-chunk
、redux-promise
、redux-saga
等,后续会分析。
applyMiddleware()
通常是上面提到的 createStore
方法的第三个参数 enhancer
,源码如下:
enhancer(createStore)(reducer, preloadedState)
结合上面代码可以等价于
applyMiddleware(...middlewares)(createStore)(reducer, preloadedState)
applyMiddleware
使用了函数式编程,接收第一个参数元素为 middlewares
的数组,返回值接收createStore
为参数的函数,返回值接收 dispatch
函数,接收一个 action
如 (reducer, state)
,最终返回增强版的 store
。
源码如下:
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args) // args 为 (reducer, initialState)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = { // 给 middleware 的 store
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI)) // chain 是一个参数为 next 的匿名函数的数组(中间件执行返回的函数)
dispatch = compose(...chain)(store.dispatch) // 经过 compose 函数后返回的结果,是经过 middlewares 包装后的 dispatch
return { // 返回 dispatch 增强后的 store
...store,
dispatch
}
}
}
compose
源码如下:
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)))
}
虽然源代码没几行,作者实现方式太厉害了,如果理解起来有点费劲,可以看看下面例子分析:
var arr = [
function fn1(next){
return action => {
// next 为 fn2 return 函数(dispatch 函数)
const returnValue = next(action)
return returnValue
}
},
function fn2(next) {
return action => {
// next 为 fn3 return 函数(dispatch 函数)
const returnValue = next(action)
return returnValue
}
},
function fn3(next3) {
return action => {
// next 为 dispatch。
const returnValue = next3(action)
return returnValue
}
},
]
var fn = arr.reduce((a, b) => (args) => a(b(args)))
fn // (args) => a(b(args)) 等价于 (dispatch) => fn1(fn2(fn3(dispatch)))
// reduce函数 执行顺序:
// 第一次:
// 变量 a: ƒ fn1(args)
// 变量 b: ƒ fn2(args)
// 返回值: (args) => fn1(fn2(args))
// 第一次返回值赋值给 a
// 变量 a: (args) => fn1(fn2(args))
// 变量 b: ƒ fn3(args)
// 第二次:
// 变量 a : (args) => fn1(fn2(args))
// 变量 b: ƒ fn3(args)
// 返回值: (args) => fn1(fn2(fn3(args)))
所以作者的用意是 compose(f, g, h)
返回 (...args) => f(g(h(...args)))
,其中 f、g、h
是一个个中间件 middleware
。
结合上面 applyMiddleware
源码有一段 dispatch = compose(...chain)(store.dispatch)
,所以 args
为 store.dispatch
,(store.dispatch) => f(g(h(store.dispatch)))
,执行第一个中间件h(store.dispatch)
,返回 dispatch
函数(参数为 action 的函数)作为下一个中间件的参数,通过一层层的传递,最终返回一个经过封装的 dispatch
函数。
下面是打印日志中间件例子:
const logger = store => next => action => {
console.log('dispatch', action)
next(action)
console.log('finish', action)
}
const logger2 = store => next2 => action2 => {
console.log('dispatch2', action2)
next2(action2)
console.log('finish2', action2)
}
...
const store = createStore(rootReducer, applyMiddleware(logger, logger2))
注意:但如果在某个中间件中使用了 store.dipsatch()
,而不是 next()
,那么就会回到起始位置,会造成无限循环了。
每次触发一个 dispatch,中间件都会执行,打印的顺序为 dispatch distapch2 finish2 finish
。
combineReducer
这个 API 理解为:如果有多个 reducer,那么需要把它们合成一个 reducer,在传给函数 createStore(reducer)
。
核心源码如下:
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] // reducer 一定是函数
}
}
const finalReducerKeys = Object.keys(finalReducers)
return function combination(state = {}, action) { // 返回一个新的 reducer 函数
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) { // 每次执行一次 dipatch(action),所有的 reducer 都会执行
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state // 判断 state 是否有变化,如果有则返回新的 state
}
}
bindActionCreators
这个 API 可以理解为:生成 action 的方法。主要是把 dispatch 也封装进 bindActionCreator(actionCreator, dispatch)
方法里面去,所以调用时候可以直接触发 dispatch(action)
,不需要在手动调用 dispatch。源码如下:
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
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
}
这篇文章主要讲的是分析 react-router 源码,版本是 v5.x,以及 SPA 路由实现的原理。
单页面应用都用到了路由 router,目前来看实现路由有两种方法 hash 路由和 H5 History API 实现。
而 react-router 路由,则是用到了 history 库,该库其实是对 hash 路由、history 路由、memory 路由(客户端)进行了封装。
下面先看看 hash 和 history 是怎样实现路由的。
hash 是 location 的属性,在 URL 中是 #后面的部分。如果没有#,则返回空字符串。
hash 路由主要实现原理是:hash 改变时,页面不发生跳转,即 window 对象可以对 hash 改变进行监听(hashchange 事件),只要 url 中 hash 发生改变,就会触发回调。
利用这个特性,下面模拟实现一个 hash 路由。
实现步骤:
代码如下:
hash.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hash router</title>
</head>
<body>
<ul>
<li><a href="#/">turn yellow</a></li>
<li><a href="#/blue">turn blue</a></li>
<li><a href="#/green">turn green</a></li>
</ul>
<button id='back'>back</button>
<button id='forward'>forward</button>
<script src="./hash.js"></script>
</body>
</html>
hash.js
class Routers {
constructor() {
this.routes = {}
this.currentUrl = ''
this.history = []; // 记录 hash 历史值
this.currentIndex = this.history.length - 1; // 默认指向 history 中最后一个
// 默认前进、后退
this.isBack = false;
this.isForward = false;
this.onHashChange = this.onHashChange.bind(this)
this.backOff = this.backOff.bind(this)
this.forward = this.forward.bind(this)
window.addEventListener('load', this.onHashChange, false);
window.addEventListener('hashchange', this.onHashChange, false); // hash 变化监听事件,support >= IE8
}
route(path, callback) {
this.routes[path] = callback || function () { }
}
onHashChange() {
// 既不是前进和后退,点击 a 标签时触发
if (!this.isBack && !this.isForward) {
this.currentUrl = location.hash.slice(1) || '/'
this.history.push(this.currentUrl)
this.currentIndex++
}
this.routes[this.currentUrl]()
this.isBack = false
this.isForward = false
}
// 后退功能
backOff() {
this.isBack = true
this.currentIndex = this.currentIndex <= 0 ? 0 : this.currentIndex - 1
this.currentUrl = this.history[this.currentIndex]
location.hash = `#${this.currentUrl}`
}
// 前进功能
forward() {
this.isForward = true
this.currentIndex = this.currentIndex >= this.history.length - 1 ? this.history.length - 1 : this.currentIndex + 1
this.currentUrl = this.history[this.currentIndex]
location.hash = `#${this.currentUrl}`
}
}
// 初始添加所有路由
window.Router = new Routers();
Router.route('/', function () {
changeBgColor('yellow');
});
Router.route('/blue', function () {
changeBgColor('blue');
});
Router.route('/green', function () {
changeBgColor('green');
});
const content = document.querySelector('body');
const buttonBack = document.querySelector('#back');
const buttonForward = document.querySelector('#forward')
function changeBgColor(color) {
content.style.backgroundColor = color;
}
// 模拟前进和回退
buttonBack.addEventListener('click', Router.backOff, false)
buttonForward.addEventListener('click', Router.forward, false)
hash 路由兼容性好,缺点就是实现一套路由比较复杂些。
history 是 HTML5 新增的 API,允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。
history 包含的属性和方法:
history.state
读取历史堆栈中最上面的值history.length
浏览历史中元素的数量window.history.replaceState(obj, title, url)
取代当前浏览历史中当前记录window.history.pushState(obj, title, url)
往浏览历史中添加历史记录,不跳转页面window.history.popstate(callback)
对历史记录发生变化进行监听,state 发生改变时,触发回调事件window.history.back()
后退,等价于浏览器返回按钮window.history.forward()
前进,等价于浏览器前进按钮window.history.go(num)
前进或后退几个页面,num
为负数时,后退几个页面实现步骤:
popstate
事件,state 发生改变时,触发回调代码如下:
history.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>h5 router</title>
</head>
<body>
<ul>
<li><a href="/">turn yellow</a></li>
<li><a href="/blue">turn blue</a></li>
<li><a href="/green">turn green</a></li>
</ul>
<script src='./history.js'></script>
</body>
</html>
history.js
class Routers {
constructor() {
this.routes = {};
this._bindPopState();
}
route(path, callback) {
this.routes[path] = callback || function () { };
}
go(path) {
history.pushState({ path: path }, null, path);
this.routes[path] && this.routes[path]();
}
_bindPopState() {
window.addEventListener('popstate', e => { // 监听 history.state 改变
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();
});
}
}
// 初始时添加所有路由
window.Router = new Routers();
Router.route('/', function () {
changeBgColor('yellow');
});
Router.route('/blue', function () {
changeBgColor('blue');
});
Router.route('/green', function () {
changeBgColor('green');
});
const content = document.querySelector('body');
const ul = document.querySelector('ul');
function changeBgColor(color) {
content.style.backgroundColor = color;
}
ul.addEventListener('click', e => {
if (e.target.tagName === 'A') {
debugger
e.preventDefault();
Router.go(e.target.getAttribute('href'));
}
});
兼容性:support >= IE10
react-router 分为四个包,分别为 react-router
、react-router-dom
、react-router-config
、react-router-native
,其中 react-router-dom
是浏览器相关 API,react-router-native
是 React-Native 相关 API,react-router
是核心也是共同部分 API,react-router-config
是一些配置相关。
react-router 是 React指定路由,内部 API 的实现也是继承 React 一些属性和方法,所以 react-router 内 API 也是 React 组件。
react-router 还用到了 history
库,这个库主要是对 hash 路由、history 路由、memory 路由的封装。
下面我们讲到的是 react-router v5.x 版本,用到了 context,context 减少了组件之间显性传递 props,具体用法可以看看官方文档。
创建 context:
var createNamedContext = function createNamedContext(name) {
var context = createContext();
context.displayName = name;
return context;
};
var context = createNamedContext("Router");
定义一个类 Router,它继承 React.Component 属性和方法,所以 Router 也是 React 组件。
_inheritsLoose(Router, _React$Component);
// 等价于
Router.prototype = Object.create(React.Component)
在 Router 原型对象上添加 React 生命周期 componentDidMount
、componentWillUnmount
、render
方法。
一般情况下,Router 都是作为 Route 等其他子路由的上层路由,使用了 context.Provider
,接收一个 value 属性,传递 value 给消费子组件。
var _proto = Router.prototype;
_proto.componentDidMount = function componentDidMount() {
// todo
};
_proto.componentWillUnmount = function componentWillUnmount(){
// todo
};
_proto.render = function render() {
return React.createElement(context.Provider, props);
};
history 库中有个方法 history.listen(callback(location))
对 location 进行监听,只要 location 发生变化了,就会 setState 更新 location,消费的子组件也可以拿到更新后的 location,从而渲染相应的组件。
核心源码如下:
var Router =
function (_React$Component) {
// Router 从 React.Component 原型上的继承属性和方法
_inheritsLoose(Router, _React$Component);
Router.computeRootMatch = function computeRootMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
};
function Router(props) { // 首先定义一个类 Router,也是 React 组件
var _this;
_this = _React$Component.call(this, props) || this; // 继承自身属性和方法
_this.state = {
location: props.history.location
};
_this._isMounted = false;
_this._pendingLocation = null;
if (!props.staticContext) { // 如果不是 staticRouter,则对 location 进行监听
_this.unlisten = props.history.listen((location) => { // 监听 history.location 变化,如果有变化,则更新 locaiton
if (_this._isMounted) {
_this.setState({
location: location
});
} else {
_this._pendingLocation = location;
}
});
}
return _this;
}
var _proto = Router.prototype; // 组件需要有生命周期,在原型对象上添加 componentDidMount、componentWillUnmount、render 方法
_proto.componentDidMount = function componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({
location: this._pendingLocation
});
}
};
_proto.componentWillUnmount = function componentWillUnmount() {
if (this.unlisten) this.unlisten(); // 停止监听 location
};
_proto.render = function render() {
// 使用了 React Context 传递 history、location、match、staticContext,使得所有子组件都可以获取这些属性和方法
// const value = {
// history: this.props.history,
// location: this.state.location,
// match: Router.computeRootMatch(this.state.location.pathname),
// staticContext: this.props.staticContext
// }
// return (
// <context.Provider value={value}>
// {this.props.children}
// </context.Provider>
// )
return React.createElement(context.Provider, {
children: this.props.children || null,
value: {
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext // staticContext 是 staticRouter 中的 API,不是公用 API
}
});
};
return Router;
}(React.Component);
Route 一般作为 Router 的子组件,主要是匹配 path 和渲染相应的组件。
Route 使用了 context.Consumer
(消费组件),订阅了 Router 提供的 context,一旦 location 发生改变,context 也会改变,判断当前 location.pathname
是否与子组件的 path 是否匹配,如果匹配,则渲染对应组件,否则就不渲染。
Route 因为有些库传递组件方式不同,所以有多种渲染,部分代码如下:
const {children, render, component} = this.props
let renderEle = null;
// 如果有 children,则渲染 children
if (children && !isEmptyChildren(children)) renderEle = children
// 如果组件传递的是 render 及 match 是匹配的,则渲染 render
if (render && match) renderEle = render(props)
// 如果组件传递 component 及 match 是匹配的,则渲染 component
if (component && match) renderEle = React.createElement(component, props)
return (
<context.Provider value={obj}>
{renderEle}
</context.Provider>
)
核心源码如下:
var Route =
function (_React$Component) {
_inheritsLoose(Route, _React$Component);
function Route() {
return _React$Component.apply(this, arguments) || this;
}
var _proto = Route.prototype;
_proto.render = function render() {
var _this = this;
// context.Consumer 每个 Route 组件都可以消费 Router 中 Provider 提供的 context
return React.createElement(context.Consumer, null, function (context$$1) {
var location = _this.props.location || context$$1.location;
var match = _this.props.computedMatch ? _this.props.computedMatch
: _this.props.path ? matchPath(location.pathname, _this.props) : context$$1.match; // 是否匹配当前路径
var props = _extends({}, context$$1, { // 处理用 context 传递的还是自己传递的 location 和 match
location: location,
match: match
});
var _this$props = _this.props,
children = _this$props.children,
component = _this$props.component,
render = _this$props.render;
if (Array.isArray(children) && children.length === 0) {
children = null;
}
// let renderEle = null
// 如果有 children,则渲染 children
// if (children && !isEmptyChildren(children)) renderEle = children
// 如果组件传递的是 render 及 match 是匹配的,则渲染 render
// if (render && props.match) renderEle = render(props)
// 如果组件传递 component 及 match 是匹配的,则渲染 component
// if (component && props.match) renderEle = React.createElement(component, props)
// return (
// <context.Provider value={props}>
// {renderEle}
// </context.Provider>
// )
return React.createElement(context.Provider, { // Route 内定义一个 Provider,给 children 传递 props
value: props
}, children && !isEmptyChildren(children) ? children : props.match ? component ? React.createElement(component, props) : render ? render(props) : null : null);
});
};
return Route;
}(React.Component);
重定向路由:from 是从哪个组件来,to 表示要定向到哪里。
<Redirect from="home" to="dashboard" />
根据有没传属性 push
,有传则是往 state 堆栈中新增(history.push
),否则就是替代(history.replace
)当前 state。
Redirect 使用了 context.Consumer
(消费组件),订阅了 Router 提供的 context,一旦 location 发生改变,context 也会改变,则也会触发重定向。
源码如下:
function Redirect(_ref) {
var computedMatch = _ref.computedMatch,
to = _ref.to,
_ref$push = _ref.push,
push = _ref$push === void 0 ? false : _ref$push;
return React.createElement(context.Consumer, null, (context$$1) => { // context.Consumer 第三个参数是一个函数
var history = context$$1.history,
staticContext = context$$1.staticContext;
// method 方法:判断是否是替换(replace)当前的 state,还是往 state 堆栈中添加(push)一个新的 state
var method = push ? history.push : history.replace;
// 生成新的 location
var location = createLocation(computedMatch ? typeof to === "string" ? generatePath(to, computedMatch.params) : _extends({}, to, {
pathname: generatePath(to.pathname, computedMatch.params)
}) : to);
if (staticContext) { // 当渲染一个静态的 context 时(staticRouter),立即设置新 location
method(location);
return null;
}
// Lifecycle 是一个 return null 的空组件,但定义了 componentDidMount、componentDidUpdate、componentWillUnmount 生命周期
return React.createElement(Lifecycle, {
onMount: function onMount() {
method(location);
},
onUpdate: function onUpdate(self, prevProps) {
var prevLocation = createLocation(prevProps.to);
// 触发更新时,对比前后 location 是否相等,不相等,则更新 location
if (!locationsAreEqual(prevLocation, _extends({}, location, {
key: prevLocation.key
}))) {
method(location);
}
},
to: to
});
});
}
Switch 组件的子组件必须是 Route 或 Redirect 组件。
Switch 使用了 context.Consumer
(消费组件),订阅了 Router 提供的 context,一旦 location 发生改变,context 也会改变,判断当前 location.pathname
是否与子组件的 path 是否匹配,如果匹配,则渲染对应的子组件,其他都不渲染。
源码如下:
var Switch =
function (_React$Component) {
_inheritsLoose(Switch, _React$Component);
function Switch() {
return _React$Component.apply(this, arguments) || this;
}
var _proto = Switch.prototype;
_proto.render = function render() {
var _this = this;
return React.createElement(context.Consumer, null, function (context$$1) {
var location = _this.props.location || context$$1.location;
var element, match;
React.Children.forEach(_this.props.children, function (child) {
if (match == null && React.isValidElement(child)) {
element = child;
var path = child.props.path || child.props.from;
match = path ? matchPath(location.pathname, _extends({}, child.props, {
path: path
})) : context$$1.match;
}
});
return match ? React.cloneElement(element, {
location: location,
computedMatch: match // 加强版的 match
}) : null;
});
};
return Switch;
}(React.Component);
Link 组件作用是跳转到指定某个路由。Link 实际是对 <a>
标签进行了封装。
点击时会触发以下:
e.preventDefault()
,所以页面没有发生跳转。history.replace
),否则是往 state 堆栈中新增(history.push
),从而路由发生了改变。核心源码如下:
function LinkAnchor(_ref) {
var innerRef = _ref.innerRef,
navigate = _ref.navigate,
_onClick = _ref.onClick,
rest = _objectWithoutPropertiesLoose(_ref, ["innerRef", "navigate", "onClick"]); // 剩余属性
var target = rest.target;
return React.createElement("a", _extends({}, rest, { // a 标签
ref: innerRef ,
onClick: function onClick(event) {
try {
if (_onClick) _onClick(event);
} catch (ex) {
event.preventDefault(); // 使用 e.preventDefault() 防止跳转
throw ex;
}
if (!event.defaultPrevented && // onClick prevented default
event.button === 0 && ( // ignore everything but left clicks
!target || target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
navigate(); // 改变 location
}
}
}));
}
function Link(_ref2) {
var _ref2$component = _ref2.component,
component = _ref2$component === void 0 ? LinkAnchor : _ref2$component,
replace = _ref2.replace,
to = _ref2.to, // to 跳转链接的路径
rest = _objectWithoutPropertiesLoose(_ref2, ["component", "replace", "to"]);
return React.createElement(__RouterContext.Consumer, null, function (context) {
var history = context.history;
// 根据 to 生成新的 location
var location = normalizeToLocation(resolveToLocation(to, context.location), context.location);
var href = location ? history.createHref(location) : "";
return React.createElement(component, _extends({}, rest, {
href: href,
navigate: function navigate() {
var location = resolveToLocation(to, context.location);
var method = replace ? history.replace : history.push;
method(location); // 如果有传 replace,则是替换掉当前 location,否则往 history 堆栈中添加一个 location
}
}));
});
}
BrowserRouter 用到了 H5 history API,所以可以使用 pushState、replaceState 等方法。
源码中主要是用到了 history 库 createBrowserHistory 方法创建了封装过 history 对象。
把封装过的 history 对象传递给 Router 的 props。
var BrowserRouter =
function (_React$Component) {
_inheritsLoose(BrowserRouter, _React$Component); // BrowserRouter 继承 React.Component 属性和方法
function BrowserRouter() {
var _this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this; // 继承自身属性和方法
_this.history = createBrowserHistory(_this.props); // 创建 browser history 对象,是支持 HTML 5 的 history API
return _this;
}
var _proto = BrowserRouter.prototype;
_proto.render = function render() {
return React.createElement(Router, { // 以 Router 为 element,history 和 children 作为 Router 的 props
history: this.history,
children: this.props.children
});
};
return BrowserRouter;
}(React.Component);
HashRouter 与 BrowserRouter 的区别,主要是创建是以 window.location.hash 为对象,返回一个 history,主要是考虑到兼容性问题。
var HashRouter =
function (_React$Component) {
_inheritsLoose(HashRouter, _React$Component);
function HashRouter() {
var _this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this;
_this.history = createHashHistory(_this.props); // 创建 hash history 对象,兼容老浏览器 hash,其他和 BrowserRouter 没有大区别
return _this;
}
var _proto = HashRouter.prototype;
_proto.render = function render() {
return React.createElement(Router, {
history: this.history,
children: this.props.children
});
};
return HashRouter;
}(React.Component);
react-router 分为四个包,分别为 react-router
、react-router-dom
、react-router-config
、react-router-native
,其中 react-router-dom
是浏览器相关 API,react-router-native
是 React-Native 相关 API,react-router
是核心也是共同部分 API,react-router-config
是一些配置相关。
react-router 是 React指定路由,内部 API 的实现也是继承 React 一些属性和方法,所以说 react-router 内 API 也是 React 组件。
react-router 还用到了 history 库,这个库主要是对 hash 路由、history 路由、memory 路由的封装。
Router 都是作为 Route 等其他子路由的上层路由,使用了 context.Provider
,接收一个 value 属性,传递 value 给消费子组件。
history 库中有个方法 history.listen(callback(location))
对 location 进行监听,点击某个 Link 组件,改变了 location,只要 location 发生变化了,通过 context 传递改变后的 location,消费的子组件拿到更新后的 location,从而渲染相应的组件。
相信大家在工作过程中,肯定有或多或少听说过同步和异步。那么同步和异步到底是什么呢?它们之间有什么区别?举个栗子,煮开水,同步就是把水放上去烧,得一直等水开,中途不能做其他事情。而异步,则是把水放上去烧,让水在烧,你可以玩手机看电视,等水开了把火关掉。同样的,代码中也是一样,同步是现在发生的,异步是未来某个时刻发生的。
同步代码:
// 烧开水
function boilWater() {
var water;
while(water_is_boiled) {
// search
}
return water;
}
var boiledWater = boilWater();
// 做其他事情
doSomethingElse();
在介绍异步编程前,先介绍下JavaScript运行机制,因为JS 是单线程运行的,所以这意味着两段代码不能同时运行,而是必须一个接一个地运行,所以,在同步代码执行过程中,异步代码是不执行的。只有等同步代码执行结束后,异步代码才会被添加到事件队列中。
(学习1: 阮一峰-Event-Loop)。
JS 中异步操作还挺多的,常见的分以下几种:
setTimeout(
function() {
console.log("Hello!");
}, 1000);
setTimout(setInterval)并不是立即就执行的,这段代码意思是,等 1s后,把这个 function 加入任务队列中,如果任务队列中没有其他任务了,就执行输出 'Hello'。
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'hello';
}, 2000);
}
执行上面代码,发现 outerScopeVar 输出是 undefined,而不是 hello。之所以这样是因为在异步代码中返回的一个值是不可能给同步流程中使用的,因为 console.log(outerScopeVar) 是同步代码,执行完后才会执行 setTimout。
helloCatAsync(function(result) {
console.log(result);
});
function helloCatAsync(callback) {
setTimeout(
function() {
callback('hello')
}
, 1000)
}
把上面代码改成,传递一个callback,console 输出就会是 hello。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
console.log(xhr.responseText);
} else {
console.log( xhr.status);
}
}
xhr.open('GET', 'url', false);
xhr.send();
上面这段代码,xhr.open 中第三个参数默认为 false 异步执行,改为 true 时为同步执行。
语法:
new Promise( function(resolve, reject) {...});
Promise 对象是由关键字 new 及其构造函数来创建的。这个“处理器函数”接受两个函数 resolve 和 reject 作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数。
new Promise 返回一个 promise 对象,在遇到 resolve 或 reject之前,状态一直是pending,如果调用 resolve 方法,状态变为 fulfilled,如果调用了 reject 方法,状态变为 rejected。
function delay() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(666)
}, 2000)
})
}
delay()
.then(function(value){
console.log('resolve...',value);
})
.catch(function(err){
cosole.log(err)
})
上面代码中,2s 后调用 resolve 方法,然后调用 then 方法,没有调用 cacth方法。
注意:then() 和 catch() 都是异步操作。
下面 promise 和 ajax 结合例子:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
resovle(xhr.responseText);
} else {
reject( xhr.status);
}
}
xhr.open('GET', url, false);
xhr.send();
});
}
ajax('/test.json')
.then(function(data){
console.log(data);
})
.cacth(function(err){
console.log(err);
});
学习1:promise-mdn
学习2:promise-google
语法:
async function name([param[, param[, ... param]]]) { statements }
调用 async 函数时会返回一个 promise 对象。当这个 async 函数返回一个值时,Promise 的 resolve 方法将会处理这个值;当 async 函数抛出异常时,Promise 的 reject 方法将处理这个异常值。
async 函数中可能会有await 表达式,这将会使 async 函数暂停执行,等待 Promise 正常解决后继续执行 async 函数并返回解决结果(异步)。
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function add1(x) {
var a = resolveAfter2Seconds(20);
var b = resolveAfter2Seconds(30);
return x + await a + await b;
}
add1(10).then(v => {
console.log(v); // 2s 后打印 60, 两个 await 是同时发生的,也就是并行的。
});
async function add2(x) {
var a = await resolveAfter2Seconds(20);
var b = await resolveAfter2Seconds(30);
return x + a + b;
}
add2(10).then(v => {
console.log(v); // 4s 后打印 60,按顺序完成的。
});
上面代码是 mdn 的一个例子。
看下面两段代码:
async function series() {
await wait(500);
await wait(500);
return "done!";
}
async function parallel() {
const wait1 = wait(500);
const wait2 = wait(500);
await wait1;
await wait2;
return "done!";
}
第一段代码执行完毕需要 1000毫秒,这段 await 代码是按顺序执行的;第二段代码执行完毕只需要 500 毫秒,这段 await 代码是并行的。
学习1:async-mdn
学习2:async-google
下面代码输出顺序是什么?
async function fn1() {
console.log(1);
await fn2();
console.log(3);
};
function fn2() {
console.log(2);
};
fn1();
new Promise(function(resolve, reject) {
console.log(4);
resolve();
}).then(function(){
console.log(5)
})
本篇文章讲解JavaScript 中垃圾回收机制,内存泄漏,结合一些常遇到的例子,相信各位看完后,会对JS 中垃圾回收机制有个深入的了解。
首先,不管什么程序语言,内存生命周期基本是一致的:
在所有语言中第一和第二部分都很清晰。最后一步在低级语言中(C语言等)很清晰,但是在像JavaScript 等高级语言中,这一步是隐藏的、透明的。因为JavaScript 具有自动垃圾收集机制(Garbage collected )。在编写 JS 时,不需要关心内存使用问题,所需内存分配以及无用内存的回收完全实现了自动管理。
内存泄漏(memory leaks),什么情况下回导致内存泄漏?可以简单理解为有些代码本来要被回收的,但没有被回收,还一直占用着操作系统内存,从而越积越多,最终会导致内存泄漏(可以理解为,内存满了,就溢出了)。
分配给web浏览器的可用内存数量通常要比分配给桌面应用程序少。这样做的目的主要是处于安全方面考虑,目的是防止运行JS 的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。
因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用。这个方法叫做解除引用。这一做法适用于大多数的全局变量和全局对象的属性。局部变量会在他们离开执行环境时自动被解除引用。
解除一个值的引用并不意味着自动回收改值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
通常,垃圾收集器(garbage collector)在运行时候会给储存在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除的工作。
那标记清除具体是如何呢?有以下几种算法:
var n = 123;
// 给数值变量分配内存
var s = "azerty";
// 给字符串分配内存
// 给对象及其包含的值分配内存
var o = {
a: 1,
b: null
};
// 给函数(可调用的对象)分配内存
function f(a){
return a + 2;
}
function foo(arg) {
// 此处bar 是全局变量,window.bar 可以访问,所以也不会被回收
bar = "this is a hidden global variable";
}
function foo() {
// 此处this 代表 window
this.variable = "potential accidental global";
}
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
// 上面这段代码,定时器setInterval 和 someResource 一直存在,不会被回收。可以改成下面代码
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// 手动移除事件监听器和变量
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
var intervalId = null, params;
function createChunks() {
var div, foo, i, str;
for (i = 0; i < 20; i++) {
div = document.createElement("div");
str = new Array(1000000).join('x');
foo = {
str: str,
div: div
};
div.foo = foo;
}
}
function start() {
if (intervalId) {
return;
}
intervalId = setInterval(createChunks, 1000);
}
function stop() {
if (intervalId) {
// 清除定时器
clearInterval(intervalId);
}
// 清除变量
intervalId = null;
}
链接观察垃圾回收是怎么工作的—Google: Watching the GC work
在上面图片中,可以观察到,点击 start 按钮,内存和节点数暴增,当点击stop 时,垃圾收集器回收了这些定时器、变量等,从而释放了内存。
promise我看了好机会,但做题时候都会出错,后来我知道学一个知识点,光看不动手敲代码是不够的。下面几道题做完,真的受益匪浅,题目大部分来自网上,文章后面会提到。
首先,promise 基础知识可以看 mdn。
const request = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
})
}
request(url1)
.then(data1 => request(data1.url))
.then(data2 => request(data2.url))
.catch(err => {
throw new Error(err)
})
request
是个promise
对象,有三个状态 pedding、fulfilled、rejected
。初始状态为 pendding
,调用 resolve
时,状态变为 fulfilled
,调用rejected
时,状态变为 rejected
。
const promise = new Promise((resolve, reject) => {
// resolve('fulfilled');
reject('111');
})
promise.then(result => {
console.log('success...', result)
}, err => {
console.log("err...", err);
})
promise.then(onFulfilled, onRejected)
,then
接受两个参数,onFulfilled
成功时调用,onRejected
失败时候调用。 一次只会调用其中一个函数。
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = new Promise((resolve, reject) => {
resolve(2);
});
const p3 = new Promise((resolve, reject) => {
resolve(3);
});
Promise.all([p1, p2, p3])
.then(data => console.log(data)) // [1, 2, 3]
.catch(err => console.log('err...',err))
Promise.race([p1, p2, p3])
.then(data => console.log(data)) // 1
.catch(err => console.log("err...", err));
Promise.all()
等待所有完成后才调用 then
Promise.race()
谁先处理完就直接调用后面函数Promise.resolve()
返回一个 fulfilled
状态的promise
对象Promise.reject()
返回一个 rejected
状态的promise对象const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('resolve');
console.log(2);
});
console.log(promise);
promise.then(() => {
console.log(3)
});
console.log(4);
// 1 2 Promise { 'resolve' } 4 3
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
// promise1 Promise { <pending> }
// promise2 Promise { <pending> }
// promise1 Promise { 'success' }
// promise2 Promise {
// <rejected> Error: error!!!
promise
有 3 种状态:pending、fulfilled
或 rejected
。状态改变只能是 pending->fulfilled
或者 pending->rejected
,状态一旦改变则不能再变。上面 promise2
并不是 promise1
,而是返回的一个新的 Promise
实例
Promise.resolve()
.then(() => {
return new Error("error!!!");
})
.then(res => {
console.log("then: ", res);
})
.catch(err => {
console.log("catch: ", err);
});
//输出 then: Error: error!!!
.then
或者 .catch
中 return
一个 error
对象并不会抛出错误,所以不会被后续的 .catch 捕获promise
的值都会被包裹成 promise
对象,即 return new Error('error!!!')
等价于 return Promise.resovle(new Error('error!!!'))
Promise.resolve()
.then(() => {
throw new Error("error!!!");
// return Promise.reject(new Error("error!!!"));
}, err => {
console.log('then1: ', err)
})
.then(res => {
console.log("then2: ", res);
})
.catch(err => {
console.log("catch: ", err);
});
//输出 catch: Error: error!!!
第一个 then
抛出错误,需要后面的捕获错误。
Promise.reject(1)
.then(() => {
return new Error('error!!!')
})
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
});
// 输出 catch 1
前面两个then
并没有捕捉错误,所以错误抛到了最后面的catch
,中间 then
并不执行了。
Promise.reject(1)
.then(() => {
return new Error("error!!!");
})
.then(res => {
console.log("then: ", res);
}, err => {
console.log("haha", err);
})
.catch(err => {
console.log("catch: ", err);
});
//输出 haha 1
错误被第二个 then
捕捉了,最后的 catch
也就不执行了。
var p = new Promise(function(resolve, reject){
resolve(1);
});
p.then(function(value){ //第一个then
console.log(value);
return value*2;
}).then(function(value){ //第二个then
console.log(value);
}).then(function(value){ //第三个then
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //第四个then
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //第五个then
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
// 1
// 2
// undefined ,因为上一个 then() 没有返回值
// resolve
// reject: reject
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log);
// 1
.then
或者 .catch
的参数期望是函数,传入非函数则会发生值穿透。
const promise = Promise.resolve().then(() => {
return promise;
});
promise.catch(console.error);
// TypeError: Chaining cycle detected for promise #<Promise>
.then
或 .catch`` 返回的值不能是
promise``` 本身,否则会造成死循环。
设计模式几十种,阅读了《JavaScript设计模式与开发实践》这本书后,个人感觉js就是围绕对象来设计的,发现日常写代码能用上的并不多,下面是常用的几种设计模式。
单一模式的核心是确保只有一个实例,并提供全局访问,在 JS 开发中,经常把用一个对象包裹,这样减少了全局变量的污染,比如 var a = {}。
普通写法:
// 每次点击点击按钮,都会创建一个 div
var createLayer1 = (function () {
var div = document.createElement('div');
div.innerHTML = '我是内容';
div.style.display = 'none';
document.body.appendChild(div);
return div;
})()
document.getElementById('#btn').onclick = function () {
var layer1 = createLayer1();
layer1.style.display = 'block';
}
单例模式:
//实例对象总是在我们调用方法时才被创建,而不是在页面加载好的时候就创建。
// 这样就不会每次点击按钮,都会创建一个 div 了
var createLayer2 = function () {
var div;
return function () {
if (!div) {
document.createElement('div');
div.innerHTML = '我是内容';
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
}
document.getElementById('#btn').onclick = function () {
var layer2 = createLayer2();
layer2.style.display = 'block';
}
策略模式代码非常优雅,最喜欢模式之一,也很便于修改,请看代码。
普通模式:
var awardS = function (salary) {
return salary * 4
};
var awardA = function (salary) {
return salary * 3
};
var awardB = function (salary) {
return salary * 2
};
var calculateBonus = function (level, salary) {
if (level === 'S') {
return awardS(salary);
}
if (level === 'A') {
return awardA(salary);
}
if (level === 'B') {
return awardB(salary);
}
};
calculateBonus('A', 10000);
策略模式:
var strategies = {
'S': function (salary) {
return salary * 4;
},
'A': function (salary) {
return salary * 3;
},
'B': function (salary) {
return salary * 2;
}
}
var calculateBonus = function (level, salary) {
return strategies[level](salary);
}
calculateBonus('A', 10000);
模板方法模式使用了原型链的方法,封装性好,复用性差。
var Coffee = function () {
};
Coffee.prototype.boilWater = function () {
// todo
console.log('把水煮沸');
};
Coffee.prototype.brewCoffee = function () {
// todo
console.log('冲咖啡');
};
Coffee.prototype.pourInCup = function () {
// todo
console.log('把咖啡倒进杯子');
};
Coffee.prototype.addSugarAndMilk = function () {
// todo
console.log('加糖和牛奶');
};
Coffee.prototype.init = function () {
this.boilWater();
this.brewCoffee();
this.pourInCup();
this.addSugarAndMilk();
}
var coffee = new Coffee();
coffee.init();
没错,我刚开始写第一个项目时候就这么嵌套的,重复代码太多,逻辑太乱,�维护下太差。
var order = function (orderType, pay, stock) {
// 500 元定金模式
if (orderType === 1) {
if (pay === true) {
console.log('500元定金预购,得到100元优惠券');
} else {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
// 200 元定金模式
} else if (orderType === 2) {
if (pay === true) {
console.log('200元定金预购,得到50元优惠券');
} else {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
// 没有定金模式
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
}
order(1, true, 500);
职责链,一系列可能处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象,减少了很多重复代码。
var order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,得到100元优惠券');
} else {
order200(orderType, pay, stock);
}
}
var order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,得到50元优惠券');
} else {
orderNormal(orderType, pay, stock);
}
}
var orderNormal = function (orderType, pay, stock) {
if (stock > 0) {
console.log('普通购买,无优惠券');
} else {
console.log('手机库存不足');
}
}
order500(1, true, 500);
order500(1, false, 500);
order500(2, true, 500);
观察者对象有三个方法:订阅消息方法、取消订阅消息方法、发送订阅消息方法
var Observer = (function() {
var messages = {}
return {
// 注册信息接口
regist: function (type, fn){
// 如果消息不存在,则新建一个消息类型
if (typeof messages[type] === 'undefined') {
messages[type] = [fn]
} else {
// 保证多个模块注册同一消息,都能够顺利执行
messages[type].push(fn)
}
},
// 发布信息接口
fire: function (type, args){
// 消息没有注册,则返回
if (!messages[type]) {
return;
}
// 定义消息信息:消息类型和消息数据
var events = {
type: type,
args: args || {}
}
var i, len = messages[type].length;
for (i = 0;i < len; i++) {
messages[type][i].call(this, events)
}
},
// 移除信息接口
remove: function (type, fn){
// 有注册过的消息
if (messages[type] instanceof Array) {
var i = messages[type].length - 1;
for (;i >= 0; i--) {
console.log('xxxx', messages[type][i] === fn)
messages[type][i] === fn && messages[type].splice(i, 1)
}
}
console.log('messages....', messages)
}
}
})()
var fn = function(e) {
console.log(e)
}
Observer.regist('test', fn)
Observer.fire('test', {msg: '测试数据'})
Observer.remove('test', fn)
上一篇文章里,action 都是同步的,也就是说 dispatch(action),经过中间件,更新得到 state 是同步的。
下面介绍几种 action 是异步执行的。
redux-thunk
redux-chunk 是 redux 的一个中间件 middleware,核心是创建一个异步的 action。
具体是怎么实现的呢?首先了解一个概念,什么是 thunk?
thunk
thunk 是一个函数,函数内有一个表达式,目的是为了延时执行。
let x = 1 + 2;
let foo = () => 1 + 2;
只有 foo()
执行时,才返回 3,那么 foo 就是 chunk 函数。
redux-thunk
源码很少,但又是经典,源码如下:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
export default thunk;
如源码所示,redux-thunk 是个中间件,和其他中间件区别就是,判断 action 类型是一个函数时候,执行这个 action。而同步 action 执行返回是一个对象。
例子:
// dispatch(action) 时,经过中间件时,执行 next(action),更新得到 state 是同步的。
const loading = text => ({
type: 'LOADING',
text
})
const cancelLoading = text => ({
type: 'CANCEL_LOADING',
text
})
// dispatch(action) 时,经过中间件时,执行 action(...args),更新得到 state 是异步的。
const fetchPeople = (url, params) => {
return ({dispatch, getState}) => {
dispatch(loading('start loading'));
fetch(url, params).then(delayPromise(3000)).then(data => {
dispatch(cancelLoading('cancel loading'));
});
};
};
redux-promise
redux-promise 也是中间件 middleware,原理和 redux-thunk 差不多,主要区别是 redux-promise 用到了 promise 异步。
源码如下:
import isPromise from 'is-promise';
import { isFSA } from 'flux-standard-action';
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload)
? action.payload
.then(result => dispatch({ ...action, payload: result }))
.catch(error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}
redux-promise 兼容了 FSA 标准(结果信息放在 payload 里),实现过程就是判断 action 或 action.payload 是否为 promise 对象,是则执行 then().catch(),否则 next(action)。
同样一个功能,可以用很多方法来实现,有时候由于项目时间紧张,导致很多时候只是实现了功能,往往忽视了代码质量。下面几种代码重构方法,以便大家可以写出更漂亮的代码。本文内容是我的读书笔记,摘自《Javascript设计模式与开发》。
var getUserInfo = function(){
ajax( 'http:// xxx.com/userInfo', function( data ){
console.log( 'userId: ' + data.userId );
console.log( 'userName: ' + data.userName );
console.log( 'nickName: ' + data.nickName );
})
})
// 重构后
var getUserInfo = function(){
ajax( 'http:// xxx.com/userInfo', function( data ){
printDetails( data ); });
};
var printDetails = function( data ){
console.log( 'userId: ' + data.userId );
console.log( 'userName: ' + data.userName );
console.log( 'nickName: ' + data.nickName );
};
var paging = function( currPage ){
if ( currPage <= 0 ){
currPage = 0;
jump( currPage );
}else if ( currPage >= totalPage ){
currPage = totalPage;
jump( currPage );
}else{
jump( currPage );
}
}
// 重构后,把重复函数独立出来
var paging = function( currPage ){
if ( currPage <= 0 ){
currPage = 0;
}else if ( currPage >= totalPage ){
currPage = totalPage;
}
jump( currPage );
};
var getPrice = function( price ){
var date = new Date();
if ( date.getMonth() >= 6 && date.getMonth() <= 9 ){
return price * 0.8;
}
return price;
};
// 重构后,改成能够理解的函数
var isSummer = function(){
var date = new Date();
return date.getMonth() >= 6 && date.getMonth() <= 9;
};
var getPrice = function( price ){
if ( isSummer() ){
return price * 0.8;
}
return price;
};
var createXHR = function(){
var xhr;
try{
xhr = new ActiveXObject( 'MSXML2.XMLHttp.6.0' );
}catch(e){
try{
xhr = new ActiveXObject( 'MSXML2.XMLHttp.3.0' );
}catch(e){
xhr = new ActiveXObject( 'MSXML2.XMLHttp' );
}
}
return xhr;
};
var xhr = createXHR();
// 重构后,使用遍历更简洁
var createXHR = function(){
var versions= [ 'MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp' ];
for ( var i = 0, version; version = versions[ i++ ]; ){
try{
return new ActiveXObject( version );
}catch(e){
// ...
}
}
};
var xhr = createXHR();
var del = function( obj ){
var ret;
if ( !obj.isReadOnly ){
if ( obj.isFolder ){
ret = deleteFolder( obj );
}else if ( obj.isFile ){
ret = deleteFile( obj );
}
}
return ret;
}
// 函数只有一个出口,改成多个出口
var del = function( obj ){
if ( obj.isReadOnly ){
return; //反转 if 表达式
}
if ( obj.isFolder ){
return deleteFolder( obj );
}
if ( obj.isFile ){
return deleteFile( obj );
}
};
var setUserInfo = function( id, name, address, sex, mobile, qq ){
console.log( 'id= ' + id );
console.log( 'name= ' +name );
console.log( 'address= ' + address );
console.log( 'sex= ' + sex ); console.log( 'mobile= ' + mobile ); console.log( 'qq= ' + qq );
};
setUserInfo( 1314, 'sven', 'shenzhen', 'male', '137********', 377876679 );
// 把参数用一个对象包裹,不用再关系参数的数量和顺序
var setUserInfo = function( obj ){
console.log( 'id= ' + obj.id );
console.log( 'name= ' + obj.name );
console.log( 'address= ' + obj.address );
console.log( 'sex= ' + obj.sex );
console.log( 'mobile= ' + obj.mobile );
console.log( 'qq= ' + obj.qq );
};
setUserInfo({
id: 1314,
name: 'sven',
address: 'shenzhen',
sex: 'male',
mobile: '137********',
qq: 377876679
});
var Spirit = function( name ){
this.name = name;
};
Spirit.prototype.attack = function( type ){
if ( type === 'waveBoxing' ){
console.log( this.name + ': 使用波动拳' );
}else if( type === 'whirlKick' ){
console.log( this.name + ': 使用旋风腿')
}
};
var spirit = new Spirit( 'RYU' );
spirit.attack( 'waveBoxing' );
spirit.attack( 'whirlKick' );
// Spirit.prototype.attack 如果要添加方法,那会变得很大,所以必要作为一个单独的类存在
var Attack = function( spirit ){
this.spirit = spirit;
};
Attack.prototype.start = function( type ){
return this.list[ type ].call( this );
};
Attack.prototype.list = {
waveBoxing: function(){
console.log( this.spirit.name + ': 使用波动拳' );
},
whirlKick: function(){
console.log( this.spirit.name + ': 使用旋风腿' );
}
};
由于 CSS3 的推出,让有些动画不在以 JS 来实现,仅仅依靠 CSS 就可以实现许多动画效果。提高了性能同时,又增加了很多趣味性。
接下来我会持续更新大家常用到的CSS效果,供大家学习。。。
// html
<div class="ball"></div>
//css
@keyframes bounce {
60%, 80%, to {
transform: translateY(400px);
animation-timing-function: ease;
}
70% { transform: translateY(300px); }
90% { transform: translateY(360px); }
}
.ball {
width: 50px;
height: 50px;
border-radius: 50%;
margin: auto;
background: rgba(0,100,100,0.5);
animation: bounce 2s cubic-bezier(.58,.13,.94,.64) forwards;
}
技术分析:主要技术点是利用贝塞尔曲线 和 ease
来控制动画速度,tansition-timing-function
还有 linear
属性。
// html
<div id="frame"></div>
// css
#frame {
width: 50px;
height: 72px;
border: 1px solid transparent;
background: url(https://s.cdpn.io/79/sprite-steps.png) no-repeat left top;
animation: frame-animation 1s steps(10) infinite;
}
@keyframes frame-animation {
0% { background-position: 0px 0; }
100% { background-position: -500px 0; }
}
技术分析:主要技术点是 steps(10)
,实现原理是,图片分为 10 部分,总共需要 10 步来完成,其中动画不是滑动实现的,而是每一步只显示一个区域。注意:steps(number)
中的 number*(每小张图片的长度)= 图片总长度相对应,才能实现 gif 效果。
// html
<div class="pic"></div>
// css
@keyframes panoramic {
to { background-position: 100% 0; }
}
.pic {
width: 150px; height: 150px;
background: url('http://c3.staticflickr.com/3/2671/3904743709_74bc76d5ac_b.jpg');
background-size: auto 100%;
animation: panoramic 10s linear infinite alternate;
animation-play-state: paused;
}
.pic:hover, .pic:focus {
animation-play-state: running;
}
技术分析: 主要技术点是 animation-play-state: paused
暂停动画。
// html
<h1 class="pic">CSS is awesome!</div>
// css
@keyframes typing {
from { width: 0 }
}
/* 光标 */
@keyframes caret {
50% { border-right-color: transparent; }
}
h1 {
font: bold 200% Consolas, Monaco, monospace;
width: 15ch;
white-space: nowrap;
overflow: hidden;
border-right: .05em solid;
animation: typing 8s steps(15),
caret 1s steps(1) infinite;
}
技术分析:CH
宽度单位是每个字体长度,还是利用了 steps() 一次只显示一个特性,而不是滑动,可以把 step() 换成其他属性如:linear,ease等
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.