React 0基礎學習路線(4)— 圖解詳述非受控與受控組件及屬性默認值、props驗證原理及children原理詳述(附詳細案例代碼解析過程)

1. 重點提煉

  • 表單
    • 非受控組件和受控組件
      • 受控組件:通過react來管理的組件(通過props傳遞數據、onChange事件監聽,來綁定表單控件的一些元素,如input輸入框,其顯示內容就是通過props傳遞數據來顯示,同時監聽input輸入框的一些事件,來達到表單當中的內容和我們React當中state綁定的一種效果)
      • 非受控組件:不通過react來管理的控件,而是通過dom元素自身來維護狀態的控件
    • ref
      • 引用的意思,通過 ref來綁定組件(原生元素),對組件(元素)進行操作
  • props 驗證
    • 通過 propTypes 庫來對組件傳入的 props的類型進行驗證(如果使用了 TS,則可以代替 propTypes
  • props 默認值
    • 類似 props 驗證,它的作用是爲傳入的 props 進行默認值的設置
  • children
    • 組件包含的內容 - dialog
    • vue 以及 原生html中的 slot(插槽)類似

2. 表單

  在 React 裏,HTML 表單元素的工作方式和其他的 DOM 元素有些不同。

  一般來說,表單以及表單中的(受用戶控制,可交互,即交互式元素)控件(如:inputselect……)是頁面中與 JavaScript 打交道最多的元素了。雖然我們可以通過 ref 的形式去操作它們,但是這樣會比較麻煩,React.js 爲我們提供了一個更好的方式把 React.js 中的數據以及邏輯與表單控件關聯起來。

2. 1 example01

  利用腳手架工具構建,我就不再細說,如有問題,請參考小迪之前的React文章。

2. 1. 1 example01-1

舉一個例子:如何操作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框裏輸入值做修改。我們發現在頁面上怎麼修改都修改不成功!這是爲什麼呢?

  因爲它是非受控組件,下面我們模擬一下這個特性:非受控特性(默認情況下)。

image-20200603170411203

參考: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

2. 1. 2 example01-2

  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;
image-20200603172017416

參考: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

2. 1. 3 example01-3

假設我們需要修改父級這個數據,我們添加事件試試:

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內部會有一個狀態(私有數據),對外暴露的是一個valueprops,但是對外接收到props以後,會賦值給內部這個私有狀態(私有數據),而內部(非受控組件)沒有提供任何方法,能去修改它的值。所以這個值我們怎麼也修改不了。

  其實對比理解,受控組件就是外部的用戶行爲可以控制組件的變化,而非受控組件則正好相反。

3. 受控組件

  受控組件 : 用 props 傳入數據的話,組件可以被認爲是受控(因爲組件被父級傳入的 props 控制)

  非受控組件 : 數據只保存在組件內部的 state 的話,是非受控組件(因爲外部沒辦法直接控制 state

  廣義來說,頁面中的任意元素都是一個獨立的組件,表單控件也是,它們內部也會維護屬於自己的狀態(如:value,selected,checked……),當然這些狀態是由原生實現的,而非 React.js 來控制的,但是有的時候我們希望通過 React.js 來管理和維護表單控件的狀態,我們把這種控件(控件)稱爲: 受控組件, 針對不同的組件,狀態的維護方式也有所差異。

  • input
  • textarea
  • select

通過 state 來控制組件狀態

  • 創建 state 與組件的某個狀態進行綁定
  • 監聽組件某些事件來更新 state

  反着理解上例,其實對於內部數據,外部可通過props去影響外部的數據(input組件值的變化),但是這個時候正好有一個相反的東西,是它內部數據的變化:

3. 1 example02

  下面我們仔細探究一下非受控組件

3. 1. 1 example02-1

  如下:假如內部的數據不綁定寫死!我們還是修改不了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屬性,導致它的頁面重新渲染。它內部的數據是內部自己維護的,我們從外部(用戶行爲)是無法操控的。

image-20200703132603358

參考: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

3. 1. 2 example02-2

  我們再看一個例子,把state屬性值,放到inputbutton中間。

  我們把外部的值傳入表單的內部,內部則會維護這個狀態,即內部會有一個數據記錄了我們傳進去的初值。緊接着,我們希望表單當中的數據,能跟接受用戶控制並且與外界的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

3. 1. 3 example02-3

  我們是否掌握一個概念,叫DOM屬性,瞭解attributeproperty嗎?及它兩者之間的差異性。我們探究一下這裏的原理。

  獲取一下inputvalue屬性的方式:兩中方式

  1. js propertyjs對象屬性)
  2. html attributehtml屬性)

attribute和property.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</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)不一樣!它們是有映射關係的。

3. 1. 3. 1 attribute和property深入探究

  在dom解析的時候,我們經常聽到dom樹,dom樹是什麼東西,和虛擬dom是一樣的,瀏覽器會在解析我們的html文檔的時候,如果我們把html文檔當作字符串去操作的話,會很麻煩的,因此做了一種關係叫對象映射,它就會分析html中的每一個元素的結構,然後把不同的元素轉成js中對象去表示。就相當於我們想操作界面元素的話,直接去操作對應的對象就行了。因爲它們在解析過程中會有對應關係,即映射關係,然後它內部又會做一件事,當我們去操作js當中對象的時候,它就會影響(重新渲染/重繪)我們的html。但是就如上面的代碼,我們定義input對象,並不完全等於html上的input標籤。

  這裏發現操作瀏覽器上的inputvalue值,實際操作的是js對象,而未對html標籤的value值產生任何的影響!

  但是我們還會發現一個現象,我們通過瀏覽器的F12工具欄中的Elements刪除input標籤,但是我們的inputjs對象還是可以運行,並且也可以輸出input.value。因爲js中的這個對象是沒有消除的,因爲它是將html標籤作爲參考而生成的對象,並且它不等同於html元素標籤。


  因此迴歸正題,我們在瀏覽器裏看到的value只是html標籤的中value值,剛刷新頁面的時候打印出來,兩者的值一樣。但是當我們修改了頁面的input框,只是修改了映射出來的js對象的value值,並沒有改變標籤裏的value屬性。

  我們還有一種做法,直接通過F12工具欄中的Elements去修改input的value值,當頁面元素髮生改變了以後,即當頁面更新的時候,頁面會動態獲取,重新映射導致兩個值一致了!

3. 1. 3. 2 example02-2小結

  回到我們正文的React。

  因此我們輸入內容以後,onChange事件處理函數中的e.target.value仍然可獲取。我們當前的value值是根據this.state.v1渲染出來的的,雖然我們在內部把這個原生js的元素的對象value值改了,就相當於改了input.value,但是並沒有反饋到當前this.state上。

3. 1. 4 example02-4

  無法修改當前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

3. 1. 4. 1 小結

  其實默認情況下,把input標籤當成一個組件就可以了,組件內部數據狀態(state)的變化,它不會主動影響外界傳進來的(原生jsprops的,導致我們最終看到的界面就是,input標籤的value根據this.state.v1進行渲染,即頁面上input框的值根據this.state進行渲染,但是內部的狀態變化又不會更改從外部傳入的this.state.v1,所以看到效果就是我們輸入的值不會顯示在input框上。那麼我們想利用React的外部props去控制input的數據,就只能採取事件的形式了。

3. 1. 4. 2 應用場景:控制和管理用戶合法輸入

  修改需求:用戶不管輸入什麼,都需要轉成大寫!我們只需要控制好數據,再去渲染即可。

changeV1(e) {
    console.log('...', e.target.value);
    this.setState({
        v1: e.target.value.toUpperCase()
    })
}

  這裏就不放案例代碼,大家自己去整吧!

image-20200604100902403

3. 1. 5 example02-5

  迴歸主題,我們這裏講的是非受控組件,input標籤value值的變化,只能通過內部去改變,但是在外部用戶輸入等外部行爲,是不能改變改value值的!而我們在onChange事件中,改變state的時候,會重新渲染(調用setState方法的緣故)input的。其實就是我們第一次將v1傳給非受控組件時頁面渲染,會解析並同步js和html的屬性值,但後面我們使jsvalue變了,除非你讓其重新渲染,否則標籤的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函數。

image-20200604103751525

  但是假若我們後續不需要改這個input值(只涉及一個初值即可),怎麼解決這個報錯問題呢?可以用defaultValue屬性。

    render() {
        return(
            <div>
                <h2>表單</h2>
                <hr/>            
                <input type="text" defaultValue={this.state.v1}/>
                {/*<input type="text" value={this.state.v1} onChange={this.changeV1} />*/}
                {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

image-20200703140841098

  同樣的後面講的textareaselect標籤也是同樣的原理。

3.2 input

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>
        );
    }
}

3. 2. 1 通過受控組件,可以更加便捷的操控組件交互

...
changeValue({target:{value}}) {
      this.setState({
            v1: value.toUpperCase()
      });
}
...

3. 3 textarea

  textareainput 類似,但是需要注意的是: 使用 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>

3. 4 select

  selectReact.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>

3. 4. 1 多選

  我們還可以設置多選 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>
...

3. 4. 2 單選

  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>
...

4. 非受控組件

  話又說回來,通過上面的學習,我們知道,每個受控組件,且不同的類型的受控組件它能控制的狀態只有那麼一些:valuechecked,但是實際上一個組件的狀態遠遠不止這些,比如 input 的焦點、禁用、只讀 等,都是組件的狀態,如果每一個狀態都通過上面的方式來管理,就會特別的麻煩了。這個時候,我們就需要用其他方式來處理了:DOM

4. 1 example03

  但是利用原生dom必然會有弊端,我們舉個例子看看。

  需求:點擊按鈕,動態獲取p標籤內容高度。

4. 1. 1 example03-1

  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

4. 1. 2 example03-2

  以上例子,當我們點擊按鈕的時候,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方法還未執行。

image-20200604122534555

  因此這種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

4. 2 Refs & DOM

  React.js 提供了多種方式來獲取 DOM 元素

  • 回調 Refs
  • React.createRef()

4. 2. 1 ref 屬性

  無論是 回調 Refs 還是 React.createRef(),都需要通過一個屬性 ref 來進行設置

<input ref={?} />

4. 2. 1. 1 回調 Refs

  這種方式,我們在前面已經使用過了

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>
        )
    }
}
4. 2. 1. 1. 1 example04

  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

4. 2. 1. 2 React.createRef()

  該方法返回一個 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>
        )
    }
}
4. 2. 1. 2. 1 example05
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>
                {/*<p ref={el => {*/}
                {/*    console.log('...', el)*/}
                {/*    {*/}
                {/*        this.refEl = el;*/}
                {/*    }*/}
                {/*}} style={{background: 'red', color: 'white'}}>{this.state.content}</p>*/}
                <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

4. 3 建議

  • 儘量避免從 props 中派生 state
  • 儘量使用 props,避免使用 state

5. 屬性默認值

5. 1 defaultProps 靜態屬性

  defaultProps 可以爲 Class 組件添加默認 props。這一般用於 props 未賦值,但又不能爲 null 的情況

注意:defaultPropsClass 的屬性,也就是靜態屬性,不是組件實例對象的屬性

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
    }
 
    render() {
        return(
            <div>
                <h2>MyComponent - {this.props.max}</h2>
            </div>
        );
    }
}
 
MyComponent.defaultProps = {
    max: 10
}
 
ReactDOM.render(
    <MyComponent />,
    document.getElementById('app')
);

5. 1. 1 example06

5. 1. 1. 1 example06-1

引例

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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                <PropsDefaultValueDemo max={1} />
            </div>
        )
    }

}

export default App;

image-20200604125855346

參考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.06-1

Branch:branch2

commit description:v1.06-1-example06-1(屬性默認值-引例)

tag:v1.06-1

5. 1. 1. 2 example06-2

  組件其實就是函數,有的時候我們需要在組件內部控制外界傳入的參數是否合法。並且有的時候沒有傳值,我們也希望其可以顯示一個默認值。

  可以用邏輯

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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                <PropsDefaultValueDemo />
            </div>
        )
    }

}

export default App;

image-20200604125855346

<PropsDefaultValueDemo max={1000} />

image-20200604130419028

參考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.06-2

Branch:branch2

commit description:v1.06-2-example06-2(屬性默認值-用邏輯

tag:v1.06-2

5. 1. 1. 3 example06-3

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>
        );
    }

}

image-20200703161120797

參考: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

5. 1. 2 基於 static 的寫法

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')
);

5. 2 非受控組件默認值

  有的時候,我們希望給一個非受控組件一個初始值,但是又不希望它後續通過 React.js 來綁定更新,這個時候我們就可以通過 defaultValue 或者 defaultChecked 來設置非受控組件的默認值

5. 2. 1 defaultValue 屬性

<input type="text" defaultValue={this.state.v1} />

5. 2. 2 defaultChecked 屬性

<input type="checkbox" defaultChecked={this.state.v2}  />
<input type="checkbox" defaultChecked={this.state.v3}  />

6. props 驗證

  隨着應用的不斷增長,也是爲了使程序設計更加嚴謹,我們通常需要對數據的類型(值)進行一些必要的驗證,React.js 提供了一個驗證庫:prop-types

  主要是對傳入對props參數對數據類型進行安全(合法性)驗證,主要進行類型驗證。不過還是推薦使用typescript做驗證,它的功能更爲強大,官方也是推薦使用ts。但兩者是有差異的,ts是在編譯過程中作類型檢測,prop-types是針對代碼層面的,還會附件一些功能。

6. 1 prop-types

  prop-types 是一個獨立的庫,需要安裝

https://www.npmjs.com/package/prop-types

6. 1. 1 安裝

npm i -S prop-types

6. 2 使用

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. <MyComponent />).
  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.'
      );
    }
  })
};

6. 2. 1 example07

  使用演示。

6. 2. 1. 1 example07-01

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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                {/*<PropsDefaultValueDemo />*/}
                {/*<PropsDefaultValueDemo max={1000} />*/}
                <PropTypesDemo max={10} />
            </div>
        )
    }

}

export default App;

image-20200604133456879

<PropTypesDemo max={'csdn'} />

  報錯:有一個不可接受的(失敗的)props類型數據,string類型的,但我們只允許number

image-20200604134003869

必傳參,不能省略

max: PropTypes.any.isRequired

image-20200604140717691

參考: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

6. 2. 1. 2 example07-02

  需求: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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                {/*<PropsDefaultValueDemo />*/}
                {/*<PropsDefaultValueDemo max={1000} />*/}
                {/*<PropTypesDemo max={19} />*/}
                <PropTypesDemo max={9} />
                {/*<PropTypesDemo max={'csdn'} />*/}
                {/*<PropTypesDemo />*/}
            </div>
        )
    }

}

export default App;

image-20200604141309168

<PropTypesDemo max={19} />

image-20200604141340920

參考: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

7. children

  一個組件通過 props 除了能給獲取自身屬性上的值,還可以獲取被組件包含的內容,也就是外部子組件,前面我們寫的組件更多的是作爲一個單標籤組件,實際應用中很多組件是雙標籤的,也就是可以包含內容的,也可稱爲:容器組件,那麼組件包含的內容,我們就可以通過 props.children 來獲取

7. 1 dialog 組件

image-20190720212419567

7. 1. 1 css

.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;
}

7. 1. 2 dialog.js

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>
        );
    }
 
}

7. 1. 3 example08

需求:模擬對話框

7. 1. 3. 1 example08-1

實現框子

對話框樣式,可以自己完善,或者參考小迪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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                {/*<PropsDefaultValueDemo />*/}
                {/*<PropsDefaultValueDemo max={1000} />*/}
                {/*<PropTypesDemo max={19} />*/}
                {/*<PropTypesDemo max={9} />*/}
                {/*<PropTypesDemo max={'csdn'} />*/}
                {/*<PropTypesDemo />*/}
                <ChildrenDemo title={'CSDN'} content={"https://mp.csdn.net/"}/>
            </div>
        )
    }

}

export default App;

image-20200604142518456

參考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.08-1

Branch:branch2

commit description:v1.08-1-example08-1(需求:模擬對話框——實現框子)

tag:v1.08-1

7. 1. 3. 2 example08-2

  如果對話框中內容放入一個表單

     <ChildrenDemo title={'CSDN'} content={
                    <form>
                        <p>
                            用戶名:<input type="text"/>
                        </p>
                    </form>
                }/>

  如果我們還想往裏嵌套組件,參數寫起來就像之前的遞歸一樣,一層一層可讀性極差!

image-20200604142854474

參考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.08-2
Branch:branch2

commit description:v1.08-2-example08-2(需求:模擬對話框——嵌套表單)

tag:v1.08-2

7. 1. 3. 3 example08-3

  實際上結構不要寫在屬性上,可以把當成容器一樣使用。這就更類似於我們平時寫的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中,其實這些物品是放在propschildren屬性中,這其實是一個虛擬dom節點(其實就是把這些物品解析成虛擬dom)。

image-20200604143400761

再修改代碼:

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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                {/*<PropsDefaultValueDemo />*/}
                {/*<PropsDefaultValueDemo max={1000} />*/}
                {/*<PropTypesDemo max={19} />*/}
                {/*<PropTypesDemo max={9} />*/}
                {/*<PropTypesDemo max={'csdn'} />*/}
                {/*<PropTypesDemo />*/}
                <ChildrenDemo title={'CSDN'}>
                    <form>
                        <p>
                            用戶名:<input type="text"/>
                        </p>
                    </form>
                </ChildrenDemo>
            </div>
        )
    }

}

export default App;

image-20200604144421848

  這其實是經常會用到的,假若我們寫一個組件,我們不可能把組件的所有內容都能夠定義好,很多時候,這個組件其實是一個容器型組件,它裏邊還可以放很多其他東西,這個時候可由外部決定。有兩種方式,第一種直接傳參,但有的時候結構可能會很複雜,傳參會很麻煩(可讀性極差),我們就可以用類似html的嵌套形式即可(解析爲虛擬dom放在children屬性裏)。

參考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.08-3
Branch:branch2

commit description:v1.08-3-example08-3(需求:模擬對話框——最終版)

tag:v1.08-3

7. 1. 4 example09

7. 1. 4. 1 example09-1

  實現一個可拖拽的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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                {/*<PropsDefaultValueDemo />*/}
                {/*<PropsDefaultValueDemo max={1000} />*/}
                {/*<PropTypesDemo max={19} />*/}
                {/*<PropTypesDemo max={9} />*/}
                {/*<PropTypesDemo max={'csdn'} />*/}
                {/*<PropTypesDemo />*/}
                {/*<ChildrenDemo title={'CSDN'}>*/}
                {/*    <form>*/}
                {/*        <p>*/}
                {/*            用戶名:<input type="text"/>*/}
                {/*        </p>*/}
                {/*    </form>*/}
                {/*</ChildrenDemo>*/}
                {/*裝飾者*/}
                <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,我們來完善拖拽!

image-20200604150222777

image-20200703192257478

參考:https://github.com/6xiaoDi/blog-react-Novice/tree/v1.09-1
Branch:branch2

commit description:v1.09-1-example09-1(需求:React實現拖拽——框子)

tag:v1.09-1

7. 1. 4. 2 example09-2

  關於拖拽原理小迪不再重複了,請參考小迪的詳細探究拖拽原理的博客。

推薦 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">

                {/*<FormDemo />*/}

                {/*<hr/>*/}

                {/*<UnControl value={this.state.v1} />*/}

                {/*<RefDemo />*/}

                {/*<PropsDefaultValueDemo />*/}
                {/*<PropsDefaultValueDemo max={1000} />*/}
                {/*<PropTypesDemo max={19} />*/}
                {/*<PropTypesDemo max={9} />*/}
                {/*<PropTypesDemo max={'csdn'} />*/}
                {/*<PropTypesDemo />*/}
                {/*<ChildrenDemo title={'CSDN'}>*/}
                {/*    <form>*/}
                {/*        <p>*/}
                {/*            用戶名:<input type="text"/>*/}
                {/*        </p>*/}
                {/*    </form>*/}
                {/*</ChildrenDemo>*/}
                {/*裝飾者*/}
                <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



(後續待補充)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章