npm i -g gh-pages
安装gh-pages
,本地或者全局均可,根据自己选择,我这里安装到了全局在本地项目的package.json
> scripts
添加命令predeploy
和deploy
,下面代码中的最后两个
1 | "scripts": { |
运行npm run deploy
https://你的github名字.github.com/你的仓库名字
就可以访问你idea应用了。安装深度学习的环境还是很复杂的,特别是 linux 下安装,新手更是闹不明白,笔者在安装的过程中因为执行错了命令,导致系统重做,各种血泪,本文是在重新安装的系统上实践出来的结果,仅供参考
Anaconda3-5.3.1-Linux-x86_64
。运行sudo bash Anaconda3-5.3.1-Linux-x86_64
进行安装,一路默认可以。我修改了默认的安装路径/root/anaconda3
到/opt/anaconda3
最后一步,询问是否将 anaconda3 添加到 .bashrc
, 选择 no
。
配置环境变量,sudo vi /etc/profile
编辑 profile。
在文件最后加入 export PATH=$PATH:/opt/anaconda3/bin
, 注意这里将 $PATH 放在了前面,主要是为了保证系统中默认的python版本不被覆盖,可以保证系统的正常运行不受影响
保存 profile , 运行 source /etc/profile
使配置生效。
运行conda --version
, 输出conda 4.5.11
表示安装成功。
conda create --name ai python=3.6
source activate ai
,这时可以在终端命令行的开头看到多了(ai)
,表示当前正在一个叫ai的虚拟环境里运行。在 anaconda 创建的 ai 虚拟环境中安装 tensorflow-gpu 的 1.11.0。1
2
3 pip install tensorflow-gpu=1.11.0
Collecting tensorflow-gpu==1.12.0
Downloading https://files.pythonhosted.org/packages/55/7e/bec4d62e9dc95e828922c6cec38acd9461af8abe749f7c9def25ec4b2fdb/tensorflow_gpu-1.11.0-cp36-cp36m-manylinux1_x86_64.whl (281.7MB)...
如果 tensorflow-gpu 的下载速度太慢,可以将下载链接复制到迅雷里下载,迅速会快很多。下载完成之后可以通过pip install /path/下载文件
来进行安装。
安装完成之后进行测试,新建一个test.py
,并写入如下内容。1
2
3
4
5
6
7import tensorflow as tf
hello = tf.constant('Hello, TensorFlow!')
sess = tf.Session()
print(sess.run(hello))
a = tf.constant(10)
b = tf.constant(32)
print(sess.run(a+b))
然后python test.py
运行,注意一下当前运行在 anaconda 的 ai 虚拟环境中。
在 anaconda 创建的 ai 虚拟环境中,运行 pip install opencv-python
安装 opencv2。
安装完成之后进行测试,新建一个testCV2.py
,并写入如下内容。1
2
3
4
5
6
7
8
9
10#导入cv模块
import cv2 as cv
#读取图像,支持 bmp、jpg、png、tiff 等常用格式
img = cv.imread("test.jpg")
#创建窗口并显示图像
cv.namedWindow("Image")
cv.imshow("Image",img)
cv.waitKey(0)
#释放窗口
cv.destroyAllWindows()
然后python testCV2.py
运行,注意一下当前运行在 anaconda 的 ai 虚拟环境中。如果弹出窗口,窗口出现指定的路径的图片,就执行成功了。
打开pytorch官网,在首页选择 pytorch 的运行环境,获取对应的执行命令。
在 anaconda 创建的 ai 虚拟环境中,运行上图中的命令安装 pytorch。
安装完成之后进行测试,新建一个testPytorch.py
,并写入如下内容。1
2
3
4
5
6
7
8
9
10#导入cv模块
import cv2 as cv
#读取图像,支持 bmp、jpg、png、tiff 等常用格式
img = cv.imread("test.jpg")
#创建窗口并显示图像
cv.namedWindow("Image")
cv.imshow("Image",img)
cv.waitKey(0)
#释放窗口
cv.destroyAllWindows()
然后python testPytorch.py
运行,注意一下当前运行在 anaconda 的 ai 虚拟环境中。如果输出如下内容,说明环境搭建成功。
1 | tensor([1.], device='cuda:0') |
环境搭建是学习的第一步,整个环境花了一天的时间才算完成,因为linux 还在学习中,python还是第一次接触,整个操作的过程中真的是剪不断理还乱,好在最后总算完成了。
]]>安装深度学习的环境还是很复杂的,特别是 linux 下安装,新手更是闹不明白,笔者在安装的过程中因为执行错了命令,导致系统重做,各种血泪,本文是在重新安装的系统上实践出来的结果,仅供参考
安装驱动
1 | sudo apt install nvidia-smi nvidia-driver |
调整显卡管理方案
使用系统自带的 显卡驱动管理器
将显卡方案设置为 NV-PRIME
。
重新启动系统,应该能够看到旋转的茶壶画面
验证安装成功与否,打开终端输入命令nvidia-smi
,如果出现类似下图,就说明安装成功了。
驱动安装完成后,就可以安装 cuda 了,不过在此之前还需要检查下gcc
和g++
的版本是否在 4.9~6.0之间,deepin自带的是7.+,需要降级。1
2
3
4
5sudo apt install g++-6 gcc-6
cd /usr/bin
sudo rm gcc g++
sudo ln -s g++-6 g++
sudo ln -s gcc-6 gcc
可以正式开始安装,cuda建议使用官方的安装的包,不要使用默认安装源内的版本,容易出现不兼容的问题。
下载 cuda 9.0,这里选择了如下图。
使用sudo sh cuda_9.0.176_384.81_linux.run
安装,安装过程中跳过 nvidia 驱动的安装,因为在上面我们已经安装过了,其他的依次正常安装即可。
将cuda添加到环境变量,在~/.bashrc文件中末尾加上
1 | # cuda |
1 | source ~/.bashrc |
查看版本
1 | $ nvcc --version |
运行 cuda samples 测试
cd ~/NVIDIA_CUDA-9.0_Samples/1_Utilities/deviceQuery
编译 make
1 | "/usr/local/cuda-9.0"/bin/nvcc -ccbin g++ -m64 -gencode arch=compute_30,code=sm_30 -gencode arch=compute_35,code=sm_35 -gencode arch=compute_37,code=sm_37 -gencode arch=compute_50,code=sm_50 -gencode arch=compute_52,code=sm_52 -gencode arch=compute_60,code=sm_60 -gencode arch=compute_70,code=sm_70 -gencode arch=compute_70,code=compute_70 -o deviceQuery deviceQuery.o |
运行编译结果./deviceQuery
1 | ./deviceQuery Starting... |
这样 cuda 的安装就大功告成了。
打开cudnn 下载地址 下载 cuda-9.0 对应版本的 cudnn,需要注意的是 cudnn 需要注册登录才能下载。
这里下载的是 cuDNN 7.5 for CUDA 9.0 对应的linux版。
下载完成后 tar -zxvf cudnn-9.1-linux-x64-v7.1.tgz
, 将解压出来的cuda文件夹复制到/usr/local
。
关键的步骤来了1
2
3$ cd /etc/ld.so.conf.d
$ touch cuda.conf
$ vi cuda.conf
在conda.conf中输入如下内容1
2/usr/local/cuda/lib64
/usr/local/cuda-9.0/lib64
之所以说是关键步骤,因为经过无数次的测试验证,这样的配置可以解决 tensorflow
运行时出现的ImportError:libcublas.so.9.0: cannot open shared object file: No such file or directory
和 ImportError:libcudnn.so.7: cannot open shared object file: No such file or directory
不断学习思考是不断进步的保证
]]>git地址在这里,今天用的是 deepin 所以直接使用源安装。
1 | apt-get install git |
全局配置 name 和 email1
2 git config --global user.name "xxx"
git config --global user.email "eamil@qq.com"
创建~/xingmu/.ssh
目录,并进入。1
2
3 cd ~
mkdir .ssh
cd .ssh
在~/xingmu/.ssh
目录下,用 ssh-keygen 命令生成一组新的 id_rsa_new 和 id_rsa_new.pub,我这里需要使用 github 和 gitee(码云)两个平台,所以需要执行两次命令,分别生成 id_rsa_github/id_rs_github.pub 和 id_rsa_gitee/id_rsa_gitee.pub两组。
1 | ssh-keygen -t rsa -C "xxx@xxx.com" |
需要注意的是,平时都是默认生成 id_rsa 和 id_rsa.pub 。现在要在第一个提示输入出现时分别输入带有表示意义的名字,以便于识别,这里我输入的是 id_rsa_github 和 id_rsa_gitee。
将公钥分别配置到对应的 git 平台上,然后在~/xingmu/.ssh
目录下新建 config 文件,配置参考如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21## github
# 域名地址的别名
Host github
# 这个是真实的域名地址
Hostname github.com
# 配置使用用户名
User xxx@xx.com
# 这里是id_rsa的目录位置
IdentityFile ~/.ssh/id_rsa_github
## 码云
# 域名地址的别名
Host gitee
# 这个是真实的域名地址
Hostname gitee.com
# 配置使用用户名
User xxx@xx.com
# 这里是id_rsa的目录位置
IdentityFile ~/.ssh/id_rsa_gitee
## 以下第三个或者更多
1 | ssh -T git@github |
如果出现如下的提示,选择 yes 继续就可以了
然后就可以愉快的玩耍了!!
]]>解压
1 | $ mv /opt |
设置环境变量
1 | $ sudo vi /etc/profile |
在最后加入如下内容1
2
3
4
5#set java environment
export JAVA_HOME=/opt/java/jdk1.8
export JRE_HOME=/opt/java/jdk1.8/jre
export CLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
让修改生效1
$ source /etc/profile
1 | $ java -version |
dde-file-manager
的命令可以在命令行打开制定路径的文件夹。1 | # 打开当前位置的文件夹 |
linux 图形化桌面 GNOME 包括了一个叫做 Nautilus 的文件管理器,可以通过安装 nautilus 来使用 nautilus 命令,在命令窗口中直接打开指定的文件夹。1
2
3
4
5
6# 安装nautilus
sudo apt-get install nautilus
# 打开当前位置的文件夹
nautilus .
# 打开指定位置的文件夹
nautilus /usr/local
这个乱码跟编码格式无关,根本原因是 IDE 的界面主题使用的字体,显示中文有 bug ,所以只有设置下中文字体就可以解决了,我这里设置的 微软雅黑。
另外说一下就是,在 deepin linux 中如果无法找到中文字体,可以去网上下载,通过系统自带的字体安装器安装到系统,然后重启 IDE 就可以选择了。
上图的绿色方块不断滚动,顶部会提示它的可见性。
传统的实现方法是,监听到scroll事件后,调用目标元素(绿色方块)的getBoundingClientRect()
方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,不断调用getBoundingClientRect()来计算元素相对位置,会造成频繁的回流和重绘,而且Element.getBoundingClientRect()运行在主线程上,容易造成性能问题。
目前有一个新的 IntersectionObserver API,可以自动”观察”元素是否可见,Chrome 51+
已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。
Intersection Observer API 会注册一个回调方法,每当期望被监视的元素进入或者退出另外一个元素的时候(或者浏览器的视口)该回调方法将会被执行,或者两个元素的交集部分大小发生变化的时候回调方法也会被执行。通过这种方式,网站将不需要为了监听两个元素的交集变化而在主线程里面做任何操作,并且浏览器可以帮助我们优化和管理两个元素的交集变化。
1 | var io = new IntersectionObserver(callback, option); |
上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数,callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点。
1 | // 开始观察 |
上面代码中,observe 的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
1 | io.observe(elementA); |
目标元素的可见性变化时,就会调用观察器的回调函数 callback。
callback 一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
1 | var io = new IntersectionObserver( |
上面代码中,回调函数采用的是箭头函数的写法。callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries 数组就会有两个成员。
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
每个属性的含义如下。
上图中,灰色的水平方框代表视口,深红色的区域代表四个被观察的目标元素。它们各自的intersectionRatio
图中都已经注明。
我写了一个 Demo,演示IntersectionObserverEntry
对象。注意,这个 Demo 只能在 Chrome 51+ 运行。
有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做”惰性加载”。
有了 IntersectionObserver API,实现起来就很容易了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function query(selector) {
return Array.from(document.querySelectorAll(selector));
}
var observer = new IntersectionObserver(
function(changes) {
changes.forEach(function(change) {
var container = change.target;
var content = container.querySelector('template').content;
container.appendChild(content);
observer.unobserve(container);
});
}
);
query('.lazy-loaded').forEach(function (item) {
observer.observe(item);
});
上面代码中,只有目标区域可见时,才会将模板内容插入真实 DOM,从而引发静态资源的加载。
无限滚动(infinite scroll)的实现也很简单。1
2
3
4
5
6
7
8
9
10
11
12var intersectionObserver = new IntersectionObserver(
function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
});
// 开始观察
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);
无限滚动时,最好在页面底部有一个页尾栏(又称sentinels)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。这样做的好处是,不需要再一次调用observe()方法,现有的IntersectionObserver可以保持使用。
IntersectionObserver
构造函数的第二个参数是一个配置对象。它可以设置以下属性。
threshold属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。1
2
3
4
5
6
7new IntersectionObserver(
entries => {
/* ... */
}, {
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);
用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]
就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在iframe
窗口里滚动)。容器内滚动也会影响目标元素的可见性,参见本文开始时的那张示意图。
IntersectionObserver API 支持容器内滚动。root
属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。1
2
3
4
5
6
7
8
9var opts = {
root: document.querySelector('.container'),
rootMargin: "500px 0px"
};
var observer = new IntersectionObserver(
callback,
opts
);
上面代码中,除了 root 属性,还有rootMargin
属性。后者定义根元素的 margin,用来扩展或缩小 rootBounds 这个矩形的大小,从而影响 intersectionRect 交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px
,表示 top、right、bottom 和 left 四个方向的值。
这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。
IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。
规格写明,IntersectionObserver
的实现,应该采用requestIdleCallback()
,即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
WICG 提供了一个 Polyfill。
1 | class Index extends Component { |
这里使用 context 的主要是为了将 store 放入 context 里面,方便子组件 connect 的时候可以拿得到 store。我们可以将这块的内容抽出来单独做个组件,并将需要使用 store 的组件作为这个组件的子组件。
这个组件命名为 Provider (提供者)。新增一个 src/Provider.js,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29import React from "react";
import PropTypes from "prop-types";
class Provider extends React.Component {
static childContextTypes = {
store: PropTypes.object.isRequired
};
static propTypes = {
store: PropTypes.object
};
getChildContext() {
return {
store: this.props.store
};
}
render() {
return (
<React.Fragment>
{this.props.children}
</React.Fragment>
);
}
}
export default Provider;
Provider 做的事情也很简单,它就是一个容器组件,会把嵌套的内容原封不动作为自己的子组件渲染出来。它还会把外界传给它的 props.store 放到 context,这样子组件 connect 的时候都可以获取到。
然后重构 src/index.js:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48import React, { Component } from "react";
import ReactDOM from "react-dom";
import Header from "./Header";
import Content from "./Content";
import "./index.css";
import Provider from "./Provider";
function createStore(reducer) {
let events = [];
let state = null;
let store = {
subscribe: (event) => events.push(event),
dispatch: (action) => {
state = reducer(state, action);
events.forEach((event) => event());
},
getState: () => state
};
// 初始化state
store.dispatch({});
return store;
}
function themeReducer(state, action) {
if (!state) {
return {
themeColor: "red"
};
}
switch (action.type) {
case "CHANGE_THEME_COLOR":
return { ...state, themeColor: action.themeColor };
default:
return state;
}
}
const store = createStore(themeReducer);
ReactDOM.render(
<Provider store={store}>
<Header />
<Content />
</Provider>,
document.getElementById("root")
);
这样我们就把所有关于 context 的代码从组件里面删除了。
现在通过这种方式大家不仅仅知道了 React-redux 的基础概念和用法,而且还知道这些概念到底是解决什么问题,为什么 React-redux 这么奇怪,为什么要 connect,为什么要 mapStateToProps 和 mapDispatchToProps,什么是 Provider,我们通过解决一个个问题就知道它们到底为什么要这么设计的了。
仿照给 connect 传入 mapStateToProps 函数来达到获取指定数据的效果,给 connect 再提供一个 mapDispatchToProps 函数来告诉 connect 组件需要如何调用 dispatch。这个函数应该是这样的:1
2
3
4
5
6
7const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: "CHANGE_THEME_COLOR", themeColor: color });
}
};
};
mapDispatchToProps 也是返回一个对象,和 mapStateToProps 不同的地方是传入的参数不是 state ,而是 dispatch。下面来修改 connect ,让他可以处理 mapDispatchToProps。
1 | import React from "react"; |
这时候我们就可以重构 ThemeSwitch,让它摆脱 store.dispatch 和 context。
1 | import React, { Component } from "react"; |
这时候这三个组件的重构都已经完成了,代码大大减少、不依赖 context,并且功能和原来一样。
对于第一个大量重复逻辑的问题,可以通过高阶组件抽取重复的逻辑来解决。高阶组件就是一个函数,传给它一个组件,它返回一个新的组件,它的作用就是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。新的组件和传入的组件通过 props 传递信息
对于第二个问题,首先需要知道可复用组件需要具有什么样的特征,在 React 中,如果一个组件的渲染只依赖于外界传进去的 props 和自己的 state,而并不依赖于其他的任何外界数据,也就是说像纯函数一样,给它什么,它就吐出(渲染)什么出来。这种组件的复用性是最强的,别人使用的时候根本不用担心任何事情,只要看看 PropTypes 它能接受什么参数,然后把参数传进去控制它就行了。
有了思路,下面就来修改代码,首先需要一个高阶组件来协助从 context 中获取数据,然后用一个傻瓜组件来帮助提交组件的复用性。
将这个高阶组件命名为 connect,他的作用是将 context 和 可复用组件连接起来。
1 | import React, { Component } from "react"; |
connect 函数接受一个组件 WrappedComponent 作为参数,把这个组件包含在一个新的组件 Connect 里面,Connect 会去 context 里面取出 store。现在要把 store 里面的数据取出来通过 props 传给 WrappedComponent。
但是每个传进去的组件需要 store 里面的数据都不一样的,所以除了给高阶组件传入 Dumb 组件以外,还需要告诉高级组件我们需要什么数据,高阶组件才能正确地去取数据。为了解决这个问题,我们可以给高阶组件传入类似下面这样的函数:1
2
3
4
5
6
7
8const mapStateToProps = (state,props) => {
return {
themeColor: state.themeColor,
themeName: state.themeName,
fullName: `${state.firstName} ${state.lastName}`
...
};
};
这个函数会接受 store.getState() 的结果和给 WrappedComponent 传递的 props 作为参数,然后返回一个对象。mapStateTopProps 相当于告知了 Connect 应该如何去 store 里面取数据,然后可以把这个函数的返回结果传给被包装的组件。
connect 现在是接受一个参数 mapStateToProps,然后返回一个函数,这个返回的函数才是高阶组件。它会接受一个组件作为参数,然后用 Connect 把组件包装以后再返回。 connect 的用法是:1
2
3
4
5
6const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
};
};
Header = connect(mapStateToProps)(Header);
现在根据上面的描述,给 connect 加上 mapStateToProps 和 数据变化的监听,connect 完整的代码应该是:
1 | import React from "react"; |
组件 Connect 的 state.allProps,它是一个对象,用来保存需要传给被包装组件的所有的参数。生命周期 componentWillMount 会调用调用 updateProps 进行初始化,然后通过 store.subscribe 监听数据变化重新调用 updateProps。
为了让 connect 返回新组件和被包装的组件使用参数保持一致,我们会把所有传给 Connect 的 props 原封不动地传给 WrappedComponent。所以在 updateProps 里面会把 stateProps 和 this.props 合并到 this.state.allProps 里面,再通过 render 方法把所有参数都传给 WrappedComponent。
现在使用 connect 修改 Header.js、Content.js。
src/Header.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import React, { Component } from "react";
import PropTypes from "prop-types";
import connect from "./connect";
class Header extends Component {
static propTypes = {
color: PropTypes.string
};
render() {
return (
<h1 style={{ color: this.props.themeColor }}>React-Redux是什么</h1>
);
}
}
function mapStateToProps(state, props) {
return {
themeColor: state.themeColor
};
}
export default connect(mapStateToProps)(Header);
src/Content.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import React, { Component } from "react";
import PropTypes from "prop-types";
import connect from "./connect";
import ThemeSwitch from "./ThemeSwitch";
class Content extends Component {
static propTypes = {
themeColor: PropTypes.string
};
render() {
return (
<div style={{ color: this.props.themeColor }}>
<p>React-Redux是Redux的官方React绑定库。它能够使你的React组件从Redux store中读取数据,并且向store分发actions以更新数据</p>
<ThemeSwitch />
</div>
);
}
}
function mapStateToProps(state) {
return {
themeColor: state.themeColor
};
}
export default connect(mapStateToProps)(Content);
现在通过 connect 抽取了使用 context 产生的重复逻辑,并提高了 Header 和 Context 组件的复用性,后面继续重构 ThemeSwitch 组件。
修改 src/index.js:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66import React, { Component } from "react";
import PropTypes from "prop-types";
import ReactDOM from "react-dom";
import Header from "./Header";
import Content from "./Content";
import "./index.css";
function createStore(reducer) {
let events = [];
let state = null;
let store = {
subscribe: (event) => events.push(event),
dispatch: (action) => {
state = reducer(state, action);
events.forEach((event) => event());
},
getState: () => state
};
// 初始化state
store.dispatch({});
return store;
}
function themeReducer(state, action) {
if (!state) {
return {
themeColor: "red"
};
}
switch (action.type) {
case "CHANGE_THEME_COLOR":
return { ...state, themeColor: action.themeColor };
default:
return state;
}
}
const store = createStore(themeReducer);
class Index extends Component {
static childContextTypes = {
store: PropTypes.object
};
getChildContext() {
return {
store
};
}
render() {
return (
<div>
<Header />
<Content />
</div>
);
}
}
ReactDOM.render(
<Index />,
document.getElementById("root")
);
然后修改 src/Header.js 和 src/Context.js,让它从 Index 的 context 里面获取 store,并且获取里面的 themeColor 状态来设置自己的颜色。
src/Header.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41import React, { Component } from "react";
import PropTypes from "prop-types";
class Header extends Component {
static contextTypes = {
store: PropTypes.object
};
state = {
themeColor: ""
};
componentWillMount() {
const { store } = this.context;
// 挂载的时候,进行第一次渲染
this.updateThemeColor();
// 然后订阅 store 的后续变化,并更新
store.subscribe(() => this.updateThemeColor());
}
updateThemeColor() {
let state = this.context.store.getState();
this.setState({
themeColor: state.themeColor
});
}
// static getDerivedStateFromProps(nextProps, prevState) {
//
// }
render() {
return (
<h1 style={{ color: this.state.themeColor }}>React-Redux是什么</h1>
);
}
}
export default Header;
src/Context.js
1 | import React, { Component } from "react"; |
修改 src/ThemeSwitch.js,添加颜色更新及两个按钮的点击事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57import React, { Component } from "react";
import PropTypes from "prop-types";
class ThemeSwitch extends Component {
static contextTypes = {
store: PropTypes.object
};
state = {
themeColor: ""
};
componentWillMount() {
let { store } = this.context;
this.updateThemeColor();
store.subscribe(() => {
this.updateThemeColor();
});
}
updateThemeColor() {
const { store } = this.context;
const state = store.getState();
this.setState({ themeColor: state.themeColor });
}
// 更新store中的颜色
handleSwitchColor(color) {
let { store } = this.context;
store.dispatch({
type: "CHANGE_THEME_COLOR",
themeColor: color
});
}
render() {
return (
<div>
<button
style={{ color: this.state.themeColor }}
onClick={this.handleSwitchColor.bind(this, "red")}
>
设置主题颜色red
</button>
<button
style={{ color: this.state.themeColor }}
onClick={this.handleSwitchColor.bind(this, "green")}
>
设置主题颜色green
</button>
</div>
);
}
}
export default ThemeSwitch;
到此 store 和 context 已经结合起来,看起来功能完成的不错,就是代码稍微啰嗦,后面继续优化。
在 React 中应用的状态存在可能被多个组件依赖或者影响,而 React 并没有提供很好的解决方案,我们只能把状态提升到依赖或者影响这个状态的所有组件的公共父组件上,我们把这种行为叫做状态提升。但是需求不停变化,导致状态一直在不断的提升,然后一层层的传递,这显然不是我们想要的。
后来 React 提供了 context 的概念,将共享状态放到父组件的 context 上,这个父组件下所有的子组件 都可以从 context 中直接获取状态,而不要一层层传递了。但是直接从 context 里面存放、获取数据增强了组件的耦合性;并且所有组件都可以修改 context 里面的状态就像谁都可以修改共享状态一样,导致让程序不可预测。
而 redux 中的 store 的数据不是谁都能修改,而是约定只能通过 dispatch 来进行修改,这样的话将 redux 和 context 结合起来,每个组件既可以去 context 里面获取 store 从而获取状态,又不用担心它们乱改数据了。下面我们来试一下。
使用 create-react-app 新建一个工程,然后安装 prop-types , 删除 src 下面除 index.js
和index.css
之外的文件,然后在 src 下面新建三个文件 Header.js、Content.js、ThemeSwitch.js。
src/Header.js:
1 | import React, { Component } from "react"; |
src/ThemeSwitch.js:
1 | import React, { Component } from "react"; |
src/Content.js:
1 | import React, { Component } from "react"; |
修改 src/index.js:
1 | import React, { Component } from "react"; |
然后 npm start
启动项目,打开页面就可以看到效果。
目前只是完成项目搭建,状态和逻辑都还没添加,后面继续。
注意: 主线程中的代码执行也是宏任务,在主线程代码执行结束后,会先去检查也没有微任务,如果有先执行微任务,然后再去查看事件队列中还有没有需要执行的宏任务。
宏任务的优先级: 主代码块 > setImmediate > MessageChannel > requestAnimationFrame > setTimeout / setInterval
微任务的优先级: process.nextTick > Promise > MutationObserver
如果在一个微任务中递归新增微任务,是可以造成类似死循环的效果
因为,async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似,
async函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调
在上边简单的说明了两种任务的差别,以及Event Loop的作用,那么在真实的浏览器中是什么表现呢?
假设有这样的一些DOM结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="renderer" content="webkit">
<title>Document</title>
<style>
#outer {
padding: 20px;
background: #616161;
}
#inner {
width: 100px;
height: 100px;
background: #757575;
}
</style>
</head>
<body>
<div id="outer">
<div id="inner"></div>
</div>
<script>
const $inner = document.querySelector("#inner");
const $outer = document.querySelector("#outer");
function handler() {
console.log("click"); // 直接输出
Promise.resolve().then(_ => console.log("promise")); // 注册微任务
setTimeout(_ => console.log("timeout")); // 注册宏任务
requestAnimationFrame(_ => console.log("animationFrame")); // 注册宏任务
$outer.setAttribute("data-random", Math.random()); // DOM属性修改,触发微任务
}
// 注册微任务
new MutationObserver(_ => {
console.log("observer");
}).observe($outer, {
attributes: true
});
$inner.addEventListener("click", handler);
$outer.addEventListener("click", handler);
</script>
</body>
</html>
如果点击#inner,其执行顺序一定是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout。
因为 click 触发了一个宏任务 handler,按照代码中的注释,在同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现了 Promise 和 MutationObserver 两个微任务,遂执行之。
因为click事件会冒泡,所以同时也触发了 #outer 的 click 事件,再次执行了 handler 函数,这个是一个同步的过程,所以会优先执行冒泡的事件(早于其他的宏任务),重复 #inner 的结果。
在执行完同步代码与微任务以后,这时继续向后查找有木有宏任务,然后执行了 animationFrame 和 timeout。
需要注意的一点是,因为我们触发了 setAttribute ,实际上修改了 DOM 的属性,这会导致页面的重绘,而这个 setAttribute 的操作是同步执行的,也就是说requestAnimationFrame的回调会早于setTimeout所执行。
Node也是单线程,但是在处理Event Loop上与浏览器稍微有些不同,就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的process.nextTick以及宏任务的setImmediate。
在官方文档中的定义,setImmediate 为一次 Event Loop 执行完毕后调用。setTimeout 则是通过计算一个延迟时间后进行执行。
但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。因为如果主进程中先注册了两个任务,然后执行的代码耗时超过setTimeout 的延迟时间,而这时定时器已经处于可执行回调的状态了。所以会先执行定时器,而执行完定时器以后才是结束了一次 Event Loop ,这时才会执行 setImmediate 。
1 | setTimeout(_ => console.log('setTimeout')) |
有兴趣的可以自己试验一下,执行多次真的会得到不同的结果。
但是如果后续添加一些代码以后,就可以保证setTimeout一定会在setImmediate之前触发了:
1 | setTimeout(_ => console.log("setTimeout")); |
如果在另一个宏任务中,必然是setImmediate先执行:1
2
3
4
5
6require('fs').readFile(__dirname, _ => {
setTimeout(_ => console.log('timeout'))
setImmediate(_ => console.log('immediate'))
})
// 如果使用一个设置了延迟的setTimeout也可以实现相同的效果
这个可以认为是一个类似于 Promise 和 MutationObserver 的微任务实现,在代码执行的过程中可以随时插入 nextTick ,并且会保证在下一个宏任务开始之前所执行。
在使用方面的一个最常见的例子就是一些事件绑定类的操作:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Lib extends require("events").EventEmitter {
constructor() {
super();
this.emit("init");
}
}
const lib = new Lib();
lib.on("init", _ => {
// 这里将永远不会执行
console.log("init!");
});
因为上述的代码在实例化Lib对象时是同步执行的,在实例化完成以后就立马发送了init事件。而这时在外层的主程序还没有开始执行到lib.on(‘init’)监听事件的这一步。所以会导致发送事件时没有回调,回调注册后事件不会再次发送。
我们可以很轻松的使用process.nextTick来解决这个问题:1
2
3
4
5
6
7
8
9
10
11
12
13class Lib extends require("events").EventEmitter {
constructor() {
super();
process.nextTick(_ => {
this.emit("init");
});
// 同理使用其他的微任务
// 比如Promise.resolve().then(_ => this.emit('init'))
// 也可以实现相同的效果
}
}
这样会在主进程的代码执行完毕后,程序空闲时触发Event Loop流程查找有没有微任务,然后再发送init事件。
递归调用process.nextTick
会导致报警,后续的代码永远不会被执行,这是对的,
state.title.text="xxx"
这样的代码。1 | function stateChanger(state, action) { |
这时 stateChanger 就同时拥有了初始化和修改 state 的能力,如果有传入 state 就生成更新数据,否则就是初始化数据。
现在可以优化 createStore 为一个参数,将原来的参数 state 变为一个局部遍历,在完成 createStore 的操作之前,通过触发一个空操作,完成局部变量 state 的初始化。
1 | function createStore(stateChanger) { |
现在我们就拥有了一个最终形态的 createStore , 它接收一个可以根据 action
修改 state 的函数,这个函数是一个不依赖外部数据,并且没有副作用的纯函数(Pure Function),现在我们把 stateChanger 改为 reducer 就完全符合 redux 的 api 了。
完整代码:
1 |
|
1 | function renderTitle(title) { |
其他代码保持不变,依旧执行一次初始化渲染,和两次更新,然后打开控制台看下log
前三个是第一次渲染打印出来的。中间三个是第一次 store.dispatch 的结果,最后三个是第二次 store.dispatch 的结果。问题就是后两次的更新都没有改动 content 对象,只是修改了 title 对象。 renderContent 是不需要执行的,这里的操作需要优化。
可以通过在每个渲染函数执行渲染操作之前先做个判断,判断传入的新数据和旧的数据是不是相同,相同的话就不渲染了。
1 | function renderTitle(newTitle, oldTile = {}) { |
然后我们用一个 oldState 变量保存旧的应用状态,在需要重新渲染的时候把新旧数据传进入去:
1 | // 生成 store |
我们的代码现在变成了这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<div id='title'></div>
<div id='content'></div>
<script>
const appState = {
title: {
text: "redux",
color: "red",
},
content: {
text: "redux文档内容",
color: "blue"
}
};
function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT": {
state.title.text = action.text;
break;
}
case "UPDATE_TITLE_COLOR": {
state.title.color = action.color;
break;
}
default:
break;
}
}
// 添加 createStore
function createStore(state, stateChanger) {
let events = [];
return {
subscribe: (event) => {
events.push(event);
},
dispatch: (action) => {
stateChanger(state, action);
events.forEach((event) => event());
},
getState: () => state
};
}
function renderTitle(newTitle, oldTile = {}) {
if (newTitle === oldTile) {
return false;// 数据没有变化就不渲染了
}
console.log("render title...");
const titleDom = document.querySelector("#title");
titleDom.innerHTML = newTitle.text;
titleDom.style.color = newTitle.color;
}
function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) {
return false;// 数据没有变化就不渲染了
}
console.log("render content...");
const contentDom = document.querySelector("#content");
contentDom.innerHTML = newContent.text;
contentDom.style.color = newContent.color;
}
function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) {
return false;// 数据没有变化就不渲染了
}
console.log("render app...");
renderTitle(newAppState.title, oldAppState.title);
renderContent(newAppState.content, oldAppState.content);
}
// 生成 store
let store = createStore(appState, stateChanger);
// 缓存旧的state
let oldState = store.getState();
// 监听数据变化
store.subscribe(() => {
// 获取新的 state
let newState = store.getState();
// 把新旧的 state 传进去渲染
renderApp(newState, oldState);
// 新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
oldState = newState;
});
renderApp(store.getState());
// 三秒钟之后,修改标题和标题颜色,并重新渲染
setTimeout(function () {
store.dispatch({ type: "UPDATE_TITLE_TEXT", text: "Redux是React是好基友" });
store.dispatch({ type: "UPDATE_TITLE_COLOR", color: "green" });
// renderApp(store.getState());
}, 3000);
</script>
</body>
</html>
打开页面,会发现只执行了第一次渲染,而后面的两次更新根本就不执行了,why???????
我们知道在 JavaScript 函数中,所有的参数都是值传递,参数为基本类型时传递的直接就是值,参数为对象时,参数的值就是对象所在的内存地址空间,看下我们修改 state 的地方:1
2
3
4
5
6
7
8
9
10
11
12
13
14function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT": {
state.title.text = action.text;
break;
}
case "UPDATE_TITLE_COLOR": {
state.title.color = action.color;
break;
}
default:
break;
}
}
通过上面的代码可以看到,我们只是修改了 state.title
中的 text 和 color 属性,而 title 对象的内存地址依然原来的,state 的内存地址也依然是原来,所以 newState 和 oldState 都是指向同一个内存地址,所以 newAppState === oldAppState
为 true
,所以也就不会触发新的渲染。
怎么办??????
是不是可以通过浅复制生成一个新的对象,然后将修改的部分覆盖到这个新的对象上,这样既可以保证没有被修改的对象内存地址保持不变,而被修改的对象又可以获得新的地址,继而触发渲染。
每次修改某些数据的时候,不去改变原来的数据,而是把需要修改数据对象都 copy 一个出来,然后再去修改新生成的数据。如上图所示,content 对象就可以在不同的阶段进行共享。
根据这个思路,来修改 stateChanger
:
1 | function stateChanger(state, action) { |
每次需要修改的时候都会产生新的对象,并且返回。而如果没有修改(在 default 语句中)则返回原来的 state 对象。
因为 stateChanger 不会修改原来对象了,而是返回对象,所以我们需要修改一下 createStore。让它用每次 stateChanger(state, action) 的调用结果覆盖原来的 state:
1 | function createStore(state, stateChanger) { |
现在的完整代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>
<body>
<div id='title'></div>
<div id='content'></div>
<script>
const appState = {
title: {
text: "redux",
color: "red",
},
content: {
text: "redux文档内容",
color: "blue"
}
};
function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT":
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
text: action.text
}
};
case "UPDATE_TITLE_COLOR":
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
color: action.color
}
};
default:
return state; // 没有修改,返回原来的对象
}
}
// 添加 createStore
function createStore(state, stateChanger) {
let events = [];
return {
subscribe: (event) => {
events.push(event);
},
dispatch: (action) => {
// 每次修改之后, 获取新的state
state = stateChanger(state, action);
events.forEach((event) => event());
},
getState: () => state
};
}
function renderTitle(newTitle, oldTile = {}) {
if (newTitle === oldTile) {
return false;// 数据没有变化就不渲染了
}
console.log("render title...");
const titleDom = document.querySelector("#title");
titleDom.innerHTML = newTitle.text;
titleDom.style.color = newTitle.color;
}
function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) {
return false;// 数据没有变化就不渲染了
}
console.log("render content...");
const contentDom = document.querySelector("#content");
contentDom.innerHTML = newContent.text;
contentDom.style.color = newContent.color;
}
function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) {
return false;// 数据没有变化就不渲染了
}
console.log("render app...");
renderTitle(newAppState.title, oldAppState.title);
renderContent(newAppState.content, oldAppState.content);
}
// 生成 store
let store = createStore(appState, stateChanger);
// 缓存旧的state
let oldState = store.getState();
// 监听数据变化
store.subscribe(() => {
// 获取新的 state
let newState = store.getState();
// 把新旧的 state 传进去渲染
renderApp(newState, oldState);
// 新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
oldState = newState;
});
renderApp(store.getState());
// 三秒钟之后,修改标题和标题颜色,并重新渲染
setTimeout(function () {
store.dispatch({ type: "UPDATE_TITLE_TEXT", text: "Redux是React是好基友" });
store.dispatch({ type: "UPDATE_TITLE_COLOR", color: "green" });
// renderApp(store.getState());
}, 3000);
</script>
</body>
</html>
刷新页面,然后打开控制台看下log
另外,并不需要担心每次修改都新建共享结构对象会有性能、内存问题,因为构建对象的成本非常低,而且我们最多保存两个对象引用(oldState 和 newState),其余旧的对象都会被垃圾回收掉。
好像是可以的,将 renderApp 传入 dispatch ,在数据更新后,重新调用下就可以了。
但是,新的问题又来了,既然 state 是共享数据,那么使用的地方必然不止一处,如果数据更新了,需要调用的渲染函数也不止一个,dispatch 就会变得特别臃肿。
这里就要用到观察者模式了,修改 createStore 为如下代码
1 | function createStore(state,stateChanger){ |
我们在 createStore 里面定义了一个数组 events和一个新的方法 subscribe,可以通过 store.subscribe(event) 的方式给 subscribe 传入一个监听函数,这个函数会被 push 到 events 中。
每次修改数据时都会调用 dispatch ,而 dispatch 除了修改数据,还会遍历调用 events 数组里面的函数,这样就可以通过 subscribe 在 events 中注册事件,来进行数据改变之后的操作。
现在只要在使用到数据的地方,通过 subscribe 注册一个事件就可以在 dispatch 触发数据改变的时候,重新渲染使用到数据的地方。
全部代码修改如下
1 |
|
现在我们有了一个比较通用的 createStore,它可以产生一种我们新定义的数据类型 store,通过 store.getState 我们获取共享状态,而且我们约定只能通过 store.dispatch 修改共享状态。store 也允许我们通过 store.subscribe 监听数据数据状态被修改了,并且进行后续的例如重新渲染页面的操作。
dispatch
控制了对共享数据 appState
操作的渠道,这种模式可以很好的解决共享数据修改难以排查的问题,现在我们再做一次抽离,使这种模式可以很好的复用到其他应用上。构建一个函数叫createStore
用来生成一个维护共享数据的中心store
。1
2
3
4
5
6function createStore(state, stateChanger) {
return {
dispatch: (action) => stateChanger(state, action),
getState: () => state
};
}
createStore
接收两个参数 state 和 stateChanger, state 用于表示应用程序的状态,stateChanger 就是上一节的 dispatch 用于根据 action 的变化去操作 state。
createStore 会返回包含两个方法 getState 和 dispatch 的对象。getState 用于返回 state 参数,dispatch 用于修改数据,和之前不同的是它只接受一个参数 action,然后它会把 state 和 action 一并传给 stateChanger,那么 stateChanger 就可以根据 action 来修改 state 了。
现在使用 createStore 来修改上一节的代码。
1 |
|
需要注意的是,Redux 是一种从 Flux 演变而来的架构模式,它不关注跟哪个库一起用,你可以把它应用到 React 和 Vue,跟 jquery 结合也没有问题。react-redux 就是一种将 Redux 和 React 结合起来的一个库。
关于 Redux 的用法可以去官网上查看,今天主要是来看下 redux 主要解决了什么问题以及怎么解决的。
通过 webstorm 新建一个项目 redux-achieve , 新建 index.html
里面的 body 结构为:
1 | <body> |
新建 index.js
,添加代码,表示应用的状态:
1 | const appState = { |
然后添加以下函数,将状态渲染到页面上。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function renderTitle(title) {
const titleDom = document.querySelector("#title");
titleDom.innerHTML = title.text;
titleDom.style.color = title.color;
}
function renderContent(content) {
const contentDom = document.querySelector("#content");
contentDom.innerHTML = content.text;
contentDom.style.color = content.color;
}
function renderApp(appState) {
renderTitle(appState.title);
renderContent(appState.content);
}
renderApp(appState);
然后打开页面,就会看到
这是一个很简单的页面,一看就明白,但是这个页面有个很大的问题,就是状态数据 appState 是一个全局变量,每个人都可以修改它,如果页面上有很多操作的话,出现问题的时候,很难排查哪一个操作改变了 appState 的值,这也就是常说的尽量避免使用全局变量。但是,很多时候确实需要全局变量来做到不同功能模块之间的数据共享,这是一个需要解决矛盾点。
为了解决这个问题,我们做一些约定,当我们需要修改共享数据的时候,只能通过制定的方法修改,而不能直接去改,以保证出现问题的时候,方便查找问题的根源。所以新建一个 dispatch
函数,专门用来修改数据。
1 | function dispatch(state, action) { |
dispatch
接收两个参数,一个是要修改的共享数据对象 state
,一个是action
,action
是一个普通的js对象,action
里面必须包含一个 type 以表示想做什么事情,dispatch 通过这个值去执行对应的操作,action
其他的属性是可以自定义传入的。
现在所有对于数据的操作都必须通过调用 dispatch
函数来进行,这时排查 bug 就只需要在 dispatch 的 case 里面打上断点就可以调试出来了。dispatch 就像一个单一功能的数据接口,只需要关注 dispatch 所有对 state 数据的操作就都被监控了。
完整代码如下:
1 |
|
1 | npm install -g npx |
npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了测试工具 Mocha。1
npm install -D mocha
一般来说,调用 Mocha
,只能在项目脚本和 package.json
的scripts字段里面, 如果想在命令行下调用,必须像下面这样。1
2 项目的根目录下执行
node-modules/.bin/mocha --version
npx
就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。1
$ npx mocha --version
npx
的原理很简单,就是运行的时候,会到node_modules/.bin
路径和环境变量$PATH
里面,检查命令是否存在。
由于 npx
会检查环境变量$PATH
,所以系统命令也可以调用。1
2 等同于 ls
npx ls
注意,Bash
内置的命令不在$PATH
里面,所以不能用。比如,cd
是 Bash
命令,因此就不能用npx cd
。
除了调用项目内部模块,npx
还能避免全局安装的模块。比如,create-react-app
这个模块是全局安装,npx
可以运行它,而且不进行全局安装。1
npx create-react-app my-react-app
上面代码运行时,npx
将create-react-app
下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载create-react-app
。
下载全局模块时,npx
允许指定版本。1
npx uglify-js@3.1.0 main.js -o ./dist/main.js
上面代码指定使用 3.1.0
版本的uglify-js
压缩脚本。
注意,只要 npx
后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server
模块,下面的命令会自动下载该模块,在当前目录启动一个 Web
服务。1
npx http-server
如果想让 npx
强制使用本地模块,不下载远程模块,可以使用--no-install
参数。如果本地不存在该模块,就会报错。1
npx --no-install http-server
反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用--ignore-existing
参数。比如,本地已经全局安装了create-react-app
,但还是想使用远程模块,就用这个参数。1
npx --ignore-existing create-react-app my-react-app
利用 npx
可以下载模块这个特点,可以指定某个版本的 Node
运行脚本。它的窍门就是使用 npm
的 node 模块
。1
2 npx node@0.12.8 -v
v0.12.8
上面命令会使用 0.12.8
版本的 Node
执行脚本。原理是从 npm
下载这个版本的 node
,使用后再删掉。
某些场景下,这个方法用来切换 Node
版本,要比 nvm
那样的版本管理器方便一些。
-p
参数用于指定 npx
所要安装的模块,所以上一节的命令可以写成下面这样。1
2 npx -p node@0.12.8 node -v
v0.12.8
上面命令先指定安装`node@0.12.8,然后再执行
node -v`命令。
-p
参数对于需要安装多个模块的场景很有用。1
npx -p lolcatjs -p cowsay [command]
如果 npx
安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx
安装的模块,后面的可执行项还是会交给 Shell
解释。1
2 npx -p lolcatjs -p cowsay 'cowsay hello | lolcatjs'
报错
上面代码中,cowsay hello | lolcatjs
执行时会报错,原因是第一项cowsay
由 npx
解释,而第二项命令localcatjs
由 Shell
解释,但是lolcatjs
并没有全局安装,所以报错。
-c
参数可以将所有命令都用 npx
解释。有了它,下面代码就可以正常执行了。1
npx -p lolcatjs -p cowsay -c 'cowsay hello | lolcatjs'
-c
参数的另一个作用,是将环境变量带入所要执行的命令。举例来说,npm
提供当前项目的一些环境变量,可以用下面的命令查看。1
npm run env | grep npm_
-c
参数可以把这些 npm
的环境变量带入 npx
命令。1
npx -c 'echo "$npm_package_name"'
上面代码会输出当前项目的项目名。
npx
还可以执行 GitHub
上面的模块源码。1
2
3
4
5 执行 Gist 代码
npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
执行仓库代码
npx github:piuccio/cowsay hello
注意,远程代码必须是一个模块,即必须包含package.json
和入口脚本。