props
传递数据、onChange
事件监听,来绑定表单控件的一些元素,如input
输入框,其显示内容就是通过props
传递数据来显示,同时监听input
输入框的一些事件,来达到表单当中的内容和我们React
当中state
绑定的一种效果)react
来管理的控件,而是通过dom
元素自身来维护状态的控件ref
来绑定组件(原生元素),对组件(元素)进行操作propTypes
库来对组件传入的 props
的类型进行验证(如果使用了 TS
,则可以代替 propTypes
)props
验证,它的作用是为传入的 props
进行默认值的设置dialog
vue
以及 原生html
中的 slot
(插槽)类似在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同。
一般来说,表单以及表单中的(受用户控制,可交互,即交互式元素)控件(如:input
、select
……)是页面中与 JavaScript 打交道最多的元素了。虽然我们可以通过 ref 的形式去操作它们,但是这样会比较麻烦,React.js 为我们提供了一个更好的方式把 React.js 中的数据以及逻辑与表单控件关联起来。
利用脚手架工具构建,我就不再细说,如有问题,请参考小迪之前的React文章。
举一个例子:如何操作React的表单
react-Novice03\app\src\components\FormDemo.js
import React from 'react';
import FormDemo from "./components/FormDemo";
function App() {
return (
<div className="App">
<FormDemo/>
</div>
);
}
export default App;
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
function App() {
return (
<div className="App">
<FormDemo/>
</div>
);
}
export default App;
看似以上显示很正常,但我们把数据传递到表单里面了,然后在页面上往input
框里输入值做修改。我们发现在页面上怎么修改都修改不成功!这是为什么呢?
因为它是非受控组件,下面我们模拟一下这个特性:非受控特性(默认情况下)。
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.01-1
Branch:branch2
commit description:v1.01-1-example01-1 (操作React的表单)
tag:v1.01-1
UnControl
组件其实就相当于input
标签。我们往input
里输入值,就相当于从外界传入了value
属性,假设我们在外界想修改App
中的this.state
,实际就改不了(上节课知识点)。那改不了的原因是什么呢?我们再往下看。
react-Novice03\app\src\components\UnControl.js
import React from 'react';
export default class UnControl extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
我的值是:{this.props.value}
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
<FormDemo />
<hr/>
<UnControl value={this.state.v1} />
</div>
)
}
}
export default App;
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.01-2
Branch:branch2
commit description:v1.01-2-example01-2 (模拟input非受控组件)
tag:v1.01-2
假设我们需要修改父级这个数据,我们添加事件试试:
react-Novice03\app\src\components\FormDemo.js
import React from 'react';
export default class FormDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
};
}
render() {
return(
<div>
<h2>表单</h2>
<hr/>
<input type="text" value={this.state.v1}/>
<button onClick={ () => {
this.setState({
v1: this.state.v1 + 1
})
}}>按钮</button>
</div>
);
}
}
这样是可以修改的!但是我们却不能直接修改input
框里的值!
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.01-3
Branch:branch2
commit description:v1.01-3-example01-3 (事件修改state控制表单数据)
tag:v1.01-3
那么什么叫非受控呢?
其实表单input
内部会有一个状态(私有数据),对外暴露的是一个value
的props
,但是对外接收到props
以后,会赋值给内部这个私有状态(私有数据),而内部(非受控组件)没有提供任何方法,能去修改它的值。所以这个值我们怎么也修改不了。
其实对比理解,受控组件就是外部的用户行为可以控制组件的变化,而非受控组件则正好相反。
受控组件
: 用 props 传入数据的话,组件可以被认为是受控(因为组件被父级传入的 props 控制)
非受控组件
: 数据只保存在组件内部的 state 的话,是非受控组件(因为外部没办法直接控制 state)
广义来说,页面中的任意元素都是一个独立的组件,表单控件也是,它们内部也会维护属于自己的状态(如:value,selected,checked……
),当然这些状态是由原生实现的,而非 React.js 来控制的,但是有的时候我们希望通过 React.js 来管理和维护表单控件的状态,我们把这种控件(控件)称为: 受控组件, 针对不同的组件,状态的维护方式也有所差异。
input
textarea
select
通过 state 来控制组件状态
- 创建 state 与组件的某个状态进行绑定
- 监听组件某些事件来更新 state
反着理解上例,其实对于内部数据,外部可通过props
去影响外部的数据(input
组件值的变化),但是这个时候正好有一个相反的东西,是它内部数据的变化:
下面我们仔细探究一下非受控组件
如下:假如内部的数据不绑定写死!我们还是修改不了input
框里的数据。
render() {
return(
<div>
<h2>表单</h2>
<hr/>
<input type="text" value="1"/>
<button onClick={() => {
this.setState({
v1: this.state.v1+1
})
}}>按钮</button>
</div>
);
}
相信不用演示,也可以猜到,如手动修改input框中的1,肯定是没有任何反应的。因此像input
内部都会维护自己的状态,除非你直接修改它的value
属性,导致它的页面重新渲染。它内部的数据是内部自己维护的,我们从外部(用户行为)是无法操控的。
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.02-1
Branch:branch2
commit description:v1.02-1-example02-1 (指定input固定值value仍不为所动的爱情)
tag:v1.02-1
我们再看一个例子,把state属性值,放到input
和button
中间。
我们把外部的值传入表单的内部,内部则会维护这个状态,即内部会有一个数据记录了我们传进去的初值。紧接着,我们希望表单当中的数据,能跟接受用户控制并且与外界的state
进行关联。
我们可以通过一种方式将其变为受控型组件,是其与state
相互影响。希望通过 React.js 来管理和维护表单控件的状态,我们把这种控件(控件)称为: 受控组件
希望input
中的value
能够随着state
数据的变化而变化,即两者能够互相影响,该如何去做呢?
虽然非受控组件不受用户行为控制,但是当发生用户行为时,会触发onChange
事件(注意这是原生的事件)。当这个事件触发,我们就可以搞事情了。
react-Novice03\app\src\components\FormDemo.js
import React from 'react';
export default class FormDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
};
this.changeV1 = this.changeV1.bind(this);
}
changeV1 (e) {
console.log('...', e.target.value);
}
render() {
return(
<div>
<h2>表单</h2>
<hr/>
<input type="text" value={this.state.v1} onChange={this.changeV1}/>
{this.state.v1}
<button onClick={ () => {
this.setState({
v1: this.state.v1 + 1
})
}}>按钮</button>
</div>
);
}
}
你一定会感觉很奇怪,小迪拼命往里input
里输数据,为啥表单显示的值不变,而触发事件中打印的log
中这个 e.target.value
会发生变化呢?
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.02-2
Branch:branch2
commit description:v1.02-2-example02-2 (input里输数据,为啥表单显示的值不变,而触发事件中打印的log却显示呢?)
tag:v1.02-2
我们是否掌握一个概念,叫DOM
属性,了解attribute
和property
吗?及它两者之间的差异性。我们探究一下这里的原理。
获取一下input
的value
属性的方式:两中方式
js property
(js
对象属性)html attribute
(html
属性)attribute和property.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<input type="text" value="1" />
<button>按钮button>
<script>
let input = document.querySelector('input');
let button = document.querySelector('button');
button.onclick = function() {
console.log(input.value);
console.log(input.getAttribute('value'));
}
script>
body>
html>
起初我们打印的log
两个属性值一致,后来我们改变input
框的值,发现input.getAttribute('value')
就没有同步了,这是为啥呢?
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.02-3
Branch:branch2
commit description:v1.02-3-example02-3(DOM属性,对attribute和property深入探究)
tag:v1.02-3
直接获取我们设置value
值,我们看不出去这两种的差异!但是假若我们在浏览器中手动修改value
值,就发现了两者的差异性了。这是为什么呢?
因此以上两种属性是有差异性的,第一个是js
的对象属性,第二个是html
的属性,但是英文单词(attribute和property
)不一样!它们是有映射关系的。
在dom
解析的时候,我们经常听到dom
树,dom
树是什么东西,和虚拟dom
是一样的,浏览器会在解析我们的html文档的时候,如果我们把html文档当作字符串去操作的话,会很麻烦的,因此做了一种关系叫对象映射,它就会分析html中的每一个元素的结构,然后把不同的元素转成js
中对象去表示。就相当于我们想操作界面元素的话,直接去操作对应的对象就行了。因为它们在解析过程中会有对应关系,即映射关系,然后它内部又会做一件事,当我们去操作js
当中对象的时候,它就会影响(重新渲染/重绘)我们的html。但是就如上面的代码,我们定义input
对象,并不完全等于html上的input
标签。
这里发现操作浏览器上的input
的value
值,实际操作的是js
对象,而未对html标签的value
值产生任何的影响!
但是我们还会发现一个现象,我们通过浏览器的F12工具栏中的Elements
删除input
标签,但是我们的input
的js
对象还是可以运行,并且也可以输出input.value
。因为js
中的这个对象是没有消除的,因为它是将html标签作为参考而生成的对象,并且它不等同于html元素标签。
因此回归正题,我们在浏览器里看到的value
只是html标签的中value
值,刚刷新页面的时候打印出来,两者的值一样。但是当我们修改了页面的input
框,只是修改了映射出来的js
对象的value值,并没有改变标签里的value
属性。
我们还有一种做法,直接通过F12
工具栏中的Elements
去修改input
的value值,当页面元素发生改变了以后,即当页面更新的时候,页面会动态获取,重新映射导致两个值一致了!
回到我们正文的React。
因此我们输入内容以后,onChange
事件处理函数中的e.target.value
仍然可获取。我们当前的value值是根据this.state.v1
渲染出来的的,虽然我们在内部把这个原生js
的元素的对象value
值改了,就相当于改了input.value
,但是并没有反馈到当前this.state
上。
无法修改当前this.state.v1
的值,因此我们需要在触发的事件中做处理!
react-Novice03\app\src\components\FormDemo.js
import React from 'react';
export default class FormDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
};
this.changeV1 = this.changeV1.bind(this);
}
changeV1 (e) {
this.setState({
v1: e.target.value
})
}
render() {
return(
<div>
<h2>表单</h2>
<hr/>
<input type="text" value={this.state.v1} onChange={this.changeV1}/>
{this.state.v1}
<button onClick={ () => {
this.setState({
v1: this.state.v1 + 1
})
}}>按钮</button>
</div>
);
}
}
组件这就受控了。
因此当我们操作input
改变其value
的时候,它内部会自己修改value
值了,但是又不受外界的控制(不影响外层的(props
)页面显示),我们的value
根据this.state.v1
来渲染,但内部value
的变化又不会影响this.state.v1
,所以导致最终input
框的值未发生更改。要想改,就需要传入一个onChange
事件即可。
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.02-4
Branch:branch2
commit description:v1.02-4-example02-4(成功将非受控组转化为受控组件)
tag:v1.02-4
其实默认情况下,把input
标签当成一个组件就可以了,组件内部数据状态(state
)的变化,它不会主动影响外界传进来的(原生js
)props
的,导致我们最终看到的界面就是,input
标签的value
根据this.state.v1
进行渲染,即页面上input框的值根据this.state
进行渲染,但是内部的状态变化又不会更改从外部传入的this.state.v1
,所以看到效果就是我们输入的值不会显示在input
框上。那么我们想利用React的外部props
去控制input
的数据,就只能采取事件的形式了。
修改需求:用户不管输入什么,都需要转成大写!我们只需要控制好数据,再去渲染即可。
changeV1(e) {
console.log('...', e.target.value);
this.setState({
v1: e.target.value.toUpperCase()
})
}
这里就不放案例代码,大家自己去整吧!
回归主题,我们这里讲的是非受控组件,input
标签value
值的变化,只能通过内部去改变,但是在外部用户输入等外部行为,是不能改变改value值的!而我们在onChange
事件中,改变state
的时候,会重新渲染(调用setState
方法的缘故)input
的。其实就是我们第一次将v1
传给非受控组件时页面渲染,会解析并同步js和html
的属性值,但后面我们使js
的value
变了,除非你让其重新渲染,否则标签的value
值肯定没有变化的,也不可能改变v1
的值,因为我们无法从外界直接访问state
属性的。
并且我们在起初写这个代码的时候,会发现浏览器报错!
import React from 'react';
export default class FormDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
};
}
render() {
return(
<div>
<h2>表单</h2>
<hr/>
<input type="text" value={this.state.v1}/>
{this.state.v1}
<button onClick={() => {
this.setState({
v1: this.state.v1+1
})
}}>按钮</button>
</div>
);
}
}
有一个有问题的prop
类型,你提供了一个value
属性给表单控件,但是又没提供一个onChange
的处理函数,这个时候就出问题了。
因此如果你希望处理当前问题,请提供onChange
函数。
但是假若我们后续不需要改这个input
值(只涉及一个初值即可),怎么解决这个报错问题呢?可以用defaultValue
属性。
render() {
return(
<div>
<h2>表单</h2>
<hr/>
<input type="text" defaultValue={this.state.v1}/>
{/**/}
{this.state.v1}
<button onClick={() => {
this.setState({
v1: this.state.v1+1
})
}}>按钮</button>
</div>
);
}
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.02-5
Branch:branch2
commit description:v1.02-5-example02-5(假若我们后续不需要改这个input值(只涉及一个初值即可),可以用defaultValue属性解决报错问题。)
tag:v1.02-5
同样的后面讲的textarea
和select
标签也是同样的原理。
class ControlledComponent extends React.Component {
constructor(args) {
super(args);
this.state = {
v1: '1'
};
this.changeValue = this.changeValue.bind(this);
}
changeValue({target:{value:v1}}) {
this.setState({
v1
});
}
render() {
return(
<div>
<input type="text" value={this.state.v1} onChange={this.changeValue} />
</div>
);
}
}
...
changeValue({target:{value}}) {
this.setState({
v1: value.toUpperCase()
});
}
...
textarea 与 input 类似,但是需要注意的是: 使用 value ,而不是 内容(innerHTML)
// 正确
<textarea value={this.state.v2} onChange={this.changeValue2} cols="30" rows="10"></textarea>
// 错误
<textarea onChange={this.changeValue2} cols="30" rows="10">{this.state.v2}</textarea>
select 在 React.js 中也做了一些处理,不在是通过 selected 属性来表示选中元素,而是通过 select 标签的 value 属性
<select value={this.state.v3} onChange={this.changeValue3}>
<option value="html">html</option>
<option value="css">css</option>
<option value="javascript">javascript</option>
</select>
我们还可以设置多选 select,对应的 value 就是一个数组。
option
的值存在state
的数组中,就会被选中。
...
this.state = {
v4: ['html', 'javascript']
}
...
...
changeValue4({target:{options}}) {
this.setState({
v4: [...options].filter(o=>o.selected).map(o=>o.value)
});
}
...
...
<select value={this.state.v4} onChange={this.changeValue4} multiple>
<option value="html">html</option>
<option value="css">css</option>
<option value="javascript">javascript</option>
</select>
...
radio 和下面的 checkbox 需要注意的是,受控的属性不在是 value ,而是 checked
...
this.state = {
v5: '女',
v6: ['前端', '后端'],
}
...
changeValue5(e) {
this.setState({
v5: e.target.value
});
}
changeValue6({target:{value}}) {
let {v6} = this.state;
if (v6.includes(value)) {
v6 = v6.filter(v=>v!==value);
} else {
v6.push(value)
}
this.setState({
v6
});
}
...
...
<label><input name="gender" type="radio" value="男" checked={this.state.v5==='男'} onChange={this.changeValue5} />男</label>
<label><input name="gender" type="radio" value="女" checked={this.state.v5==='女'} onChange={this.changeValue5} />女</label>
<label><input name="interest" type="checkbox" value="前端" checked={this.state.v6.includes('前端')} onChange={this.changeValue6} />前端</label>
<label><input name="interest" type="checkbox" value="后端" checked={this.state.v6.includes('后端')} onChange={this.changeValue6} />后端</label>
...
话又说回来,通过上面的学习,我们知道,每个受控组件,且不同的类型的受控组件它能控制的状态只有那么一些:value、checked,但是实际上一个组件的状态远远不止这些,比如 input 的焦点、禁用、只读 等,都是组件的状态,如果每一个状态都通过上面的方式来管理,就会特别的麻烦了。这个时候,我们就需要用其他方式来处理了:DOM
但是利用原生dom
必然会有弊端,我们举个例子看看。
需求:点击按钮,动态获取p标签内容高度。
React内是没有方法可以用的,我们还是得进行dom
操作。因此有了框架之后,就避免所有的dom
操作是不现实的,因此如果开发组件和库的话,甚至开发过程中稍微顶层一点,原生dom
操作是无法避免的。
react-Novice03\app\src\components\RefDemo.js
import React from 'react';
export default class UnControl extends React.Component {
constructor(props) {
super(props);
this.state = {
content: 'Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师'
};
this.getHeight = this.getHeight.bind(this);
}
getHeight() {
let p = document.querySelector('p');
console.log(p);
}
render() {
return(
<div>
<p style={{background: 'red', color: 'white'}}>{this.state.content}</p>
<button onClick={this.getHeight}>按钮</button>
</div>
);
}
}
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.03-1
Branch:branch2
commit description:v1.03-1-example03-1(需求:点击按钮,动态获取p标签内容高度。dom操作。)
tag:v1.03-1
以上例子,当我们点击按钮的时候,p标签已经出现在页面上了(已经渲染出来了)。但有时候组件还没渲染出来,我们就获取它的dom
节点。如在构造函数中:
constructor(props) {
super(props);
this.state = {
content: 'Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师'
};
this.getHeight = this.getHeight.bind(this);
let p = document.querySelector('p');
console.log(p);
}
此时获取就是null
,因为在constructor
执行的时候,render
方法还未执行。
因此这种dom
获取节点的方式,其实是不推荐的。
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.03-2
Branch:branch2
commit description:v1.03-2-example03-2(需求:点击按钮,动态获取p标签内容高度。dom操作弊端。)
tag:v1.03-2
React.js 提供了多种方式来获取 DOM 元素
无论是 回调 Refs 还是 React.createRef(),都需要通过一个属性 ref 来进行设置
<input ref={?} />
这种方式,我们在前面已经使用过了
class UnControlledComponent extends React.Component {
constructor(props) {
super(props);
this.selectURL = this.selectURL.bind(this);
this.getElementInfo = this.getElementInfo.bind(this);
}
selectURL() {
this.refInput.select();
}
getElementInfo() {
this.refDiv.getBoundingClientRect()
}
render() {
return (
<input ref={el => this.refInput = el} type="text" value="http://www.baidu.com" />
<button onClick={this.selectURL}>点击复制</button>
<hr/>
<button onClick={this.getElementInfo}>获取元素信息</button>
<div ref={el => this.refDiv = el} style={{width: '100px', height:'100px',background:'red'}}></div>
)
}
}
ref
可以传回调函数,其实它本身类似于回调函数。
如下代码,当React解析下面的p标签的时候,它发现这里有一个ref
属性,并且其值是一个函数,那这个函数就会执行了。并且这个回调函数会接受一个参数,我们打印可以得到,它实际是就是这个元素。那如何实现动态获取高度呢?我们不能在这里获取,因为是点击按钮以后才获取高度的。我们可以在对象里定一个自定义属性,当ref
属性被解析后,我们就赋值给该属性。当我们点击的时候,直接获取该属性下的高度即可。这样就免去了每次都需要获取dom
节点了,因为在对象中原生dom
操作,在构造函数中获取到全局对象必然是null
(刚刚讲了原因),所以用原生的话,每个函数要用到该节点都得重新获取,会显得非常麻烦和冗余。并且原生中,你万一不确定是否解析完成,就获取成null
了。而用ref
,必然是解析过后才能得到的。
react-Novice03\app\src\components\RefDemo.js
import React from 'react';
export default class UnControl extends React.Component {
constructor(props) {
super(props);
this.state = {
content: 'Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师'
};
this.getHeight = this.getHeight.bind(this);
this.refE1 = null;
}
getHeight() {
console.log(this.refEl.offsetHeight)
}
render() {
return(
<div>
<button onClick={this.getHeight}>按钮</button>
<p ref={el => {
console.log('...', el)
{
this.refEl = el;
}
}} style={{background: 'red', color: 'white'}}>{this.state.content}</p>
</div>
);
}
}
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.04
Branch:branch2
commit description:v1.04-example04(需求:点击按钮,动态获取p标签内容高度。回调Refs实现。)
tag:v1.04
该方法返回一个 ref 对象,在 jsx 通过 ref 属性绑定该对象,该对象下的 current 属性就指向了绑定的元素或组件对象
class ChildComponent extends React.Component {
constructor(props) {
super(props);
}
hello() {
console.log('ChildComponent');
}
render() {
return(
<div>
<h2>ChildComponent</h2>
</div>
);
}
}
class UnControlledComponent extends React.Component {
constructor(props) {
super(props);
this.selectURL = this.selectURL.bind(this);
this.getElementInfo = this.getElementInfo.bind(this);
this.refInput = React.createRef();
this.refDiv = React.createRef();
this.refChild = React.createRef();
}
selectURL() {
this.refInput.current.select();
}
getElementInfo() {
this.refDiv.current.getBoundingClientRect()
}
getElementInfo() {
this.refChild.current;
}
render() {
return (
<input ref={this.refInput} type="text" value="http://kaikeba.com" />
<button onClick={this.selectURL}>点击复制</button>
<hr/>
<button onClick={this.getElementInfo}>获取元素信息</button>
<div ref={this.refDiv} style={{width: '100px', height:'100px',background:'red'}}></div>
<hr/>
<ChildComponent ref={this.refChild} />
<button onClick={this.getReactComponent}>获取 React 实例对象</button>
)
}
}
import React from 'react';
export default class UnControl extends React.Component {
constructor(props) {
super(props);
this.state = {
content: 'Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师Web前端工程师'
};
this.getHeight = this.getHeight.bind(this);
this.refE1 = null;
// 自动生成帮助赋值节点的函数,但是并不是直接就是这个元素标签,得到是一个对象,对象内的current才是真正的标签对象
this.refEl2 = React.createRef();
}
getHeight() {
console.log(this.refEl2);
console.log(this.refEl2.current.offsetHeight);
}
render() {
return(
<div>
<button onClick={this.getHeight}>按钮</button>
{/* {*/
}
{/* console.log('...', el)*/}
{/* {*/}
{/* this.refEl = el;*/}
{/* }*/}
{/*}} style={{background: 'red', color: 'white'}}>{this.state.content}*/}
<p ref={this.refEl2} style={{background: 'red', color: 'white'}}>{this.state.content}</p>
</div>
);
}
}
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.05
Branch:branch2
commit description:v1.05-example05(需求:点击按钮,动态获取p标签内容高度。React.createRef()实现。)
tag:v1.05
defaultProps 可以为 Class 组件添加默认 props。这一般用于 props 未赋值,但又不能为 null 的情况
注意:defaultProps 是 Class 的属性,也就是静态属性,不是组件实例对象的属性
class MyComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
MyComponent - {this.props.max}
);
}
}
MyComponent.defaultProps = {
max: 10
}
ReactDOM.render(
,
document.getElementById('app')
);
引例
react-Novice03\app\src\components\PropsDefaultValueDemo.js
import React from 'react';
export default class PropsDefaultValueDemo extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>值 - {this.props.max}</h2>
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
<PropsDefaultValueDemo max={1} />
</div>
)
}
}
export default App;
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.06-1
Branch:branch2
commit description:v1.06-1-example06-1(属性默认值-引例)
tag:v1.06-1
组件其实就是函数,有的时候我们需要在组件内部控制外界传入的参数是否合法。并且有的时候没有传值,我们也希望其可以显示一个默认值。
可以用逻辑或
react-Novice03\app\src\components\PropsDefaultValueDemo.js
import React from 'react';
export default class PropsDefaultValueDemo extends React.Component {
constructor(props) {
super(props);
}
render() {
let max = this.props.max || 1;
return(
<div>
<h2>值 - {max}</h2>
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
<PropsDefaultValueDemo />
</div>
)
}
}
export default App;
<PropsDefaultValueDemo max={1000} />
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.06-2
Branch:branch2
commit description:v1.06-2-example06-2(属性默认值-用逻辑
或
)tag:v1.06-2
react-Novice03\app\src\components\PropsDefaultValueDemo.js
import React from 'react';
export default class PropsDefaultValueDemo extends React.Component {
/**
* 给当前组件的props设置默认值
*/
static defaultProps = {
max: 10
};
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>值 - {this.props.max}</h2>
</div>
);
}
}
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.06-2
Branch:branch2
commit description:v1.06-3-example06-3(属性默认值-defaultProps)
tag:v1.06-3
class MyComponent extends React.Component {
static defaultProps = {
max: 10
}
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>MyComponent - {this.props.max}</h2>
</div>
);
}
}
ReactDOM.render(
<MyComponent />,
document.getElementById('app')
);
有的时候,我们希望给一个非受控组件一个初始值,但是又不希望它后续通过 React.js 来绑定更新,这个时候我们就可以通过 defaultValue 或者 defaultChecked 来设置非受控组件的默认值
<input type="text" defaultValue={this.state.v1} />
<input type="checkbox" defaultChecked={this.state.v2} />
<input type="checkbox" defaultChecked={this.state.v3} />
随着应用的不断增长,也是为了使程序设计更加严谨,我们通常需要对数据的类型(值)进行一些必要的验证,React.js 提供了一个验证库:prop-types
主要是对传入对props
参数对数据类型进行安全(合法性)验证,主要进行类型验证。不过还是推荐使用typescript
做验证,它的功能更为强大,官方也是推荐使用ts
。但两者是有差异的,ts
是在编译过程中作类型检测,prop-types是针对代码层面的,还会附件一些功能。
prop-types 是一个独立的库,需要安装
https://www.npmjs.com/package/prop-types
npm i -S prop-types
import PropTypes from 'prop-types';
它的使用并不复杂,与 defaultProps 类似,我们在组件类下添加一个静态属性 propTypes ,它的值也是一个对象,key 是要验证的属性名称,value 是验证规则
MyComponent.propTypes = {
// You can declare that a prop is a specific JS primitive. By default, these
// are all optional.(提供的验证函数如下)
optionalArray: PropTypes.array, // 是不是数组
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func, // 是不是函数
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object, // 是不是对象
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
// Anything that can be rendered: numbers, strings, elements or an array
// (or fragment) containing these types.
optionalNode: PropTypes.node, // 是不是node节点
// A React element (ie. ).
optionalElement: PropTypes.element, // 是不是元素
// A React element type (ie. MyComponent).
optionalElementType: PropTypes.elementType,
// You can also declare that a prop is an instance of a class. This uses
// JS's instanceof operator.
optionalMessage: PropTypes.instanceOf(Message), // 是不是某个对象
// You can ensure that your prop is limited to specific values by treating
// it as an enum.
optionalEnum: PropTypes.oneOf(['News', 'Photos']), // 值是否是该数组中的其中之一
// An object that could be one of many types
optionalUnion: PropTypes.oneOfType([ // 当前类型是否是其中之一
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// An array of a certain type 是否包含其中
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// An object with property values of a certain type 是否包含其中
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
// You can chain any of the above with `isRequired` to make sure a warning
// is shown if the prop isn't provided.
// An object taking on a particular shape
optionalObjectWithShape: PropTypes.shape({
optionalProperty: PropTypes.string,
requiredProperty: PropTypes.number.isRequired
}),
// An object with warnings on extra properties
optionalObjectWithStrictShape: PropTypes.exact({
optionalProperty: PropTypes.string,
requiredProperty: PropTypes.number.isRequired
}),
requiredFunc: PropTypes.func.isRequired, // 代表必传参,不能省略
// A value of any data type
requiredAny: PropTypes.any.isRequired,
// You can also specify a custom validator. It should return an Error
// object if the validation fails. Don't `console.warn` or throw, as this
// won't work inside `oneOfType`.(自定义规则<常用>)
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error( // 错误提示可以自己编写
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
},
// You can also supply a custom validator to `arrayOf` and `objectOf`.
// It should return an Error object if the validation fails. The validator
// will be called for each key in the array or object. The first two
// arguments of the validator are the array or object itself, and the
// current item's key.
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
};
使用演示。
react-Novice03\app\src\components\PropTypesDemo.js
import React from 'react';
import PropTypes from 'prop-types';
export default class PropTypesDemo extends React.Component {
static propTypes = {
// 会把props的值传给PropTypes.number函数,对其进行数字验证。如果没满足要求则抛出一个错误。
max: PropTypes.number
};
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>值 - {this.props.max}</h2>
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
import PropTypesDemo from "./components/PropTypesDemo";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/* */}
<PropTypesDemo max={10} />
</div>
)
}
}
export default App;
<PropTypesDemo max={'csdn'} />
报错:有一个不可接受的(失败的)props
类型数据,string
类型的,但我们只允许number
。
必传参,不能省略
max: PropTypes.any.isRequired
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.07-1
Branch:branch2
commit description:v1.07-1-example07-1(props验证测试)
tag:v1.07-1
需求:max
的值必须在10-100之间
react-Novice03\app\src\components\PropTypesDemo.js
import React from 'react';
import PropTypes from 'prop-types';
export default class PropTypesDemo extends React.Component {
static propTypes = {
// props对象 propName:props名称 componentName 组件名称
max(props, propName, componentName) {
console.log('....');
let v = props[propName]; // 取值方式
console.log(v);
if (v < 10 || v > 100) {
throw new RangeError('max的值必须在10-100之间');
}
}
};
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>值 - {this.props.max}</h2>
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
import PropTypesDemo from "./components/PropTypesDemo";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
<PropTypesDemo max={9} />
{/* */}
{/* */}
</div>
)
}
}
export default App;
<PropTypesDemo max={19} />
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.07-2
Branch:branch2
commit description:v1.07-2-example07-2(props验证测试——需求:max的值必须在10-100之间)
tag:v1.07-2
一个组件通过 props 除了能给获取自身属性上的值,还可以获取被组件包含的内容,也就是外部子组件,前面我们写的组件更多的是作为一个单标签组件,实际应用中很多组件是双标签的,也就是可以包含内容的,也可称为:容器组件,那么组件包含的内容,我们就可以通过 props.children 来获取
.dialog {
position: fixed;
left: 50%;
top: 30%;
transform: translateX(-50%) translateY(-50%) ;
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0,0,0,.3);
box-sizing: border-box;
background: #fff;
width: 60%;
}
.dialog_header {
padding: 20px 20px 0;
text-align: left;
}
.dialog_title {
font-size: 16px;
font-weight: 700;
color: #1f2d3d;
}
.dialog_content {
padding: 30px 20px;
color: #48576a;
font-size: 14px;
text-align: left;
}
.dialog_close_btn {
position: absolute;
right: 10px;
top: 5px;
}
.dialog_close_btn:before {
content: 'x';
color: #999;
font-size: 20px;
cursor: pointer;
}
import React from 'react';
import './dialog.css';
export default class Dialog extends React.Component {
static defaultProps = {
title: '这是默认标题'
}
render() {
return(
<div className="dialog">
<i className="dialog_close_btn"></i>
<div className="dialog_header">
<span className="dialog_title">{this.props.title}</span>
</div>
<div className="dialog_content">
{this.props.children}
</div>
</div>
);
}
}
需求:模拟对话框
实现框子
对话框样式,可以自己完善,或者参考小迪github
上的源码。
react-Novice03\app\src\components\ChildrenDemo.js
import React from 'react';
import './dialog.css';
export default class ChildrenDemo extends React.Component {
static defaultProps = {
title: '这是默认标题',
content: '这是默认的内容'
}
render() {
console.log(this.props);
return(
<div className="dialog">
<i className="dialog_close_btn"></i>
<div className="dialog_header">
<span className="dialog_title">{this.props.title}</span>
</div>
<div className="dialog_content">{this.props.content}</div>
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
import PropTypesDemo from "./components/PropTypesDemo";
import ChildrenDemo from "./components/ChildrenDemo";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
<ChildrenDemo title={'CSDN'} content={"https://mp.csdn.net/"}/>
</div>
)
}
}
export default App;
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.08-1
Branch:branch2
commit description:v1.08-1-example08-1(需求:模拟对话框——实现框子)
tag:v1.08-1
如果对话框中内容放入一个表单
<ChildrenDemo title={'CSDN'} content={
<form>
<p>
用户名:<input type="text"/>
</p>
</form>
}/>
如果我们还想往里嵌套组件,参数写起来就像之前的递归一样,一层一层可读性极差!
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.08-2
Branch:branch2commit description:v1.08-2-example08-2(需求:模拟对话框——嵌套表单)
tag:v1.08-2
实际上结构不要写在属性上,可以把当成容器一样使用。这就更类似于我们平时写的html了,可读性会更好。
<ChildrenDemo title={'CSDN'}>
<form>
<p>
用户名:<input type="text"/>
</p>
</form>
</ChildrenDemo>
但是在页面中并不存在此结构,貌似并没有将其放入props
。这其实就是我们常说的影子dom
和子元素
之间的差异性了。ChildrenDemo
的背后其实是我们在src/components/ChildrenDemo.js
中的render
中返回值,而在这里包含的是我们所写的表单标签。
假设ChildrenDemo
是一个盒子,而src/components/ChildrenDemo.js
中的render
中返回值就是修饰盒子的边框,这里的我们所写的表单标签就是盒子里存放的物品(子元素),两者不是一套东西。
我们看到log
中,其实这些物品是放在props
的children
属性中,这其实是一个虚拟dom
节点(其实就是把这些物品解析成虚拟dom
)。
再修改代码:
react-Novice03\app\src\components\ChildrenDemo.js
import React from 'react';
import './dialog.css';
export default class ChildrenDemo extends React.Component {
static defaultProps = {
title: '这是默认标题',
content: '这是默认的内容'
}
render() {
console.log(this.props);
return(
<div className="dialog">
<i className="dialog_close_btn"></i>
<div className="dialog_header">
<span className="dialog_title">{this.props.title}</span>
</div>
<div className="dialog_content">{this.props.children ? this.props.children : this.props.content}</div>
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
import PropTypesDemo from "./components/PropTypesDemo";
import ChildrenDemo from "./components/ChildrenDemo";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
<ChildrenDemo title={'CSDN'}>
<form>
<p>
用户名:<input type="text"/>
</p>
</form>
</ChildrenDemo>
</div>
)
}
}
export default App;
这其实是经常会用到的,假若我们写一个组件,我们不可能把组件的所有内容都能够定义好,很多时候,这个组件其实是一个容器型组件,它里边还可以放很多其他东西,这个时候可由外部决定。有两种方式,第一种直接传参,但有的时候结构可能会很复杂,传参会很麻烦(可读性极差),我们就可以用类似html的嵌套形式即可(解析为虚拟dom
放在children
属性里)。
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.08-3
Branch:branch2commit description:v1.08-3-example08-3(需求:模拟对话框——最终版)
tag:v1.08-3
实现一个可拖拽的div
元素组件,即扩展为选择哪个元素,就可以进行拖拽。如第三方库不具备此特性,我们怎样将其加工为可拖拽呢(禁止更改其源码)?
类似于面向对象的设计模式—装饰者模式:通过一种无侵入式的方式(不需要修改此对象的本身,而对这个对象进行功能等扩展,得到一个具有新特性的对象),来扩展某个元素的特性。
react-Novice03\app\src\components\Drag.js
import React from 'react';
export default class Drag extends React.Component {
constructor(props) {
super(props);
}
render() {
// 被拖拽的元素
let el = this.props.children;
console.log(el);// 虚拟dom对象,不是原生js对象
return(
<div>
{this.props.children}
</div>
);
}
}
react-Novice03\app\src\App.js
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
import PropTypesDemo from "./components/PropTypesDemo";
import ChildrenDemo from "./components/ChildrenDemo";
import Drag from "./components/Drag";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/**/ }
{/* }
{/* */
}
{/* 用户名:*/}
{/* */}
{/* */}
{/**/}
{/*装饰者*/}
<Drag>
<div ref={el => {
console.log(el); // 解析过后的元素
}} style={{
width: '100px',
height: '100px',
position: 'absolute',
background: 'red',
}}></div>
</Drag>
</div>
)
}
}
export default App;
虚拟dom
对象里有一个ref属性,这里会解析成真实dom
,我们来完善拖拽!
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.09-1
Branch:branch2commit description:v1.09-1-example09-1(需求:React实现拖拽——框子)
tag:v1.09-1
关于拖拽原理小迪不再重复了,请参考小迪的详细探究拖拽原理的博客。
推荐 Event事件学习实用路线(10)——Event事件之拖拽原理思路详解
import React from 'react';
import FormDemo from "./components/FormDemo";
import UnControl from "./components/UnControl";
import RefDemo from "./components/RefDemo";
import PropsDefaultValueDemo from "./components/PropsDefaultValueDemo";
import PropTypesDemo from "./components/PropTypesDemo";
import ChildrenDemo from "./components/ChildrenDemo";
import Drag from "./components/Drag";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
v1: 1
}
this.moveElemnt = this.moveElemnt.bind(this);
}
moveElemnt(el) {
let startPos = {} // 1. 鼠标点击的位置
let boxPos={} // 2. 元素的初始位置
el.addEventListener("mousedown", (e)=>{
// 保存
// 初始鼠标位置
startPos.x = e.clientX;
startPos.y = e.clientY;
// 元素的初始位置
boxPos.x = parseFloat(getComputedStyle(el).left);
boxPos.y = parseFloat(getComputedStyle(el).top);
document.addEventListener("mousemove", drag);
let i = 1;
el.addEventListener("mouseup", ()=>{
console.log(i++);
document.removeEventListener("mousemove", drag);
},{
// 只绑定一次事件
once:true
});
});
function drag(e){
let nowPos = {
x : e.clientX,
y : e.clientY
}
let dis = {
x : nowPos.x - startPos.x,
y : nowPos.y - startPos.y
}
let newBoxPos = {
left : boxPos.x + dis.x,
top : boxPos.y + dis.y
}
// 限制左侧
if (newBoxPos.left < 0){
newBoxPos.left = 0;
}
// 限制右侧
let maxLeft = document.documentElement.clientWidth - el.offsetWidth;
if (newBoxPos.left > maxLeft){
newBoxPos.left = maxLeft;
}
// 限制上侧
if (newBoxPos.top < 0){
newBoxPos.top = 0;
}
// 限制下侧
let maxTop = document.documentElement.clientHeight;
if (newBoxPos.top > maxTop) {
newBoxPos.top = maxTop;
}
el.style.top = newBoxPos.top + 'px';
el.style.left = newBoxPos.left + 'px';
}
}
render() {
return (
<div className="App">
{/* */}
{/*
*/}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/* */}
{/**/ }
{/* }
{/* */
}
{/* 用户名:*/}
{/* */}
{/* */}
{/**/}
{/*装饰者*/}
<Drag>
<div ref={el => {
console.log(el); // 解析过后的元素
this.moveElemnt(el);
}} style={{
width: '100px',
height: '100px',
position: 'absolute',
background: 'red',
}}></div>
</Drag>
</div>
)
}
}
export default App;
参考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.09-2
Branch:branch2
commit description:v1.09-2-example09-2(需求:React实现拖拽——最终版)
tag:v1.09-2