Listentolife

简单就好

  • 首页
  • 标签
  • 分类
  • 归档

Vue.js散记

发表于 2018-12-19 | 分类于 JavaScript , Vue

这里主要记录一下学习Vue过程中遇到的问题。

1. [Vue warn]: Error in render: “TypeError: Cannot read property ‘xxx’ of undefined

之前敲代码的时候,发现了这个报错,一开始的观察点在TypeError上,认为是某个props没有正确定义,但是检查的时候并没有写错。后面发现应该要看前面Error in render,是渲染时报错。最后是找到有个地方变量名写错了。

2. 为什么data必须是一个函数

这个问题其实在vue.js官网有说明,并且给出了一个例子。正确的写法应该是

1
2
3
4
5
data: function () {
return {
count: 0
}
}

这是因为,在创建组件的实例时,data作为一个函数可以返回一个初始化对象,能独立于其他实例的data对象。如果data不是作为一个函数,而只是一个数据,则会导致所有实例公用一个data对象。

3. 计算属性computed,侦听属性wtach&方法methods

计算属性computed的设计,是用于处理一些复杂的运算。一般在template模板中是支持使用表达式的,但是如果需要进行一些复杂的运算或数据处理,就可以把这些运算或数据处理放到计算属性中处理。

当然,这些复杂的运算或数据处理也可以定义为一个方法放到methods中,但是放在计算属性和放到方法中是有其不同的地方的。

vue官网有明确的指出这两者的不同。计算属性computed是基于它们的依赖进行缓存的。也就是说,methods中所有的运算结果只有当依赖的变量发生改变的时候才会重新计算,否则只要变量不改变,计算属性不会重新执行函数,直接返回缓存的计算结果。相反的,方法methods是并非响应式的,每次触发重新渲染时,调用的方法总会在此执行函数,即使依赖并没有发生变化。

计算属性computed的优势在于可以减少很多不必要的性能开销。而方法methods中的函数都是需要根据页面的渲染或不需要缓存函数结果的

侦听属性watch跟计算属性computed也有极为相似的方面,侦听属性是需要监听数据的变化并作出响应。但是一般用于执行异步或开销较大的操作时,才比较适合。

4. 指令v-for & key

v-for是vue.js用于渲染列表的指令,针对数组可以使用(item, index) in items的语法迭代数组,针对对象可以使用(item, key, index) in items的语法迭代对象。其中,key的用途主要在于优化渲染过程。vue.js在处理数据项的变化时,对只改变顺序的数据项会选择复用,这样能优化渲染性能。如果没有提供key也只是会报提示,不影响项目运行,但是建议提供(如果是在组件上使用,则必须提供key)。

5. 父子组件通信

vue.js的父子组件通信跟react类似,父组件向子组件传值,是通过props传入,子组件是通过$emit方法触发父组件的事件。父组件在使用子组件时,通过v-bind指令传入或通过v-for传入数组中的一个元素,子组件需要在props中声明这些接收的值。

父组件通过props向子组件传值这点来看,vue.js的操作跟react是类似的,但是子组件向父组件通信的处理,就有点不同了。

vue.js中,父组件中需要在子组件上用v-on指令监听事件,然后子组件上通过$emit(event, arg)来向父组件触发事件,必要时传入参数arg。react处理上不同,react中父组件需要通过props向子组件传入方法,子组件只要使用这个方法并传入参数即可,并不是通过事件触发。

6. 通过插槽分发内容

另外开一篇文章来记录这个内容,因为插槽的分类比较多,能用起来的话作用也很强大。

算法分析笔记

发表于 2018-12-19 | 分类于 JavaScript , Algorithm Analysis

最近一段时间有看一些关于算法的文章,主要是因为在leetcode跟warcode都有做过一些题目了。算法以前学过一些皮毛,但是还是会觉得不够,有些算法还是需要找很多资料花一些时间才能想明白。这里整理一些关于算法分析的笔记。

复杂度分析

什么是复杂度分析

数据结构和算法解决的是“如何让计算机更短时间,更省空间的解决问题”,因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能。

我们可以分别用时间复杂度和空间复杂度两个概念来描述算法的性能问题,这两个概念也统称为算法的复杂度分析。因此,算法的复杂度描述的是算法执行的时间或占用的空间与数据规模的增长关系。

为什么要做复杂度分析

其一,和性能测试相比,复杂度分析有不依赖执行环境,成本低,效率高,已操作,指导性强的特点。

其二,掌握复杂度分析,将有助于我们编写出性能更优的代码,有利于降低系统开发和维护成本。

如何计算复杂度

大O表示法

算法的执行时间与每行代码的执行次数成正比,用T(n) = O(f(n))表示其中T(n)表示内行代码执行的总次数,而n表示数据的规模。

以时间复杂度为例,由于时间复杂度描述的是算法执行时间与数据规模的增长变化趋势,所以常量阶,低阶以及系数实际上对这种增长趋势不产生决定性的影响,所以在做时间复杂度分析时可以忽略这些项。

复杂度分析技巧:

单独一段代码,只需要找到最高频运行的代码分析,比如循环代码。

多段代码的分析,则取所有代码中最高频运行的代码分析,比如一段代码中同时有单循环和多重循环的,只需要分析多重循环的代码复杂度;即:如果T1(n) = O(f(n)), T2(n) = O(g(n)),则T(n) = T1(n) + T2(n) = max(O(f(n)), O(g(n))) = O(max(f(n), g(n)))。

嵌套代码的分析,可以先把嵌套拆分分析复杂度,再相乘。即:如果T1(n) = O(f(n)), T2(n) = O(g(n)),则T(n) = T1(n) x T2(n) = O(f(n) x g(n)) = O(f(n)) x O(g(n))。

多参数多段代码的分析,需要分别分析每个参数参与代码的复杂度,然后再相加。即:如果T1(n) = O(f(n)), T2(m) = O(g(m)),则T(n, m) = T1(n) + T2(m) = O(f(n)) + O(g(m))。

常见的时间复杂度

以下是一些常见的时间复杂度量级:

  1. 常量阶O(1):常量阶一般指代码的时间复杂度不随n的增大而增长。

  2. 对数阶O(logn),O(nlogn):简单的例子:用while循环计算一个2的n次方,n是参数,其时间复杂度为O(logn)。

  3. 线性阶O(n):线性阶指代码的时间复杂度跟n成比例。

  4. 平方阶O(n^2),立方阶O(n^3):平方阶跟立方阶都是随n的增大而呈现高倍增长。

  5. 非多项式阶-指数阶O(2^n),阶乘阶O(n!):非多项式阶的时间复杂度会随着数据的规模增长而大幅度增长,所以这类算法的性能都很差。

空间复杂度

一般来说,提及复杂度分析主要指的是事件复杂度分析。但是实际还有空间复杂度分析。空间复杂度全程为渐进空间复杂度,表示算法的储存空间与数据规模之间的增长关系。同样是使用大O表示法,但是分析的是存储空间。

react学习总结系列-redux&react-redux

发表于 2018-12-19 | 分类于 JavaScript , React

本篇将介绍redux跟react-redux。这里打算是介绍这两个插件在react项目中的运用。redux是可以在其他库中使用的。

前面使用react开发项目,是可以实现大部分需求的。但是react毕竟是一个实现视图层的框架,它在数据层的实现还是存在一些不足的。比如当项目较大,需要使用大量的数据,父子组件之间的数据通信特别频繁的时候,性能就会直线下滑;当数据发生改变的时候,就无从得知数据是从哪里发生改变的。

redux作为一个应用数据流框架,它的最大的特点就是应用状态的管理,它用一个单独的常量状态树store保存整个应用的状态。这个状态树不能直接被改变。

我们分析一下。如果没有redux,react组件的所有状态state将各自存储于组件中,一旦遇到不同父组件的组件之间的通信问题,需要把数据先传到能使两个组件通信的上层组件,然后在下发到另一个组件。而如果使用redux,则数据都存储在常量状态树store中,所有组件获取状态数据都从store中直接获取。而组件需要修改状态数据时,只需要直接修改store中的数据,其他组件就会获知store的变化并自动重新获取状态。这样可以减少组件之间大规模的通讯消耗。

redux工作流程

我们了解了react的组件components跟redux的常量状态树store,那他们是怎么串连起来,是怎么样的工作流程呢?

这里需要考虑三个场景,一个是components需要修改store中的数据,一个是store中实现数据的更新,还有一个就是store的数据更新之后新状态同步到components中。

最主要是前两个场景。这里需要引入redux中两个概念。

actionCreator

因为常量状态树store的状态数据不允许直接修改,所以当components需要修改store中的数据时,需要通过actionCreator中的action去让store做数据的更新。

actionCreator是一个action的集合,这个集合中包含了各种用于申请修改的action。这个action需要包括修改数据的类型跟需要修改的数据。在components发出store.dispatch(action)之后提交给store,完成修改申请。

reducer

当store得到action时,它本身没有什么方法可以修改数据,所以需要用到reducer来实现状态的更新。store会把当前的state跟前面接收到的action传给reducer,reducer会根据传入的action类型进行状态更新,然后再返回新的state。

组件的状态更新

关于最后store的数据更新之后新状态同步到components中,阮一峰的《Redux 入门教程(一):基本用法》中有提到,当store中state发生变化后,store会调用监听函数,监听函数就可以更新组件中的state,组件就可以重新渲染页面。

react-redux实战

安装react-redux

在react项目中,引入react-redux还需要引入redux:

1
2
npm install redux --save
npm install react-redux --save

这样在react项目中才可以使用react-redux。

创建store

先在项目中创建一个store的文件夹,然后在创建一个index.js的js文件。在这个js文件中写下以下代码:

1
2
3
4
5
import { createStore } from 'redux';

const store = createStore(reducer);

export default store;

首先,先从redux中引入createStore,然后通过createStore来创建一个store,最后导出。

创建reducer

上面我们已经完成store的创建,然后需要创建reducer来给store处理数据。在store目录下创建一个reducer.js的js文件。redux中,reducer其实是一个函数,所需要引入的参数为当前的state(preState)跟action,而返回的是一个修改后的state(newState)。一般初始化的时候,没有当前的state(preState),所以会先声明一个defaultState:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const defaultState = {
/*
*这里初始化数据
*/
};

export default (state = defaultState, action) => {
switch (action.type) {
/*
* 这里判断action.type,然后修改获得newState
*/
}
return newState;
}

上面的代码中,先声明一个defaultState作为state的初始化状态,然后导出一个函数,这个函数中通过判断action.type来修改获得newState,然后返回出去。

创建之后,需要把reducer传入到store中,因此上面的index.js需要稍加修改:

1
2
3
4
5
6
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;

创建actionCreator

actionCreator还是要强调,它是一个方法的是一个action的集合,这个集合中包含了各种用于申请修改的action。当然,把所有action都只在需要的时候再编写也可以,但是把所有action集中起来可以提高代码的可读性,可维护性。每一个action方法最终都是返回一个对象,这个对象包含有修改类型type跟需要修改的值。在reducer.js中,大家可以看到传入的action.type是会在switch语句中作为判断值,然后修改值则会在判断后做对应的修改。

在store目录下创建一个actionCreator.js的js文件,然后写入所有需要导出的action方法:

1
2
3
4
5
6
7
8
9
export const action1 = (value1) => ({
type: actionType1,
value1: value1
})

export const action2 = (value2) => ({
type: actionType2,
value2: value2
})

在reducer中,action.type作为判断时,会再写一次。为了保证不出抄写错误,方便后期排查bug,一般还可以把actionType集中起来:

1
2
export const actionType1 = 'action_type_1';
export const actionType2 = 'action_type_2';

那么,在actionCreator中就可以改写成:

1
2
3
4
5
6
7
8
9
10
11
import * as constants from './actionType';

export const action1 = (value1) => ({
type: constants.actionType1,
value1: value1
})

export const action2 = (value2) => ({
type: constants.actionType2,
value2: value2
})

组件中的使用

组件中使用一般分引入store,调用action申请及store修改订阅。

组件中会现在构造函数中引入store:

1
2
3
4
constructor (props) {
super(props);
this.state = store.getState(); // 引入store,把store中的状态赋给state
}

当状态需要修改时,通过store.dispatch()调用action:

1
store.dispatch(actionCreators.action1(value1));

action方法返回对象将交给store,但是store不会处理,而是通过dispatch方法交给reducer处理。reducer接收到action后,会根据修改类型type对数据进行更新。在store的状态进行更新之后组件就需要更新。但是组件的状态更新还需要做一步操作,就是对store变化的订阅。这个订阅也是在constructor()完成:

1
2
3
4
5
constructor(props) {
super(props);
this.state = store.getState(); // 引入store,把store中的状态赋给state
store.subscribe(this.handleStoreChange); // 订阅store的状态修改,如果检测到修改,则调用this.handleStoreChange监听函数
}

以上就是对redux跟react-redux使用的总结。

react学习总结系列-styled-components

发表于 2018-12-19 | 分类于 JavaScript , React

这一篇主要整理关于React的CSS插件styled-components。这个插件是我一开始学习React跟做项目的时候推荐并使用的。用了之后觉得这个插件确实很适合React的项目,有无缝对接的效果。

这里也是留一下styled-components官网文档地址:

https://www.styled-components.com/docs

安装

简单说一下安装。

代码1 NPM&CDN安装

1
2
3
4
5
/* NPM install */
npm install --save styled-components

/* CDM install */
; <script src=" https://unpkg.com/styled-components/dist/styled-components.min.js" />

简单使用

styled-components使用也比较简单。最简单的写法就是声明styled变量,然后把css样式以模板字符串形式写入,最后把这个变量以React组件的形式写到render()中就可以了。

代码2 styled-components简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;

const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;

render(
<Wrapper>
<Title>
Hello World, this is my first styled component!
</Title>
</Wrapper>
);

上面是官网用例。可以看到,其实是把css样式表嵌入到JavaScript中,然后通过styled-components转成组件,然后再在其他组件中使用。所以使用起来非常简单。

传递参数

因为style-components把css样式表转成一个个的组件,所以也可以实现一些样式的传参。

代码2 传递参数,对代码1的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: \function(e){return e.inputColor||"palevioletred"};
`;

render(
<div>
<Title inputColor="rebeccapurple" >
Hello World, this is my first styled component!
</Title>
</div>
);

代码2中,在变量Input的样式表中,color后面是跟着一个\,接着一个匿名函数。这个匿名函数有一个传参e,函数中会返回e.inputColor或默认值"palevioletred"。在render()函数中,第二个Input组件标签中声明了一个inputColor的属性值,而第一个没有。在最终的显示上,第一个组件字体样式是紫罗兰红(palevioletred),第二个组件字体样式是丽贝卡紫(rebeccapurple)。也就是说,变量Input的样式表中的匿名函数,传入的参数e应该指向当前组件对象

传递参数的类型还可以是真假值。组件的属性上,可以直接声明一个属性并不传值,匿名函数中可将直接判断是否存在这个属性,以确定返回的样式值。

代码3 传递属性,对代码1的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: ${props => props.primary ? "palevioletred" : "white"};
`;

render(
<div>
<Title primary >
Hello World, this is my first styled component!
</Title>
</div>
);

组件继承

一个项目中或多或少会出现一些组件上设计样式的类似,比如不同级标题可能HTML样式,尺寸是一样的,但是颜色会不同。如果都是写成独立的组件样式表,则会出现代码冗余,不好维护。这种情况下就可以考虑使用组件继承。

代码4 组件继承,对代码1的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;

const SecondTitle = styled(Title)`
font-size: 1em;
font-weight: bold;
`

render(
<div>
<Title>
Hello World, this is my first styled component!
</Title>
<SecondTitle>
This is the second title!
</SecondTitle>
</div>
);

如果Title中没有样式的话,那它就是直接继承react.component的组件,仍然可以被继承。

动画

styled-components对动画也有很好的支持。内置了keyframes来实现CSS3中的@keyframes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const rotate360 = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;

const Rotate = styled.div`
display: inline-block;
animation: ${rotate360} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;

render(
<Rotate>&lt; 💅 &gt;</Rotate>
);

除了上面介绍的方面之外,styled-component插件还有一些更复杂的用法,但是目前大部分的样式实现只要上面这些内容掌握了,在项目中也能解决大部分的问题了。剩下的以后有涉及到会再介绍。

下一篇将会介绍redux及react-redux插件。

react学习总结系列-react(二)

发表于 2018-11-24 | 分类于 JavaScript , React

这篇主要写的是React中父子组件数据传递跟生命周期。

父子组件数据传递

上一篇其实有说到,组件跟组件之前其实是相对独立的。虽然每个组件的数据是可以独立处理,但是组件与组件之间的数据还是存在联系的,而所有的组件之间的数据关系,都是可以归结为父组件跟子组件之间的数据传递关系。再拆分这种数据传递关系,就是要解决父组件数据传给子组件,子组件数据传给父组件两个问题。

  1. 父组件传入参数。在React中,父组件给子组件传递数据其实挺简单的,就是在子组件标签中通过变量进行传入,子组件通过props获取传入的数据:

代码1 父组件传参

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
/* 自己写豆瓣的一段代码,
* 图书页面Book组件中把页面类型参数Contants.Book传入
* 搜索Search组件
* /
class Book extends Component {
render () {
return (
<Fragment>
<Search
page={this.state.book}
searchInfo={this.getSearchInfo}
/>
</Fragment>
)
}
}

/* 搜索Search组件通过props.page接收父组件的参数,
* 判断input标签的属性placeholder的值
* 项目中还使用了styled-components插件,后面会另有介绍
* /
class Search extends Component {
// 根据父组件传入的props.page确定input标签的属性placeholder的值
showPlaceHolder () {
const { placeHolders, page } = this.props
const placeHoldersJS = placeHolders.toJS()
return placeHoldersJS[page]
}
render () {
return (
<SearchWrapper>
<div>
<SearchInput
value={this.props.keyword}
placeholder={this.showPlaceHolder()}
/>
</div>
</SearchWrapper>
)
}
}

上面的代码有所省略,但基本上是对父子组件之前传递数据有基本的实现。注意,父组件的数据需要在子组件标签内用一个变量传入,这个变量将会放在子组件的props属性中。子组件调用这个变量时需要到props中调用。

  1. 子组件传出参数。一般来说,React的数据流是单向流动,即父组件的数据可以流入子组件,但是子组件的数据是不能向父组件传递或修改父组件的数据。但是子组件是可以通过自己的事件处理函数,手动触发父组件传递进来的回调函数,在回调函数上把数据通过参数传递进去。这样就可以让子组件当数据有变化的时候,父组件也可以根据变化进行响应处理:

代码2 子组件调用父组件回调函数

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
// 父组件Book组件向子组件Search先传入getSearchInfo()方法
class Book extends Component {
constructor (props) {
super(props)
this.getSearchInfo = this.getSearchInfo.bind(this)
}

getSearchInfo (newKeyword) {
/* 省略代码 */
}

render () {
const { pullDownStatus, pullUpStatus } = this.props
return (
<Fragment>
<Search
searchInfo={this.getSearchInfo}
/>
</Fragment>
)
}
}

// 子组件在handleClick()方法中调用父组件传入的回调函数searchInfo(keyword),并把值传入
class Search extends Component {
constructor (props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}

handleClick () {
const { searchInfo, keyword} = this.props
if (keyword !== '') {
searchInfo(keyword)
}
}

render () {
return (
<SearchWrapper>
<div>
<SearchInput />
<SearchButton
onClick={this.handleClick}
>搜索</SearchButton>
</div>
</SearchWrapper>
)
}
}

这里有个地方需要注意的。父组件方法getSearchInfo()虽然是对象中的方法,但是它也有自己的作用域,所以正常来说它的this指向它自己的作用域,this.props就不存在了。所以需要在constructor()中指定它的作用域为整个类,需要补上this.getSearchInfo = this.getSearchInfo.bind(this)。

生命周期

我们来回头说一下组件的生命周期。组件的生命周期包括了初始化,第一次加载渲染,更新渲染跟卸载四个阶段。而React的生命周期函数主要分布在后面三个阶段。我们先每个阶段简单整理一下:

初始化

这个阶段主要还是调用了ES6中类的constructor()。通过构造器完成对父类的继承,变量的初始化和函数指定作用域。这个阶段还没有调用生命周期函数。

第一次加载渲染

第一次加载渲染的过程会调用三个生命周期函数:

1.componentWillMount(),这个函数将会在组件第一次加载渲染之前被调用。可以做一些组件渲染到页面前的操作。只调用一次。

2.render(),这个函数就是负责创建虚拟DOM,此时所有数据都至少已经初始化了。

3.componentDidMount(),这个函数是在组件第一次加载渲染完成之后马上被调用的,也只调用一次。这个函数可以调用ajax请求,返回的数据会在后面阶段更新组件状态,并触发重新渲染

更新渲染

第一次加载渲染完成之后到组件被卸载之前,组件中的state状态跟props数据还是可以改变。所以会调用下面几个生命周期函数:

1.componentWillReceiveProps(nextProps),在组件第一次加载渲染之后,如果props数据将要发生改变,那改变之前会先调用这个函数。函数的传参为props改变之后的值。

2.shouldComponentUpdate(nextProps,nextState),在组件props或state将发生改变,进行更新渲染之前会调用这个函数。函数的传参为props或者state改变之后的值。函数最后返回值一定是布尔值,表示是否重新渲染组件(true为重新渲染,false为阻止渲染)。这里可以判断props或state的值是否达到需要重新渲染组件的要求。这个函数的判断有助于提高页面性能,因为当父组件重新渲染时也会导致其所有子组件重新渲染,所以在组件中做这个判断可以阻止不必要的重新渲染。

3.componentWillUpdate (nextProps,nextState),在组件调用shouldComponentUpdate(nextProps,nextState)并返回true之后,进行更新渲染之前会调用这个函数。这里同样可以拿到nextProps跟nextState进行操作。

4.render(),这里的渲染就是根据新更新的props跟state的值进行重新创建虚拟DOM,然后在根据diff算法比对新旧DOM数,找到有差异的最小DOM节点,并重新渲染。

5.componentDidUpdate(prevProps,prevState),在组件重新渲染之后会调用这个函数。函数的传参为props或者state改变之前的值。

组件卸载

当组件将不再被渲染到页面上时,组件将会被卸载。卸载前只有一个函数会被触发调用:

componentWillUnmount () ,这个函数因为是在组件被卸载前被调用,所以很适合以下一些操作:

1.清除组件内的所有定时setTimeout,setInterval;

2.移除组件内的所有监听事件 removeEventListener;

3.处理未完成的ajax请求。一般ajax请求之后都会修改state,导致会调用组件的setState(),如果组件被卸载的时候ajax请求未完成,会导致请求完成后调用报错,所以可以在组件内设置状态值控制ajax请求完成后是否执行组件内setState(),然后在componentWillUnmount ()中修改状态值。

父子组件的生命周期

这里拓展一下,父子组件的生命周期被调用也是有顺序的。

在组件初次渲染阶段,会从父组件开始,先调用父组件的constructor()构造函数、componentWillMount(),然后接着子组件开始调用这两个函数,直到最底层子组件调用这两个函数。接着调用对底层子组件的componentDidMount(),再一层层往上调用父组件的这个函数,直到最顶层父组件。

在组件的卸载阶段,则会先从最顶层父组件开始调用componentDidUnmount(),直到最底层子组件。

父子组件之间的性能优化

其实在React中,父组件的数据变化,是会影响子组件进行重新渲染,倒是出现不必要的性能消耗。

前面有说到,可以通过shouldComponentUpdate(nextProps,nextState)函数来判断是否需要重新渲染组件,但这样一来,每个组件都要手动判断重新渲染。每次只要出现父组件传来的数据,包括state状态的变化,都会调用这个函数,也会导致代码冗余。所以React还提供了一种处理方案。

React V15中就引入了React.PureComponent。这个PureComponent跟Component用法上是一样的,只是组件继承了PureComponent类后,组件会自动实现props跟state的浅比较。当组件的props跟state的类型都比较简单的时候,就可以直接使用PureComponent。当如果props跟state是嵌套对象或数组时,浅比较将得不到预期的结果。

关于 React的内容就先把这些基础的先介绍到这里,主要还是要看文档,这里的就算是一些个人整理。下一篇将会介绍style-components插件。

react学习总结系列-开篇

发表于 2018-09-12 | 分类于 JavaScript , React

前面写了好几篇关于Canvas射击小游戏的博文,内容还总结了挺多的。不过其实在写之前主要的时间都在学习react。react是一个很热门的mvc框架,支持的插件非常多,从小页面到大工程都是可以hold得住,所以很有必要学习react并要熟练的使用。所以连着做了两个项目。一个是找了些学习资料边学边做的,是比较简单的简书PC端网页项目,学习完后就去拿豆瓣的数据做了一个简单的豆瓣app。现在打算开始写新的系列博客,把中间学习react的内容做一下整理回顾,把掌握的东西再加强一下。

内容主要涉及react框架及使用过或期间学习过的框架的,有一些内容比较多,会结合代码去说,有一些学得比较浅,就先把项目中有用到的先介绍了,以后在慢慢的深入。再者就是做了两个项目了,对项目的分析跟设计也会想说一下。

先给后面会涉及到的内容做一个列表,然后一项项的完成。

列表:
react框架
styled-components插件
PropTypes插件
redux及react-redux插件
redux-thunk及redux-saga插件
redux-immutable插件
react-router-dom插件
react-transition-group插件
jest测试

列表可能会继续更新,然后下一篇将会从react框架开始。

react学习总结系列-react(一)

发表于 2018-09-12 | 分类于 JavaScript , React

这里开始总结一下React。最好了解跟学习react的方法当然是去看react的官方文档,所以这里主要写的是各个知识点需要注意的点。

JSX语法

JSX语法就是在JS中直接写类似HTML标签,但实际上写的不是HTML代码,这中写法其实是React.creatElement(component, props, ...children)的语法糖。这种语法糖的书写相较于后者的书写更为方便易懂。

代码1 react文档中的例子

1
2
3
4
5
6
7
8
9
10
11
// JSX语法
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>

// 编译为:
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)

react文档有非常详细的介绍,这里主要提一些需要留意的点:

1.字母大小写。标签名(组件名)都是驼峰命名法(大驼峰命名法),后面也会说到的style-component插件也是用这样的写法。

2.className。因为JSX语法其实是JS封装的,所以class还是js的关键词。所以在标签中如果要使用class类的话,需要写成className而不是class。写成class也不会影响代码的运行,但是会报一个提示。其他的html标签中使用的属性在JSX中都是小驼峰命名法的写法。

3.标签之间的值。如果标签之间没有值,则可以直接一个标签闭合。在html中,有一些标签是单一标签,不用写/闭合也是可以的,但是在JSX中所有标签都需要用/来闭合,所以像input,img都需要写成<input />、<img />的闭合形式。如果是标签之间有值,那么支持像html一样直接把字符串写在标签之间,也可以直接写变量,如果遇到数组,可以用map函数枚举数组所有项,但是需要注意的是,标签之间不支持多个语句,最多只支持一个语句。上面用到的map也是一个语句下完成的。不支持if-else判断语句,但是支持三元判断。

4.获取dom。react中支持用ref来获取dom对象,但实际上是react封装的一个React对象。如果要获取原生dom对象,可以使用innerRef。(PS: 这一部分其实涉及起来可以有很多内容,不过先知道这些就好,而且一般也建议少用ref)

代码2 获取div的React对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// v16之前写法
class MyComponent extends React.Component {
render() {
return <div ref={(myRef) => {this.myRef = myRef}} />;
}
}

// v16的写法
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

5.标签中支持写入带html代码的字符串,但是如果直接填入这样的字符串并不会被渲染成相应的内容,因为react也有这方面的安全防御。如果一定要填入这样的字符串,则需要通过dangerouslySetInnerHTML这个属性来传入。`dangerouslySetInnerHTML={{__html: }}`

代码3 在Content组件中传入带HTML代码的字符串this.props.content

1
<Content dangerouslySetInnerHTML={{__html: this.props.content}} />

6.JSX语法可以在函数中使用。一般指函数最后返回的值是直接放在标签中的。其实这些JSX语句在函数中还是以字符串的形式专递的,直到进入render()的return中才会被编译。

7.属性。标签中的属性都是小驼峰命名法。属性支持的值可以是各种类型的(包括Function),也可以是一个执行语句,但是一样不支持多语句,判断语句中只支持三元判断。

组件

组件是react中一个很重要的概念。首先,从一个用户来说,整个页面可以分为多个信息块,不同的信息块可以获得不同的信息,而从开发者来说,整个页面可以分为多个开发模块,每个模块都是相对独立,互不干扰,而且有些模块可能在整个网站多个页面都会出现。组件其实就是这些开发模块,它拥有属于自己的数据(信息),根据自己的样式被渲染到页面上。

react中组件的写法很简单,上一篇已经提到了JSX语法,针对的就是react的组件。组件中可以直接书写字符串,非引用类型变量,执行一条语句等,也可以什么都不写,自我闭合。

组件中还可以嵌套子组件,但是前提是子组件必须已经定义或者引入。下面来说一些关于组件开发中的一些需要注意的点:

1.组件的返回值只能有一个根元素。组件的返回值如果不只有一个根元素,则编译的时候回出现报错,无法渲染到页面。一般在实现一个组件时,会在最外层用一个html标签包住内容。

2.Fragment组件。有时候因为一些样式的原因,组件最外层的标签会影响页面显示的效果。这个时候可以考虑使用react提供的方案,Fragment。先把Fragment从react中引入,然后把组件最外层的标签替换成<Fragment>。在编译之后,DOM中将不会出现Fragment组件,组件内部直接渲染出来。这样既减少不必要的div标签,还可以保证一些样式的效果。

3.React DOM更新。大家都知道,React是通过虚拟DOM来提高页面性能的。React对DOM的操作其实都是对虚拟DOM的操作。当虚拟DOM的修改完成之后,React在把虚拟DOM跟DOM做比对,把需要更新的部分进行更新。注意,这里是只更新需要更新的部分,其他没变动的部分不会更新。

4.props跟state 。props跟state其实区别还挺明显的,不过这个还是用得越多也清楚这个区别。props可以理解为传入的参数的集合,因为这个集合内的变量都是传入进来的,所以是不可以修改的。state可以 理解问组件自身的属性值集合,这些属性都是会根据不同状态页面的表现而改变,所以是可以修改的,但是需要调用setState()进行修改,不能随意修改。所有的props在组件外传入,包括父组件,url等等,而state是需要在组件的constructor()中先声明后才可以赋值使用。

5.super(props); 。在写constructor(),必须要先写一个语句super(props);。我看网上的解释是因为我们写的组件都是继承父类React.component,而我们在组件中写的constructor()会覆盖掉父类的constructor(),导致你父类构造函数没执行,所以手动执行下。

其他的注意点想到或遇到后更新上来,现在就先写这么多。下一篇会介绍React的父子组件数据传递跟生命周期。

Canvas 射击小游戏详解系列(六)

发表于 2018-09-07 | 分类于 HTML5 , Canvas

这篇将介绍页面动画跟项目设计实现。先会对动画做介绍,然后再总结项目中实现动画的设计。

动画

这里说的页面动画指的是js动画,使用requestAnimationFrame()实现。

实现动画效果的原理是把动画拆成一帧帧的画面,当达到每秒至少24帧时,可以给人造成流畅的视觉变化效果。也就算在实现动画效果时,要经历更新画面,清除画面,绘制画面三个部分。

代码1 每个循环的代码逻辑

1
2
3
4
5
6
7
var animate = function () {
/*
* 1.更新下一帧画面,比如移动
* 2.清除当前页面画面
* 3.绘制下一帧画面
*/
}

第一步是更新下一帧的画面,处理完一些画面元素的数据调整跟状态调整;第二步是清除当前页面画布,腾出画布空间;第三步绘制下一帧画面,刷新画布内容。

完成三步循环之后,还需要通过requestAnimationFrame()进入下一个循环周期。

另外,既然有动画就需要用暂停或者结束的情况,需要做状态的判断。判断状态转变成暂停或者结束的条件一般有监听事件,碰撞检测,位置范围,超时等等。一般在第一步前判断是否需要更新,调用requestAnimationFrame()前判断是否需要进入下一个循环周期。

最终,一个完整的动画设计就明了了。

代码2 完整的动画代码逻辑

1
2
3
4
5
6
7
8
9
var animate = function () {
/*
* 判断条件,选择暂停动画或继续动画
* 1.更新下一帧画面,比如移动
* 2.清除当前页面画面
* 3.绘制下一帧画面
* 判断条件,选择调用requestAnimationFrame(animate)或结束动画
*/
}

项目设计

项目中的动画主要是在GAME对象中。当玩家点击开始游戏后,会执行play()方法。这个方法会先设置游戏状态,然后重置关卡数据,创建跟初始化画布所有的元素,然后再调用animate()方法。

动画设计

判断

进入animate()方法后,先做判断。先判断暂停状态变量是否为真,是则直接在画布上渲染暂停的图标。但是不是真的停下动画,只是让每一帧的渲染都是一样而已。直到暂停状态变量修改成假,就可以给移动量赋值。

在项目中,判断前还需要对键盘对象做一次检测,确认玩家是否有出现键盘事件,再根据不同的键盘事件修改。

更新动画

完成判断,继续动画时,根据前面得到的移动量,传入到飞机,怪兽类的实例对象中修改坐标位置。调用碰撞测试的方法collision(),判断碰撞是否发生。

清除画布

参考上一篇,直接使用context.clearRect(0, 0, canvas.width, canvas.height)。

绘制画面

完成清除之后,调用drawOject()方法。这个方法内部会遍历所有画布的对象,调用对象的绘制方法。

调用requestAnimationFrame(animate)

这里需要再加一个判断。因为在前面的更新动画中,只是判断了是否暂停,这里判断是否结束动画。前面绘制之后,可能结束的条件是怪兽已经被消灭完或怪兽已经到达底部,其他情况下继续动画,需要调用requestAnimationFrame(animate)。

碰撞设计

碰撞设计主要是判断两个画布元素是否有重叠,其次是碰撞之后的状态跟动画效果的处理。这里主要讨论碰撞判断。

碰撞的判断到底为画布元素的边界是否有交叉。比如矩形的碰撞判断,为两个矩形的左边界与右边界,上边界与下边界的交叉判断。比如圆形,则是判断两个圆形圆心的距离是否小于两个圆形的半径之和。

如果是考虑矩形跟圆形,则需要判断矩形四个角跟圆形的位置关系及矩形中心到圆心的距离;

如果是考虑矩形缺角或圆形缺弧,则需要判断矩形各个角跟圆心及弧角的位置关系及矩形中心到圆心的距离。

如果是跟复杂的图形,则需要增加参考移动行为。比如项目中的怪兽跟子弹。不管怪兽的形状有多复杂,子弹的移动只有向上一个移动行为,所以主要的判断可以是子弹最上方的坐标是否进入怪兽边界内。

越复杂的碰撞判断,越要参考除了图形之外,其他的因素,比如移动行为,形状变化等等。

以上就是关于项目中动画部分的总结。

系列结语

整个项目其实整体的重点是动画的流程,怎么实现完整的动画每一帧的代码逻辑。然后拆分成两个问题,怎么渲染游戏元素,怎么设计游戏对象。

这个只是前端的一部分,还需要继续学习。加油!

Canvas 射击小游戏详解系列(五)

发表于 2018-09-05 | 分类于 HTML5 , Canvas

这篇开始介绍Canvas的内容。Canvas是属于HTML5的内容。下面就来介绍一下Canvas。

Canvas元素及画布

Canvas元素其实写法跟其他的HTML元素写法一样,比如下面的项目中的例子:

代码1 项目中Canvas元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="game" data-status="start">
<-- 其他代码 !->
<canvas id="canvas" width="700" height="600">
<!-- 动画画板 -->
</canvas>
</div>
<-- 其他代码 !->
</body>
</html>

代码1中,Canvas元素在页面上创建了一个宽700,长600的画布,这个就是Canvas实现图形的基础。

然后需要在JavaScript代码中写下这么两行代码:

代码2 获取Canvas对象

1
2
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

这段代码先是获取了canvas元素,然后在调用canvas元素的getContext方法访问获取2d渲染上下文。得到的context才能进行绘画的操作。

下面是一些关于Canvas的绘制的介绍,因为是基础内容的介绍,所以就主要以代码形式说明。

Canvas绘制

Canvas绘制分下面几种: 线跟圆弧,几何图形,文本,图片。

绘制原理

Canvas绘制的原理是把画布坐标化,以左上角为原点,上边为x坐标正方向,左边为y坐标正方向。所有的绘制方法都是context下的方法。

线跟圆弧

代码1 线跟圆弧的绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 线的绘制 */
// 设置路径
context.beginPath();
// 设置路径起始点
context.moveTo(320,470);
// 设置路径终点
context.lineTo(320,480);
// 绘制路径
context.stroke();

/* 圆弧的绘制 */
context.beginPath();
// 圆弧的绘制使用context.arc()方法
// context.arc(x, y, radius, startAngle, endAngle, anticlockwise)
// x,y为圆心坐标
// redius为圆的半径
// startAngle,endAngle分别为开始角度跟结束角度(圆的水平右侧为0°)
// anticlockwise为顺/逆时针设置,默认值为false顺时针
context.arc(250, 250, 300, 0, Math.PI, false);
// 绘制路径
context.stroke();
// 使用fill()方法将会把圆弧两端直接闭合填充
// context.fill();

几何图形

代码2 集合多边形跟圆形的绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 矩形的绘制 */
context.beginPath();
context.moveTo(320, 470);
context.lineTo(350, 470);
context.lineTo(350, 500);
context.lineTo(320, 500);
// 设置闭合路径
context.closePath();
context.stroke();
// 填充路径用fill()方法
// context.fill();
// 也可以直接使用矩形的绘制方法context.rect(x, y, width, height);
// context.rect(320, 500, 30, 30);

/* 圆形的绘制 */
context.beginPath();
//context.arc(250, 250, 300, 0, 2 * Math.PI, false);
// 使用fill()方法将填充整个圆形
// context.fill();

文本

代码3 文本的绘制

1
2
3
4
5
6
7
8
/* 文本的绘制 */
// 先设置字体样式
context.font = '25px arial';
// context.strokeText()方法将文本描边绘制
// 三个参数分别为被绘制的字符串,坐标(x,y)
context.strokeText("描边文本", 100, 100);
// context.fillText()方法将文本填充绘制
context.fillText("填充文本", 100, 100);

图片

代码4 图片的绘制

1
2
3
4
5
6
7
8
9
10
11
/* 图片的绘制 */
// 设置图片对象
var image = new Image();
// 设置图片地址
image.src = 'plane.png';
// 设置图片加载完成后的回调函数
image.onload = function () {
// 绘制图片context.drawImage(img, x, y, width, height)
context.drawImage(image, 50, 50, 60, 100)
// 裁剪图片也是用context.drawImage(image, source_x, source_y, source_width, source_height, x, y, width, heigh);
}

样式设置

代码5 绘制样式设置

1
2
3
4
5
6
7
8
9
10
11
/* 描边颜色设置 */
context.fillStyle = #999;

/* 填充颜色设置 */
context.strokeStyle = #333;

/* 线宽设置 */
context.lineWidth = (Number);

/* 设置字体样式 */
context.font = '25px arial';

Canvas擦除

代码6 绘制擦除

1
2
3
4
/* 擦除 */
// 擦除可以使用context.clearRect(x, y, width, height)方法
// 下面直接擦除整个画布
context.clearRect(0, 0, canvas.width, canvas.height);

下一篇将会介绍Canvas动画跟项目中的关于Canvas跟动画的设计。

Canvas 射击小游戏详解系列(四)

发表于 2018-09-02 | 分类于 JavaScript , 对象设计

写着写着内容有点多了,不过还是觉得这些东西记下来还是很有用的。下面开始介绍项目中对象的设计。

项目中主要对象的设计

整个项目中最重要的是游戏中的三个对象元素:飞机,怪兽,子弹。

元素分析

飞机支持在画布的底部进行左右的连续移动,移动时依赖玩家的操作,并仅支持左右键的操作。飞机还可以进行射击,玩家点击空格键,上键或Enter键会可以让飞机射击出子弹,并且支持连续射击。

上面总结的飞机行为可以得到,飞机有渲染,移动,临界判断跟射击的功能。需要的参数包括渲染时的画布对象,坐标,飞机图片,飞机尺寸,移动的速度,临界判断的画布上渲染的最大最小x坐标及射击的子弹长度跟移动速度参数。

怪兽只是在画布上部的一个区域进行从上到下蛇形自动移动。被子弹击中会有爆炸效果并消失。

上面总结的怪兽行为可以得到,怪兽有渲染,移动,临界判断的功能。需要的参数包括渲染时的画布对象,坐标,怪兽图片,爆炸图片,怪兽尺寸,移动的速度,临界判断的画布上渲染的最大最小x坐标。怪兽的下移移动不在对象中实现。

子弹通过飞机射击射出,保持一定速度向上移动,如果碰到怪兽会消失,如果一直到达画布顶部也会自动消失。

上面总结的子弹行为可以得到,子弹有渲染,移动,临界判断,清除的功能。需要的参数包括渲染时的画布对象,坐标,子弹长度,移动的速度。临界判断不在对象中设置方法。

对象设计

通过对三个元素的分析,飞机跟怪兽有可抽象成一致的属性(options属性及context画布)跟方法drawing(),move(),translate()。

options传入的参数为JSON对象,包括了大量的参数,如果对象所渲染到画布上的图片,图片渲染的坐标点,图片尺寸,在画布上的可渲染的最大最小x坐标,移动速度等。Plane父类跟Enemy子类的options传入的参数会有差异,但是做了抽象统一。

两个元素的移动可以统一实现,所以我让怪兽的构造函数继承飞机的构造函数。

Enemy类虽然继承了Plane类,但是还是有一些差异点。Plane类中还有一个创建子弹的shoot()方法,这个方法也有被Enemy类继承,但是当前项目中Enemy类的实例并没有使用这个类,是存在代码冗余的问题。但是也支持了项目进一步优化,Enemy类的实例可以进一步提高难度,增加项目的趣味。

Enemy类的方法drawing()做了重写,因为怪兽有一个爆炸的渲染判断,涉及判断是否存活(属性isLive)。

子弹的行为跟前两者不同,虽然也是一样的属性,但是方法基本需要重写,所以不做继承。

键盘对象设计

因为飞机的移动跟射击都是支持连续,如果只是在Game对象中实现键盘事件监听,实际的移动跟射击都会有卡顿感,所以把键盘事件转换成点击状态,效果上会更加顺畅。

键盘对象包括了左右上键,空格键,Enter键的状态,并把document.onkeydown跟document.onkeyup都指向了自身的方法,在放在中判断键盘按键是否按下。

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
/**
* @constructor KeyBoard
* @description 键盘监听对象
* @see The <a href="#">KeyBoard</a >.
* @example
* this.keyBoard = new KeyBoard();
*/
function KeyBoard () {
document.onkeydown = this.keydown.bind(this);
document.onkeyup = this.keyup.bind(this);
}

KeyBoard.prototype = {
pressedLeft: false, // 是否点击左键
pressedRight: false, // 是否点击右键
pressedUp: false, // 是否按了上报
pressedSpace: false, // 是否按了上报
pressedEnter: false, // 是否按了上报
keydown: function (event) {
// 判断哪个按键按下,修改按键状态
},
keyup: function (event) {
// 判断哪个按键松开,修改按键状态
}
};

其他

除了这几个对象的设计,因为还涉及对象的继承,所以还另外齐起了一个文件common.js放处理继承父类原型对象的函数inheritPrototype()。具体的介绍可以会看上一篇关于继承的介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @description 继承父类原型对象的函数
* @param {Object} subType 子类对象
* @param {Object} subType 父类对象
*/
var inheritPrototype = function (subType, superType) {
// 把父类对象的原型对象赋值给proto
var protoType = Object.create(superType.prototype);
// proto的constructor指向子类对象,进行重置
protoType.constructor = subType;
// 把子类的原型指向原型
subType.prototype = protoType;
}

关于对象中设计到的Canvas的部分,下一篇将介绍。

1…345

Listentolife

Listentolife's Blog

49 日志
19 分类
61 标签
GitHub E-Mail
© 2020 Listentolife
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
   |