库和框架对比

  • library(库):小而巧,只提供特定API,可以很简单的从一个库切换到另一个库,但是代码几乎不变
  • FrameWork(框架):大而全,提供一整套的解决方案

模块化和组件化对比

模块化:

  • 代码的角度来进行分析的
  • 把一些可复用的代码,抽离成单个模块,便于项目的维护和开发

组件化:

  • UI界面的角度来进行分析的

  • 把一些可复用的UI元素,抽离成单独的组件,便于项目的维护和开发

  • 随着项目的增大,手里的组件越来越多,很方便的就能把现有的组件拼接成一个完成的页面

Vue是如何实现组件化的

通过.vue文件来创建对应的组件

  • template:结构
  • script:行为
  • style:样式

React是如何实现组件化的

  • React有组件化的概念,但是,并没有像Vue这样的组件模版文件;React中,一切都是以JS来表现的
  • 也就是说,结构,行为,样式,都是使用JS来实现的

React中核心的概念

虚拟DOM

  • Virtual Document Object Model
  • Dom的本质:是浏览器中的概念,用JS对象来表示页面上的元素,并提供了操作DOM对象的API
  • React中的虚拟DOM:是框架中的概念,程序员用JS对象来模拟页面上的DOM和DOM嵌套
  • 为什么要使用虚拟DOM(虚拟DOM的目的):为了实现页面中DOM元素的高效更新

DOM的按需更新

  1. 现在有一个每列都支持排序的表格,点击表头即可进行排序
  2. 一般的做法都是将数据以对象数组的形式储存在浏览器中,一旦用户点击就对对象数组进行排序,然后进行重新更新
  3. 但是有点排序可能只是交换了两行数据,并不是大量行进行重新排序,这样就会带来性能问题
  4. 这样就需要按需更新

如何实现按需更新

  • 获取到内存中两个新旧两颗DOM树,进行对比。得到需要更新的DOM元素
  • 浏览器中,并没有直接提供获取DOM树的API,因此我们无法拿到浏览器内存中的DOM树。
  • 但是程序员可以使用JS对象手动模拟两颗DOM树 , 这两颗手动模拟的DOM树,就是React中虚拟DOM的概念

使用JS模拟DOM树:

1
2
3
4
5
6
<div id="mydiv" title="mytitle" data-index=0>
hello world
<p>
hhh
</p>
</div>

使用JS对象表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var div = {
tagName:"div",
attrs:{
id:"mydiv",
title:"mytitle",
"data-index":0
},
childrens:[
"hello world",
{
tagName:"p",
attrs:{},
childrens:[
'hhh'
]
}
]
}

Diff算法

  • DOM树是每一层组成的
  • 一层是由每一个组件组成的
  • 一个组件是由每一个元素组成的

tree diff

  • 逐层对比两个DOM
  • 当逐层对比完毕,所有需要被更新的DOM元素必然能被找到

component diff

  • 在tree diff的时候,每一层中组件级别的对比
  • 如果对比前后,组件的类型相同,则暂时认为此组件不需要被更新
  • 如果对比前后,组件的类型不同,则需要移除旧组件,创建新组件,并追加到页面上

element diff

  • 如果进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比

react使用

1
npm i react react-dom -S
  • React:专门用于创建组件和虚拟DOM的,同时组件的生命周期都在这个包中
  • react-dom:专门进行DOM操作的,最主要的应用场景就是ReactDOM.render()

基本使用

1
2
<!-- 创建一个容器,将来渲染的虚拟DOM会放在容器内显示 -->
<div id="app"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建组件,虚拟DOM元素,生命周期
import React from 'react'

// 把创建好的组件和虚拟DOM放在页面上展示
import ReactDOM from 'react-dom'

//创建虚拟DOM元素
// 参数1:元素类型
// 参数2:一个对象或者null,表示当前这个DOM元素的属性
// 参数3及以上:子节点(包括其他虚拟DOM,获取文本子节点)
// <div>this is a div<h1 id='myh1' title='this is a h1'>hello world</h1></div>
const myh1 = React.createElement('h1',{id:'myh1',title:'this is a h1'},'hello world')
const mydiv = React.createElement('div',null,'this is a div',myh1)

// 使用ReactDOM吧虚拟DOM渲染到页面上
// 参数1:要渲染的那个虚拟DOM元素
// 参数2:指定页面上的DOM元素作为一个容器
ReactDOM.render(mydiv,document.getElementById('app'))

JSX

安装babel插件

1
2
npm i babel -core babel-loader babel-plugin-transform-runtime -D
npm i babel-preset-env babel-preset-stage-0 -D

安装能是被转换jsx语法的包

1
npm i babel-preset-react -D
  • 上面的写法太过复杂,可以使用babel来转换HTML标签

  • 这种在JS中混合可写入类似与HTML的语法,叫做JSX,也就是符合XML规范的JS

    • JSX本质还是在运行的时候转成React.createElement来执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const myh1 = React.createElement('h1',{id:'myh1',title:'this is a h1'},'hello world')
ReactDOM.render(mydiv,document.getElementById('app'))

//改成JSX
const mydiv = <div id="myid" title="this is a div">hhh</div>
ReactDOM.render(<div>123</div>,document.getElementById('app'));

// 甚至还可以使用变量和表达式
let a = 10;
const arr = [
<h1>this is h1</h1>
<h2>this is h2</h2>
]
ReactDOM.render(<div>{ a }</div>,document.getElementById('app'))
ReactDOM.render(<div>{ a + 2 }</div>,document.getElementById('app'))
ReactDOM.render(<div>{ arr }</div>,document.getElementById('app')) // 会自动渲染出h1,h2

外部循环

1
2
3
4
5
6
7
8
9
const arrName = ['czj','hyl','gzr','dsz']
const res = []

arrName.forEach(item => {
const temp = <h5>{item}</h5>
res.push(temp)
})

ReactDOM.render(<div>{ res }</div>,document.getElementById('app'))

内部循环

  • 这里的key和vue中的key一摸一样
  • 需要注意的是,react中的key只能加在map控制的第一层元素
1
2
3
4
5
6
7
8
9
10
11
const arrName = ['czj','hyl','gzr','dsz']

ReactDOM.render(<div>{ arrName.map(item =>{
return <h2 key={item}>{item}<h2>
}) }
</div>,document.getElementById('app')
{/* 这是JSX的注释 */}
{
// 这也是JSX的注释
}
)

JSX的属性名冲突

  • class变成className
  • for 变成htmlFor
1
2
ReactDOM.render(<div className="aaa"><label htmlFor='bbb'></label></div>,
document.getElementById('app'))

JSX根节点

使用JSX创建的节点,必须是由一个根节点包裹起来

JSX的编译时的解析原则

  • 遇到<>就把它当成html标签编译
  • 遇到{}就把它内部代码当成JS代码编译

react创建组件的两种方式

使用构造函数来创建

  • 如果要接收外界传递的数据,需要在构造函数的参数列表中使用props来接受,
  • 构造函数必须首字母大写
  • 构造函数必须要向外return一个合法的JSX创建的虚拟DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 构造函数首字母必须大写
function Hello(props){
// 如果在一个尊见中return一个null,表示此组件是空的,什么都不会渲染
// return null

// 在组件中,必须返回一个合法的JSX虚拟DOM元素
return <div name={props.name}>这是一个合法的hello组件</div>
}

const dog = {
name :'dogName',
age:3,
gander:'famal'
}

React.render(<div>
123
{/* 直接把组件的名称以标签的形式丢到页面上即可 */}
<Hello name={dog.name} age={dog.age}></Hello>
</div>,
document.getElementById('app')
)

使用ES6的特性 : 展开运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
const dog = {
name :'dogName',
age:3,
gander:'famal'
}

React.render(<div>
123
{/* 直接把组件的名称以标签的形式丢到页面上即可 */}
<Hello {...dog}></Hello>
</div>,
document.getElementById('app')
)

使用class关键字来创建

  • 在class组件内部,this表示当前组件的实例对象
  • 在class关键字创建的组件中,如果需要外部传递props参数,不需要接收,直接使用this.props.XXX访问即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Hello extends React.Component {
render(){
// render函数作用:渲染当前组件对应的虚拟DOM元素
// render函数中,必须返回合法的JSX虚拟DOM结构
return <div name={this.props.name}>这是class创建的组件</div>
}
}

const dog = {
name :'dogName',
age:3,
gander:'famal'
}

React.render(<div>
<Hello name={dog.name} age={dog.age}></Hello>
<Hello {...dog}></Hello>
</div>,
document.getElementById('app')
)

两种方法的区别

  • 使用构造函数创建出来的组件,叫做无状态组件,使用class关键字创建的组件,叫做有状态组件

    无状态组件和有状态组件的区别就是:

    • 有没有state属性
    • 有没有生命周期函数
  • 使用class创建的组件拥有自己的私有数据,但是function创建的组件,只有props,没有自己的私有数据和生命周期函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Hello extends React.Component {
constructor(){
super()
// 这个this.state = {} 就相当于 Vue 中的data(){ return {} }
// 这是state是可读可写的,可以在render函数中使用
this.state = {
msg : '我是class创建的Hello组件'
}
}

render(){
this.state.msg = "可以修改state的值"
return <div name={this.props.name} title={this.state.msg}>这是class创建的组件</div>
}
}

props和state之间的区别

  • 从数据来源上看,props的数据都是外界传递过来的,state的数据是组件私有的
  • props中的数据都是只读的,不能重新赋值;state中的数据,都是可读可写的

使用函数式组件的优点

  • 模块化代码 — 可以在整个项目范围内复用你的代码段;
  • 只依赖于 props — 默认没有 state;
  • 更便于单元测试 — 对测试工具 enzyme/jest 更友好的测试接口;
  • 更便于 Mock 数据 — 可以对不同场景方便的进行数据 Mock。

评论列表案例

基本架构

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
// 评论项组件
function CommentItem(props) {
return <div>
<h1>评论人:{props.user}</h1>
<p>评论内容:{props.content}</p>
</div>)
}

// 评论列表组件
class CommentList extends React.COmponent{
constuctor(){
super()
// 评论列表
this.state = {
CommentList : [
{'id':1,user:'hyl',content:'content1'},
{'id':2,user:'dsz',content:'content2'},
{'id':3,user:'czj',content:'content3'},
{'id':4,user:'grz',content:'content4'},
]
}
}

render(){
return <div>
<h1>这是评论列表</h1>
{this.state.CommentList.map(item => <CommentItem key={props.id} {...item}></CommentItem>}
</div>
}
}

组件中的style

  • JSX中,如果要写行内样式,style属性的值不能是字符串,应该是一个对象
  • { { color:"red",fontSize:"25px" } }:第一层括号表示里面的是JS代码,第二层括号表示这是一个对象
1
2
3
4
5
6
function CommentItem(props) {
return <div>
<h1 style={ { color:"red",fontSize:"25px" } }>评论人:{props.user}</h1>
<p>评论内容:{props.content}</p>
</div>)
}

一般会把对象提取出来:

1
2
3
4
5
6
7
8
9
10
11
12
// 一般会把itemStyle单独放在一个文件中
const itemStyle = {
user:{ color:"red",fontSize:"25px" },
content:{ color:"green",fontSize:"40px" },
}

function CommentItem(props) {
return <div>
<h1 style={ itemStyle.user }>评论人:{props.user}</h1>
<p style={ itemStyle.content }>评论内容:{props.content}</p>
</div>)
}

CSS文件模块化

1
2
3
4
// css/commentitem.css
.title{
color:red
}
1
2
3
4
5
6
7
8
9
// 直接导入的css是全局的,会作用于整个项目
import '@/css/commentitem.css'

function CommentItem(props) {
return <div>
<h1 clssName='title'>评论人:{props.user}</h1>
<p>评论内容:{props.content}</p>
</div>)
}

我们可以开启全局模块化,把CSS文件当成模块暴露出去,接着我们这里就可以使用cssobj对象了

1
2
3
4
5
6
7
8
import cssobj from '@/css/commentitem.css'

function CommentItem(props) {
return <div>
<h1 clssName={cssobj.title}>评论人:{props.user}</h1>
<p>评论内容:{props.content}</p>
</div>)
}

注意:css模块化只针对类选择器ID选择器生效,不会对标签选择器生效

React中绑定事件

  1. 事件名称都是React提供的

  2. 为事件提供处理函数格式如下

    1
    onClick = { function }
  3. 用的最多的事件绑定形式为

    1
    2
    3
    4
    5
    6
    <button onClick={ ()=> this.show('传参') }>按钮</button>

    // 事件的处理函数,需要定义为一个箭头函数,然后赋值给函数名称
    show = (arg1) => {
    console.log('show方法' + arg1)
    }
  4. 在React中,如果想要修改state的数据,推荐使用this.setState( {} )

    1. 注意this.setState( {} )是异步的

    2. 如果要想编写同步代码,就可以使用this.setState( {} )的第二参数:回调函数

      1
      2
      3
      4
      5
      6
      myClickHandler = (name)=> {
      this.setState(
      {name:name},
      ()=>{console.log(name)}
      )
      }

基本使用:行内写函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'

class BindEvent extends React.Component {
constructor(){
super()
this.state = {}
}

render(){
return <div>
BindEvent组件
{ /* 在React中,有一套自己的事件绑定机制 */ }
<button onClick={ function(){console.log("ok")} }>按钮</button>
</div>
}
}

进阶使用:将函数提取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react'

class BindEvent extends React.Component {
constructor(){
super()
this.state = {}
}

myClickHandler(){
console.log('22222')
}

render(){
return <div>
BindEvent组件
<button onClick={ this.myClickHandler }>按钮</button>
</div>
}
}

最常使用:使用箭头函数

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
import React from 'react'

class BindEvent extends React.Component {
constructor(){
super()
this.state = {
name:'czj'
}
}

myClickHandler = (name)=> {
// 在函数内部修改state的值
// 但是不建议使用this.state.msg = "值被修改了"
// 注意:setState是异步的,要通过使用回调函数来实现同步的代码
this.setState(
{name:name},
()=>{console.log(name)}
)
}

render(){
return <div>
BindEvent组件
{ this.state.name }
<button onClick={ ()=> this.myClickHandler('hyl') }>按钮</button>
</div>
}
}

React的state和页面元素的单项绑定

  • 单项绑定是指state值和页面元素的单项绑定,即state -> 页面元素。state的值的变化能自动导致页面元素发生改变,但是页面的改变不会对state的值造成影响。(这里的自动是指不做任务多余操作,如果你添加了监听函数当然就属于例外)
  • 默认情况下,在React中,如果页面上的表单元素绑定了state上的状态值,那么,每当state上的状态值改变时,必然会自动把最新的状态值自动同步到页面上

使用readOnly属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react'

class BindEvent extends React.Component {
constructor(){
super()
this.state = {
name:'czj'
}
}

render(){
return <div>
// 渲染时input的value就是csj
// 但是因为React是单向绑定,所以会报一个警告。说我们没有没有提供一个onChange函数来监听input里的值,然后来修改State
// 此时有两种解决方法,一种使用readOnly属性来说明State不再改变;一种添加onChange函数来修改State
<input type="text" style={{ width:"100%" }} value={this.state.name} readOnly></input>
</div>
}
}

使用onChange处理函数

在onChange函数中,获取文本框的值有两种方案:

  • 通过事件参数e来获取
  • 通过ref属性获取
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
import React from 'react'

class BindEvent extends React.Component {
constructor(){
super()
this.state = {
name:'czj'
}
}

txtChange = (e) => {
// 在onChange函数中,获取文本框的值有两种方案:
// 1.通过 事件参数e 来获取
const newVal = e.target.value
this.setState({
name:newVal
})

// 2.通过 ref属性 获取
const newVal = this.refs.mytxt.value
this.setState({
name:newVal
})
}

render(){
return <div>
// 这里的ref属性和Vue的$ref属性一样,都是获取 元素的引用
<input type="text" style={{ width:"100%" }} value={this.state.name} onChange={(e)=>{this.txtChange(e)} ref=“mytxt”}></input>
</div>
}
}

Vue的生命周期

  • 什么是生命周期:

    • 一个组件从创建到被销毁的全部时间就是生命周期
  • 什么是生命周期函数:

    • RPG中,1-4级角色有1个装备栏,5级角色有2两个装备栏。那么,当角色A从4级变到5的瞬间,就会执行某个函数,导致你的装备栏的容量加一。那么这个函数就是生命周期函数。
    • 也就是说,当组件从某一个状态转变成另一个状态的时候,会调用的函数,就是生命周期函数。

生命周期:

每个组件的实例,从创建到运行,直到销毁,在这个过程中,会发出一些事件,这些事件就叫做组件的生命周期函数。

  1. new Vue():这时new了一个Vue实例对象,此时,就会进入组件的创建过程

  2. init Event & LifeCycle:初始化组件的生命周期函数,当执行完这一步之后,组件的生命周期函数就已经全部初始化好了,等待着依次去调用

    beforeCreate:这是第一个生命周期函数,此时组件的data和methods以及页面DOM结构都还没有初始化,所以此阶段什么都做不了

  3. init injections & reactivity:这个阶段中,正在初始化date和methods中的数据以及方法。(此时DOM还没有被渲染)

    created:这个组件创建阶段第二个生命周期函数,组件的data和methods已经可用了。但是页面还没有渲染出来。在此阶段经常会发起AJax请求

  4. has ”el“ option?:判断是否有el属性

  5. has “template”?:判断是否有template属性

    • yes:compile template into render function
    • No:compile el’s outerHTML as template
    • 4和5步骤正在编译模版部分,把data上的数据拿到,并且解析执行模版结构中的指令,当所有指令被解析完毕,那么模版就被渲染到内存中
    • 当模版 编译完成后,模版页面还没有挂载到页面上,只是存在于内存中,所以5步骤执行后,用户还是看不到页面

    beforeMount:当模版在内存中编译完成,会执行实例创建阶段的第三个生命周期函数。此时内存中的模版结构还没有真正渲染到页面上,此时页面看不到真实的数据。

  6. Crete vm.$el and replace “el” with it:把内存中渲染好的模版结构替换到真实页面上

Mounted:组件创建阶段的最后一个生命周期函数。此时页面已经真正的渲染完毕了,用户可以真正看到页面了。当这个生命周期函数执行完毕之后,组件就离开了创建阶段,进入到运行中阶段。

如果用到了第三方UI插件,而且需要初始化插件,那么必须在这个生命周期函数进行初始化

  1. mounted:已经将数据挂载到页面上。

    Beforeupdate:组件运行中的生命周期函数。会根据data 的数据变化,有选择性的触发0次或者N次。

    当执行此生命周期的时候,数据是最新的,但是页面是旧的。

  2. Virtual DOM re-render and patch:正在根据最新的data数据,重新渲染模版中的数据结构,并把渲染好的模版结构替换到页面上。

    Updated:组件运行中的生命周期函数。此时,数据是最新的,同时页面也是新的。

    当data数据变化时,页面更新的步骤:

    1. 拿到最新的data数据
    2. 根据最新的data数据,在内存中重新渲染一颗新的DOM树
    3. 把旧的页面移除,同时把新的DOM树渲染出来

    beforeDestory:当执行此生命周期函数时,组件即将被销毁,但是还没有真正开始销毁。此时组件还是正常使用。data,methods还是可以正常使用

  3. TearDown watcher.child componenets and event listeners:销毁组件的过程

  4. Destoryed:组件已经销毁

    destoryed:组件已经完成销毁。此时组件的data和methods已经不可使用分

React组件的生命周期

分成三部分

  1. 组件创建阶段:一辈子只执行依次
    1. componentWillMount
    2. render
    3. componentDidMount
  2. 组件运行阶段:按需,根据props属性或state状态的改变,有选择性的执行0到多次
    1. componentWillReceiveProps
    2. shouldComponentUpdate
    3. componentWillUpdate
    4. render
    5. componentDidupdate
  3. 组件销毁阶段:一辈子只执行一次
    1. componentWillUnmount