文章目录
1、基本理解和使用
2、组件三大核心属性:state
3、组件三大核心属性:props
4、组件三大核心属性:ref
5、受控组件和非受控组件
6、高阶函数和函数柯里化
7、组件的生命周期
8、虚拟DOM与DOM Diffing算法
一、基本理解和使用
1、函数式组件(适用于简单组件<无状态组件>)
// 创建函数式组件
function MyComponent() {
// 此处的this是undefined,因为babel编译后开启了严格模式
console.log(this);
return <h2>函数式组件(简单)</h2>
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
2、类式组件(适用于复杂组件<有状态组件>)
// 创建类式组件
class MyComponent extends React.Component {
// 类中的构造器不是必须要写的,要对实例进行初始化的操作,如添加指定属性时才填写
constructor(props) {
// super代表的是父类的构造函数,但它内部的this指向的是当前子类MyComponent的实例对象
// super只能在子类的构造函数中调用,且有继承父类的话,必须调用
// 子类未定义constructor时,super方法会被默认添加
// ES5 的继承的实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))
// ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
super(props);
this.name = props.name;
this.age = props.age;
}
// 类中定义的方法,都是放在原型对象上,供实例去使用
speak() {
console.log(`我叫${
this.name},年龄${
this.age}`)
}
render() {
// render是放在MyComponent的原型对象上,供实例使用
// this指向的是MyComponent的实例对象
console.log('render中的this', this);
return <h2>类式组件(复杂组件)</h2>
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
3、渲染组件的基本流程
- 执行ReactDOM.render( < MyComponent /> , document.getElementById(‘root’))
- React解析组件标签,找到了MyComponent组件
- 发现组件是使用类定义的,随后new出该类的实例,并通过实例调用到原型上的render方法
- 将render返回的虚拟dom转换为真实dom
- 插入到指定页面的元素内部
4、注意事项
- 组件名的首字母必须大写
- 虚拟DOM元素只能有一个根元素
- 虚拟DOM元素必须有结束标签
二、组件三大核心属性:state
1、定义
react把组件看成一个状态机,通过与用户的交互,实现不同的状态,然后渲染UI,让用户界面和数据保持一致。此时,就需要一个数据状态state来管理数据。
state是组件对象最重要的属性,值是key-value的对象。
在组件初始化时,可以在构造函数通过赋值给this.state初始化state,或直接在类中赋值给state初始化state。在初次调用render时,会用这个数据来渲染组件
2、更新state
- 通过赋值方式直接修改state,可以修改state对象的值,但是不能触发react重新调用render()渲染
- 需要触发react重新调用render()渲染,要通过setState去修改,该方法是异步方式
- 对象式更新setState(stateChange, [callback])、函数式更新setState(updateFunction, [callback])
// 创建类式组件
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.name = props.name;
this.age = props.age;
// 初始化状态
this.state = {
value1: 1,
value2: 1
}
}
// 初始化状态的另一种写法
// state = { value1: 1, value2: 1 }
// 对象式更新setState(stateChange, [callback])
changeState1 = () => {
// 读取状态
const {
value1 } = this.state;
// 修改状态
this.setState({
value1: value1 + 1
})
}
// 函数式更新setState(updateFunction, [callback])
changeState2 = () => {
// 修改状态
this.setState((state, props) => {
return {
value2: state.value2 + 1}
})
}
render() {
return (
<div>
<h2 onClick={
this.changeState1}>{
this.state.value1}</h2>
<h2 onClick={
this.changeState2}>{
this.state.value2}</h2>
</div>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
注意:直接在class中声明并初始化变量,相当于给该实例添加了一个属性;而直接定义函数,该函数是在其原型对象上。
setData的坑
class MyComponent extends React.Component {
constructor(props){
super(props);
this.state = {
num: 1 }
this.addNum = function () {
this.setState({
num:this.state.num+1})
this.setState({
num:this.state.num+1})
this.setState({
num:this.state.num+1})
}.bind(this)
}
render() {
return (
<button onClick={
this.addNum}>{
this.state.num}</button>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
// 当addNum函数被触发后,num只加了1,并没有加了3
// setData是异步的,调用后,并不会立即映射新的值
解释:
- 无论调用多少次setState,都不会立即执行更新。而是将要更新的state存入’_pendingStateQuene’,将要更新的组件存入’dirtyComponent’;
- 当根组件didMount后,批处理机制更新为false。此时再取出’_pendingStateQuene’和’dirtyComponent’中的state和组件进行合并更新;
3、state的作用
主要用于组件保存、控制以及修改自己的属性。
属于组件的私有属性,只能组件内部自己访问,外部是访问不了的。
4、this的指向
class A {
constructor(name) {
this.name = name
}
getName() {
console.log('this是', this)
}
}
const a = new A('小一')
a.getName(); // 可以直接获取到this,指向的是实例a
const f = a.getName;
f(); // 获取不到this,为undefined,不是通过实例直接调用
// 这个就像是react标签绑定的回调函数,不是通过实例直接调用,直接去调函数是获取不到this
5、事件处理
- 通过onXxx属性指定事件处理函数注意大小写
- React使用的是自定义(合成)事件,而不是使用原生的dom事件。为了兼容不同浏览器。如:onClick,其原生dom事件是onclick
- React中的事件是通过事件委托方式处理的(委托给组件的最外层元素)
- 通过event.target得到发生事件的DOM元素对象
6、事件处理绑定的3种方式
在构造器中声明绑定(官方推荐)
class MyComponent extends React.Component {
constructor(props){
super(props);
this.state = {
num: 1 }
this.addNum = this.addNum.bind(this)
}
addNum () {
const {
num } = this.state;
console.log('this', this);
this.setData({
num: num + 1
})
}
render() {
return (
<button onClick={
this.addNum}>{
this.state.num}</button>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
简单方法
class MyComponent extends React.Component {
constructor(props){
super(props);
this.state = {
num: 1 }
}
addNum () {
const {
num } = this.state;
console.log('this', this);
this.setData({
num: num + 1
})
}
render() {
return (
<button onClick={
this.addNum.bind(this)}>{
this.state.num}</button>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
箭头函数
class MyComponent extends React.Component {
constructor(props){
super(props);
this.state = {
num: 1 }
}
addNum () {
const {
num } = this.state;
console.log('this', this);
this.setData({
num: num + 1
})
}
render() {
return (
<button onClick={
() => this.addNum()}>{
this.state.num}</button>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
三、组件三大核心属性:props
1、定义
props是组件(包括函数组件和class组件)间的内置属性,每个组件对象都会有props属性;父组件传值给子组件,放到引用子组件的标签属性上,子组件再通过props接收。
所有React组件都必须像纯函数一样保护它们的props不被更改。
state和props主要的区别在于props是不可变的(组件内部不要修改props数据),而state可以根据与用户交互来改变。组件需要定义state来更新和修改数据,而子组件只能通过props来传递数据。
在React典型的数据流中,props传递是父子组件交互的唯一方式;通过传递一个新的props值来使子组件重新re-render,从而达到父子组件通信。
2、基本使用
-
简单调用(只读)
类组件在读取props的值,例如:this.props.value
函数组件需接收参数props,然后读取props的值,例如:props.value
// 父组件A文件
import C from './C'
class A extends React.Component {
render() {
return (
<div>
<div>我是父组件A</div>
<B value={
'props测试B'} />
<C value={
'props测试C'} />
</div>
)
}
}
// 函数组件B
function B(props) {
return(
<div>
我是子组件B,
接收到父组件的值:{
props.value}
</div>
)
}
// 渲染组件到页面
ReactDOM.render(<A />, document.getElementById('root'));
// 子组件B文件
class C extends React.Component {
constructor(props){
super(props);
this.state = {
num: 1 }
}
render() {
return (
<div>
我是子组件C,
我的state属性num的值:{
this.state.num}.
接收到父组件的值:{
this.props.value}
</div>
)
}
}
-
扩展属性 {…props}
展开props属性的一种简洁写法
var props = {
a: 1, b: 2};
<C {
...props} />
//等价于下面的写法
<C a=1 b=1 />
- props.children指的是组件的子元素
<C>hello,world</C>
function C(props){
// props.children指的就是 hello,world
return <p>{
props.children}</p>
}
3、对props进行限制和默认属性值
react中使用prop-types对props的值的类型和必要性进行校验
// 父组件A文件
import C from './C';
class A extends React.Component {
render() {
return (
<div>
<div>我是父组件A</div>
<C value={
'props测试C'} name={
'小东'} />
</div>
)
}
}
// 渲染组件到页面
ReactDOM.render(<A />, document.getElementById('root'));
// 子组件B文件
import ProTypes from 'prop-types';
class C extends React.Component {
constructor(props){
super(props);
this.state = {
num: 1 }
};
static propTypes = {
value: ProTypes.number.isRequired,
name: ProTypes.string
age: ProTypes.number
};
static detaultProps = {
age: 18
};
render() {
return (
<div>
我是子组件C,
我的state属性num的值:{
this.state.num}.
接收到父组件的值:{
this.props.value}
</div>
)
}
}
// C.propTypes = {
// value: ProTypes.number.isRequired,
// name: ProTypes.string
// age: ProTypes.number
// }
// C.detaultProps = {
// age: 18
// };
四、组件三大核心属性:ref
1、含义
组件内的标签/组件可定义ref属性来标识自己,使用ref可以获取到绑定ref的标签节点/组件,可以去获取该节点上面需要使用的信息。
ref挂到组件上时,表示对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例
ref挂到HTML标签的dom元素上时,表示具体的dom元素节点
2、用法
ref有3种使用方式:字符串、回调函数、使用createRef
-
字符串形式的ref(官方文档不建议用)
绑定:使用 ref=“标识名称”
获取对应的ref节点:this.refs.标识名称
class MyComponent extends React.Component { showData () { const { input1 } = this.refs; alert(input1.value) } render() { return ( <div> <input ref="input" type="text" placeholder="点击按钮提示数据" /> <button onClick={ () => this.showData()}>点击提示输入框内容</button> </div> ) } } // 渲染组件到页面 ReactDOM.render(<MyComponent />, document.getElementById('root'));
-
回调函数(官方文档建议使用的方法)
ref属性接受一个回调函数,在组件被加载或卸载时会立即执行。
ref回调函数在组件被卸载时或原有的ref属性本身发生变化时,回调立即执行,参数会传入null,以确保内存不会泄漏。
当给HTML元素添加ref属性时,ref回调接收了底层的dom元素作为参数。
当给组件添加ref属性时,ref回调函数接收了该组件的实例,在componentDidMount或omponentDidMount这些生命周期前执行。
class MyComponent extends React.Component { showData () { const { input1 } = this.refs; alert(input1.value) } render() { return ( <div> <input ref={ node => this.input1=node} type="text" placeholder="点击按钮提示数据" /> <button onClick={ () => this.showData()}>点击提示输入框内容</button> </div> ) } } // 渲染组件到页面 ReactDOM.render(<MyComponent />, document.getElementById('root'));
-
使用createRef
在React 16.3版本后,使用此方法来创建ref。将其赋值给一个变量,通过ref挂载在dom节点或组件上,该ref的current属性,将能拿到dom节点或组件的实例。
class MyComponent extends React.Component { myRef = React.createRef(); myRef1 = React.createRef(); showData () { const { input1 } = this.myRef.current.value; alert(input1.value) } render() { return ( <div> <input ref={ this.myRef} type="text" placeholder="点击按钮提示数据" /> <button onClick={ () => this.showData()}>点击提示输入框内容</button> <input ref={ this.myRef1} type="text" /> </div> ) } } // 渲染组件到页面 ReactDOM.render(<MyComponent />, document.getElementById('root'));
3、总结
不要过度使用ref:当发生事件的元素正好是需要操作的元素本身时,ref可省略,使用event.target代替。
使用ref时,不用担心会导致内存泄露的问题,react会自动帮你管理好,在组件卸载时ref值也会被销毁。
不要在组件的render
方法中访问ref
引用,render
方法只是返回一个虚拟dom,这时组件不一定挂载到dom中或者render返回的虚拟dom不一定会更新到dom中。
五、受控组件和非受控组件
1、受控组件
在HTML表单元素中,输入类DOM,随这用户的输入,通过onChange事件和setState,将数据维护到state中,在需要时,从state中获取数据的组件。
受控组件类似于VUE的双向绑定作用。
class MyComponent extends React.Component {
state= {
username: '',
password: ''
}
saveUsername = (event) => {
this.setState({
username: event.target.value})
}
savePassword = (event) => {
this.setState({
password: event.target.value})
}
handleSubmit = (event)=>{
// 通过state获取数据
const {
username,password} = this.state
alert(`你输入的用户名是:${
username},你输入的密码是:${
password}`)
}
render() {
return (
<form onSubmit={
this.handleSubmit}>
用户名称:<input onChange={
this.saveUsername} type="text" name="username" />
密码:<input onChange={
this.savePassword} type="text" name="password" />
</form>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
2、非受控组件
与受控组件相对的,如果仅仅只是想获取某个表单元素的值,并不关心它是如何改变的,可以通过获取DOM节点的方式或取值,通常是ref,这种不依赖于state。
class MyComponent extends React.Component {
handleSubmit = (event)=>{
// 通过state获取数据
const {
username,password} = this
alert(`你输入的用户名是:${
username},你输入的密码是:${
password}`)
}
render() {
return (
<form onSubmit={
this.handleSubmit}>
用户名称:<input ref={
currentNode => this.username = currentNode} type="text" name="username" />
密码:<input ref={
currentNode => this.password = currentNode} type="text" name="password" />
</form>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
六、高阶函数和函数柯里化
能够优化代码,让代码更加简洁
1、含义
高阶函数:一个函数接收的参数是一个函数,或调用返回值仍是一个函数,则称之为高阶函数。
常见的高阶函数:Promise、setTimeout、arr.map等等
函数的柯里化: 通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编码形式。
// 函数的柯里化写法
function sum(a) {
return (b) => {
return (c) => {
return a + b + c
}
}
}
const result = sum(1)(2)(3)
2、示例
class MyComponent extends React.Component {
state= {
username: '',
password: ''
}
// 这个函数的返回值交给onChange作为回调
// 这个saveFormData是个高阶函数
// 同时也是函数的柯里化写法,两个函数接收到两个参数,dataType统一处理
saveFormData = (dataType) => {
return (event) => {
this.setState({
[dataType]: event.target.value})
}
}
handleSubmit = (event)=>{
event.preventDefault();// 阻止默认事件(阻止表单提交)
// 通过state获取数据
const {
username,password} = this.state
alert(`你输入的用户名是:${
username},你输入的密码是:${
password}`)
}
render() {
return (
<form onSubmit={
this.handleSubmit}>
用户名称:<input onChange={
this.saveFormData('username')} value={
this.state.username} type="text" name="username" />
密码:<input onChange={
this.saveFormData('password')} this.saveFormData('password') type="text" name="password" />
</form>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
onChange={this.saveFormData}时,onChange事件的回调函数调用的是saveFormData函数;
onChange={this.saveFormData(‘username’)}时,其回调函数是执行了saveFormData函数所返回的函数;
这就说明,如果其dom上绑定事件所赋予的函数如果有加括号,在组件new实例化后调用render函数的初始化渲染的时候,就会默认去执行该函数,其回调函数就是该函数的返回值,无返回值就是underfined
3、不同柯里化的其他写法(较常用)
class MyComponent extends React.Component {
state= {
username: '',
password: ''
}
// 这个函数的返回值交给onChange作为回调
// 这个saveFormData是个高阶函数
// 同时也是函数的柯里化写法,两个函数接收到两个参数,dataType统一处理
saveFormData = (dataType, event) => {
this.setState({
[dataType]: event.target.value})
}
handleSubmit = (event)=>{
event.preventDefault();// 阻止默认事件(阻止表单提交)
// 通过state获取数据
const {
username,password} = this.state
alert(`你输入的用户名是:${
username},你输入的密码是:${
password}`)
}
render() {
return (
<form onSubmit={
this.handleSubmit}>
用户名称:<input onChange={
event => this.saveFormData('username', event)} value={
this.state.username} type="text" name="username" />
密码:<input onChange={
event => this.saveFormData('password', event)} this.saveFormData('password') type="text" name="password" />
</form>
)
}
}
// 渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('root'));
React调用onChange事件中绑定的函数才能接收到参数event,可以直接给onChange一个回调函数就好。让React去帮我们调用onChange事件,接收到event参数后,将dataType和event都传入函数saveFormData(),这样,就不需要使用函数的柯里化,只要直接将数据保存到state中即可。
七、组件的生命周期
1、生命周期(图)
生命周期函数指在某一个时刻组件会自动调用执行的函数
组件从创建到死亡它会经历一些特定的阶段;包含一系列的钩子函数(生命周期的回调函数),会在特定的时刻调用,做特定的工作。
根据广义描述,生命周期分为三个阶段:挂载、渲染、卸载。
生命周期分为以下四大阶段
组件初始化阶段(Initialization)
组件挂载阶段(Mount):组件第一次渲染到DOM树
组件更新阶段(update):组件state、props变化引发的重新渲染
组件卸载阶段(Unmount):组件从Dom树删除
2、初始化阶段
发生在constructor中的内容,在constructor中进行state、props的初始化,在这个阶段修改state,不会执行更新阶段的生命周期,可以直接对state赋值
3、挂载阶段
当组件实例被创建并插入DOM中时,其生命周期的顺序:
- constructor: 挂载前会调用其构造函数,通常在这里初始化state对象、给自定义方法绑定this、外部传入参数props本地化。
- componentWillMount: 预装载函数(较少用),组件经历了构造函数初始化数据后,还未渲染dom前。不能在这边更改state,是无效的。在此函数的操作,都可以提前到构造函数。
- render: 仅用于渲染的纯函数,只返回要渲染的东西,不应该包含业务逻辑,不能省略的函数,一定要有返回值,返回null或false表示不渲染任何dom元素。返回值取决于state和props,因此不允许任何修改props、state、拉取数据等具有副作用的操作。
- componentDidMount: 挂载成功函数,组件初次被渲染到dom树之后被调用;因render只是返回JSX对象并没有立即挂载到dom树上,该函数不会再render函数调用完之后立即调用。可以获取到dom节点并操作;可以在此调用ajax请求,返回的setState后组件重新渲染。
4、更新阶段
当组件挂载到DOM树上之后,props/state被修改会导致组件进行更新操作
- componentWillReceiveProps(nextProps): 主要时提供对props发生改变的监听,如果需要在 props 发生改变后,通过对比nextProps和this.props,来重新setState, 不会二次渲染,而是直接合并 state。
- shouldComponentUpdate(nextProps, nextState): 性能优化。返回bool值,true表示要更新,false表示不更新,使用得当将大大提高React组件的性能,避免不需要的渲染。因为react父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的,因此需要在子组件的该生命周期中做判断。不可以 setState,会导致循环调用。
- componentWillUpdate(nextProps,nextState): 预更新函数,shouldComponentUpdate返回true以后,组件进入重新渲染的流程,进入componentWillUpdate,这里同样可以拿到nextProps和nextState。
- render:渲染函数
- componentDidUpdate(prevProps,prevState): 更新完成函数,每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state
5、卸载阶段
- componentWillUnmount: 在组件卸载及销毁之前直接调用。用于去除componentDidMount函数带来的副作用,例如清除计时器、删除componentDidMount中创造的非React元素。
6、新生命周期(图)
对比之前的生命周期可以发现,React 16 中去掉了componentWillMount、componentWillReceiveProps、componentWillUpdate。
官方给出的解释是 react 打算在17版本推出新的 Async Rendering,提出一种可被打断的生命周期,而可以被打断的阶段正是实际 dom 挂载之前的虚拟 dom 构建阶段,也就是要被去掉的三个生命周期。
本身这三个生命周期所表达的含义是没有问题的,但 react 官方认为我们(开发者)也许在这三个函数中编写了有副作用的代码,所以要替换掉这三个生命周期,因为这三个生命周期可能在一次 render 中被反复调用多次。
取代这三个生命周期的是以下两个新生命周期:
-
static getDerivedStateFromProps(nextProps, prevState): 字面意思是:从props中获取派生的state。代替componentWillReceiveProps(). 老版本中的componentWillReceiveProps()方法判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。
// before componentWillReceiveProps(nextProps) { if (nextProps.isLogin !== this.props.isLogin) { this.setState({ isLogin: nextProps.isLogin, }); } if (nextProps.isLogin) { this.handleClose(); } } // after static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.isLogin !== prevState.isLogin) { return { isLogin: nextProps.isLogin, }; } return null; } componentDidUpdate(prevProps, prevState) { if (!prevState.isLogin && this.props.isLogin) { this.handleClose(); } }
-
getSnapshotBeforeUpdate(prevProps, prevState): 字面意思是:在组件更新之前获取快照。代替componentWillUpdate,在最近一次渲染输出(提交到 DOM 节点)之前调用。
getSnapshotBeforeUpdate和componentWillUpdate这两者的区别在于:
- 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
- getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说再getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。此生命周期返回的任何值都将作为参数传递给componentDidUpdate()
八、虚拟DOM与DOM Diffing算法
react/vue中的key有什么作用?为什么遍历列表时,最好不要用key?
1、虚拟dom中key的作用
- 简单的说,key是虚拟dom对象标识,在更新显示时key起着极其重要的作用。
- 详细的说,当状态中的key发生改变时,react会根基【新数据】生成【新虚拟dom】,随后react进行【新的虚拟dom】与【旧虚拟dom】的diff比较,比较规则如下:
- 旧虚拟dom中找到与新虚拟dom相同的key:
- 若虚拟dom的内容没变,直接用之前的真实dom;
- 若虚拟dom的内容发生改变,则生成新的真实dom,随后替换掉页面中之前的真实dom
- 旧虚拟dom中未找到与新虚拟dom相同的key。根据数据创建新的真实dom,随后渲染到页面
- 旧虚拟dom中找到与新虚拟dom相同的key:
2、用index可能会引发的问题
- 若对数据进行:逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实dom更新。页面效果没问题,但效率低。
- 如果结构中还包含输入类dom,会产生错误dom更新。
- 注意:如果不存在对数据逆序添加、逆序删除等破坏顺序操作,仅用于渲染到页面展示,使用index作为key是没问题的。
3、开发中如何选择key
- 最好使用每条数据的唯一标识作为key,比如id、学号、身份证等唯一值
- 如果确定只是简单展示,用index也是可以的