React底層原理以及在工做中碰到的細節問題

滴水能把石穿透,萬事功到天然成——zZ先森css

1.React腳手架__create-react-app

全局安裝腳手架

$ npm install -g create-react-app
//或者
$ yarn add -g create-react-app
複製代碼

基於腳手架快速構建工程化項目

$ create-react-app xxx
//xxx:項目名稱遵循npm包規範,使用小寫字母、數字、橫槓組合方式
複製代碼

若是電腦上安裝了yarn,默認會基於yarn安裝html

安裝最新版本腳手架

要求npm的版本號在5.2以上才能夠,而且一步到位安裝到局部vue

$ npx create-react-app my-app
複製代碼

腳手架項目目錄

  • node_modules: 安裝所需的全部模塊。
  • public: 存放編譯模板
    • index.html:

    在index.html中引入的公共資源文件即public中與index.html同級的資源,在導入的時候,前綴加上<%PUBLIC_URL%>(當前目錄),在webpack打包編譯的時候會加以處理。vue中是<%BASE_URL%>node

    <head>
    ...
    <link rel="icon" href="`<%PUBLIC_URL%>/favicon.ico"/>
    ...
    </head>
    <body>
    //把最後編譯完成的html放到id爲root的盒子裏
    <div id="root"></div>
    </body>
    複製代碼
    • 公共資源: 基於srclink調入html中,這樣的話webpack對此不作處理。

    存放一些不支持CommonJs規範、ES6Module規範,在逼不得已使用的時候,在這裏直接導入。react

  • src: 存放項目源碼
    • index.js: 當前項目入口,建議新建爲index.jsx 在新建組件的時候一搬爲.jsx文件,webpack中支持.jsx文件的解析和編譯,vscode也就能夠識別
    • api: 數據處理api
    • store: REDUX公共狀態管理
    • assets: 存儲公共資源
    • routes: 路由管理
    • untis: 公共的js模塊
    • components 公共的組件
  • package.json: 默認的配置清單
    • 生產依賴項
      • react REACT框架的核心,提供了狀態、屬性、組件、生命週期等
      • react-dom 把JSX語法渲染成爲真實的DOM,最後顯示在瀏覽器中
      • react-scripts 包含了當前工程化項目中webpack配置的東西(嫌棄把webpack放到項目目錄中看上去太醜,腳手架把全部webpack的配置項和依賴都隱藏到node_modules中了,react-scripts這個REACT腳本執行命令,會通知webpack打包編譯)
    • scripts 當前項目可執行的腳本命令($ yarn xxx)
      • $ yarn start => 開發環境下啓動項目(默認會基於WEBPACK-DEV-SERVER建立一個服務,用來隨時編譯和渲染開發的內容)
      • $ yarn build => 生產環境下,把編寫內容打包編譯,放到build文件目錄下(服務器部署)
      • $ yarn eject => 把全部隱藏在node_modules中的webpack配置項都暴露出來(方便本身根據項目需求,二次更改webpack配置)

yarn eject(將配置文件暴露出來進行二次配置)

  • babel-preset-react-app 解析JSX語法的webpack

  • scriptsgit

    • start.js =>執行$ yarn start的入口文件
    • build.js =>執行 $ yarn build的入口文件

    若是執行yarn start/build 提示少模塊,咱們則少了誰就安裝誰github

    @babel/plugin-transform-react-jsxweb

    @babel/plugin-transform-react-jsx-sourcenpm

    @babel/plugin-transform-react-jsx-self

  • config |- 這裏存儲的就是webpack的配置項

PACKAGE.JSON

配置端口號和本地域名和HTTPS協議

"scripts": {
	"startMac": "PORT=8081 node scripts/start.js",
	"start": "set PORT=8081&&set HOST=127.0.0.1&&set HTTPS=true&&node scripts/start.js",
	"build": "node scripts/build.js"
},
複製代碼

修改less的處理配置

  • 先安裝less以及less-loader $ yarn add less less-loader

  • 配置config/webpack.config.js

const cssRegex = /\.(css|less)$/;


{
	test: cssRegex,
	exclude: cssModuleRegex,
	use: getStyleLoaders({
		importLoaders: 1,
		sourceMap: isEnvProduction && shouldUseSourceMap,
	}, "less-loader"),
	// Don't consider CSS imports dead code even if the // containing package claims to have no side effects. // Remove this when webpack adds a warning or an error for this. // See https://github.com/webpack/webpack/issues/6571 sideEffects: true, }, 複製代碼

2.JSXJavaScript XML(HTML)基礎語法

JSX編寫出來的就是虛擬DOM

  • 每個組件視圖只能有一個根元素節點 :有必要的話,能夠添加一個Fragment空文檔標記標籤<></>

    ReactDOM.render([JSX],[CONTAINER],[CALLBACK])

    • [CONTAINER]不建議是body或者是html而且指定一個元素容器
    • [CALLBACK]將虛擬DOM渲染到頁面上,而後出發回調函數,通常不用
  • JSX語法中基於大括號來綁定動態數據值和JS表達式

    • nullundefined表明的是空元素
    • {}中不能寫對象和函數等其餘引用數據類型(除數組外),寫數組的話會將其轉換爲字符串。
    • jsx虛擬DOM對象能夠放在{}
  • 給JSX設置類名和樣式

    • 給JSX元素設置樣式類名爲className
    • 給JSX元素設置樣式,必須在大括號內放入對象、
    • 若是{}裏放的是JS表達式,那麼返回結果能夠是新的JSX元素或者元素值
  • 動態綁定數據

    • 在JSX語法中基於大括號來綁定數據,而且大括號裏面只能是空元素和數組,再者必須是JS表達式,因此能夠在大括號中基於數組的一些方法來綁定數組中的一些數據,這些方法也必須是返回一個值。
    • 在JSX語法中,循環綁定數據的時候,要求在給每一個循環的數據都要加一個key,由於key是DOM DIFF中重要的憑證,key值通常不設置爲循環的索引,而是設置惟一不變的值
let name = "zZ",
    styObj = {color:'blue'},
    data=[
    {
        id:1,
        name:'shu'
    },
    {
        id:2,
        name:'gang'
    }
    ];
ReactDOM.render(<div className="box" style={styObj}>
<ul>
     data.map(item=>{
        return <li key={item.id}>
               <span>{item.id}</span>
               <span>{item.name}</span>
        </li> 
     })
</ul>
</div>)
複製代碼

此處的索引不能用索引的,主要目的是爲了數據變更的時候,DIFF渲染的時候,渲染速度更快,且不容易產生組件的錯誤。

3.虛擬DOM到真實DOM

1.把JSX語法經過PABEL-PRESET-REACT-APP語法解析包變爲CREATE-ELEMENT格式

Vue中經過VUE-LOADER解析template模板

ReactDOM.render(<div className="box" style={styObj}>
                     zZ是個好男孩
                    <span>優秀</span>
            </div>)
複製代碼

經過解析解析爲如下格式:

React.createElement("div",{
    className:"box",
    style:{color:"blue"}
},"zZ\u662f\u4e2a\u597d\u7537\u5b69",React.creatElement("span",null,"\u4f18\u79c0"))
複製代碼
  • 每個標籤都會解析爲一個CREATE-ELEMENT格式
  • 經過解析,實則是構建了一棵樹,在執行編譯的時候是從最底層的子節點開始從右到左、從下到上執行編譯

2.執行React.createElement()

返回的的是一個對象:它就是一個虛擬DOM

  • $$typeof:Symbol(react.element),
  • key:null,
  • ref:null,
  • type:標籤名/組件名
  • props:給元素標籤上設置的屬性,除key和ref
    • children:若是元素有子節點,纔有children屬性,而且值會根據子節點的類型而定。
      • 若是是單個子節點,屬性值爲字符串或者對象
      • 若是是多個子節點,屬性值爲數組

3.執行REACT-DOM.RENDER

render函數把執行React.createElement()返回的對象,變爲真實的DOM,最後渲染到指定容器中,呈現到頁面上。

4.虛擬DOM到真實DOM實現原理

模擬虛擬DOM如何到真實DOM,封裝以下代碼:

let React = {},
    ReactDOM={};
    //直接操做私有屬性 將屬性名和屬性值暴露給回調函數
function each(obj,callback){
    Obiect.keys(obj).forEach(item=>{
         let val = obj[item];
         callback && callback(val,item)
    })
}
React.creatElement = function createElement(type,props,...children){
    let virtualDOM = {
        type,
        props:{},
        key:null,
        ref:null
    };
    //處理props
    if(props){
        each(props,(value,item)=>{
            if(item === "key"){
                virtualDOM.key = value;
                return
            }
            if(item === "ref"){
                virtualDOM.ref = value;
                return
            }
        })
        delete props["key"];
        delete props["ref"];
        virtualDOM.props = {...props}
    }
    //處理children
    if(children.length>0){
        virtualDOM.props.children = children.length===1 ? children[0] : children
    }
    
}
//把虛擬DOM轉換爲真實DOM
ReactDOM.render = function render(virtualDOM,container,callback){
    let {
        type,
        props
    }=virtualDOM;
    let element = document.createElement(type);
    each(props,(val,item)=>{
        if(item==="className"){
            element.className = val;
            return;
        }
        if(item==="style"){
            each(item,(value,key)=>{
                element.["style"]["key"] = value;
            })
            return;
        }
        if(item === "children"){
            let children = val;
            //統一做爲數組
            children = Array,isArray(children) ? children : [children];
            each(children,(val,item)=>{
                if(typeof val === "string"){
                    element.appendChild(document.creatTextNode(val));
                    return 
                }
                render(val,element)
            })
            return
        }
        //將其餘屬性直接掛載到元素上
        element.setAttribute(item,val)
    })
    //放到指定的容器中
    container.appendChild(element);
    if(typeof callback === "function"){
        callback();
    }
}
export default {React,ReactDOM}

複製代碼

5.組件

React中的組件:每個組件(類組件)都是一個單獨的個體(數據私有,有完整的生命週期函數,有本身的視圖)

組件命名能夠以.jsx爲後綴名,在create-react-app腳手架建立項目中,包含了對.JSX文件的處理

1.組件分類

  • 函數組件(靜態組件): 一個函數返回JSX對象。經過HOOKS將其動態化
  • 類組件(動態組件):建立一個類,並繼承React.Component/PureComponent,且必需要有一個render函數做視圖的渲染,在函數中返回JSX
  • REACT HOOKS

2.動態組件和靜態組件的區別

  • 靜態組件沒有本身的狀態、生命週期函數等,因此組件一旦被渲染內容就固定了,可是能夠屢次使用,優點: 渲染速度快,開發維護簡單。弊端: 靜態化以及功能簡單
  • 動態組件: 有本身的狀態和生命週期函數,即使在渲染完成,還能夠經過狀態的改變從新渲染修改的部分,優點 功能強大,動態化管理數據,缺點運行速度較慢
  • 最經常使用的是HOOKS,根據業務的需求,將函數靜態組件加以動態化,實現動態組件的狀態、生命週期以及經過refs修改DOM元素。

3.調用組件

組件調用】在JSX語法中,ReactDOM.render進行處理組件的時候,當發現type不是標籤字符串,則把當前的組件執行,若是是函數組件,則直接執行,若是是類組件,則進行new執行,並建立一個實例,並把父組件調用子組件傳遞進去的props都傳遞給函數或者實例來進行渲染。注意倆點:與vue不一樣的是單閉合和雙閉合標籤皆可props是隻讀的不能修改

函數組件】相對比vue插槽來講,若是在父組件調用子組件的時候,將屬性、標籤、其餘組件經過props屬性傳遞給子組件,也就是函數接收的參數爲props。在React.createElement方法造成虛擬DOM的時候,將存儲在JSX對象的props中,標籤和組件都將存儲在props屬性名爲children中,想要控制在不一樣的位置渲染的時候,有倆種方法第一種: React提供了一個遍歷children的方法集合對象=>Children,Children中有①count/②forEach③map④only ⑤toArray五個方法。第二種: 利用索引來獲取children數組中的每一項,放到不一樣位置。

類組件

【繼承】:1. 原型繼承 2.call繼承 3.寄生組合式 繼承 4.ES6中基於class實現繼承 關鍵字extends

【super】相似於call繼承,會把父類看成函數執行,讓函數中的this是子類的實例。當前類中必須有constructor。且在調用組件的時候傳遞進來的屬性,傳遞給constructor。在REACT中,在構建類組件的時候,類組件繼承了React.Component,則在super執行的時候,至關於把React.Component看成普通函數執行,讓方法中的THIS是當前實例。this=>{props:xxx,content:xxx,refs:{},updater:{...}} 即爲了能讓傳遞進來的屬性掛載到實例上,則會給super傳遞props

REACT-DOM.RENDER渲染的時候,若是發現虛擬DOM中的TEPE是一個類組件,會建立這個類實例,並把它解析出來的props傳遞這個類,constructor就能夠接收進來props。執行constructor以後才建立一個當前類的實例。雖然已將props傳遞給constructor,可是實例上並未掛載這些屬性,基於this.props不能獲取到值,可是能夠直接使用形參中的props。當constructor執行完,REACT會幫咱們繼續處理,經過render方法把PROPS/CONTEXT...掛載到實例上,後期的生命週期函數均可以基於THIS.PROPS來獲取和應用傳遞進來的屬性值。 必需要有render函數,它返回的是當前組件要渲染的視圖。

4.Component&PureComponent

類組件進行繼承的時候,React提供了倆種: Component PureComponent。 倆個父類區別在於:PureComponent相對於Component,會默認建立一個鉤子函數shouldComponentUpdate,若是咱們手動添加shouldComponentUpdate則以咱們添加的爲主,shouldComponentUpdate運行原理是淺比較,給這個鉤子函數傳遞進來的就是更新的新數據,經過判斷類決定返回true仍是false。

注意在修改的狀態是數組或者其餘引用數據類型的時候,注意其地址若是沒有更新,其存儲的值修改的話,照樣是不渲染的。基於解構到新數組來給予新的地址加以區分,而後返回true,視圖纔會渲染。

import React from 'react';

export default class Vote extends React.PureComponent {
	state = {
		arr: [10, 20]
	};

	render() {
		return <div className="voteBox">
			{this.state.arr.map((item, index) => {
				return <span key={index}>
					{item}
				</span>;
			})}
			<button onClick={this.handle}>按鈕</button>
		</div>;
	}
	handle = () => {
		let arr = this.state.arr;
		arr.push(30);
		this.setState({
			arr: [...arr]
	});
}

複製代碼

6.數據管控

咱們把基於狀態或者屬性的更新來驅動視圖渲染,叫受控組件。屬性不能修改,但能夠經過設置默認值、讓父組件從新調用傳遞不一樣的屬性、或者把屬性值賦值給組件的狀態,經過借刀殺人的方式修改屬性。非受控組件: 不受狀態管控,而是直接操做DOM。

屬性——props

給屬性設置規則須要第三方插件【prop-types】 設置的規則不會阻礙內容的渲染,不符合規則的在控制檯報錯。

  • Installation
$ npm install --save prop-types
//&
$yarn add prop-types
複製代碼
  • Importing
import PropTypes from "prop-types";
//&
var PropTypes = require("prop-types");
複製代碼
  • 設置默認值: 在props裏面沒有該屬性值 則會走默認的值
static defaultProps = {
    title:"zZ很懶"
}
複製代碼

PropTypes.isRequired 必須傳遞 PropTypes.string/bool/number/func/object/symbol/node(元素節點)

element(JSX元素)

instanceOf(Xxx)(必須是某個類的實例)

oneOf(["News","photos"])(多箇中的一個)

oneOfType([PropTypes.string,PropTypes.number,PropTypes.instanceOf(Message)])多個類型中的一個

static propTypes = {
    title"PropTypes.string.isRequired } 複製代碼

狀態——state(私有狀態/公共狀態REDUX)

1.在constructor構造函數中初始化,要求後期再組件中使用的狀態都要在這裏初始化一下

2.經過setState方法來通知視圖從新渲染,setState(partialState,callback),在某些狀況下是異步的,也能夠理解在鉤子函數中是局部異步的。須要等待周期函數執行完成,再去修改狀態,保證了周期函數的穩定性,以及總體執行邏輯的完整性。有些狀況是同步的,在設置定時器或者在綁定事件中執行setState,這個時候就是同步的,走修改狀態的流程。

  • 異步狀況:

    • 在鉤子函數componentWillMount中設置setState,爲了提升性能,React爲了不沒必要要的從新渲染,執行完componentWillMount,就當即修改了狀態。實現了局部異步性。
    • 在鉤子函數componentDidMount中設置setState,由於已經執行完render了,因此此時的異步的等到生命週期函數搜執行完再去修改狀態。
  • 同步狀況:

    • 設置定時器: 在修改狀態外加一層定時器,自己就在異步操做裏面,不用考慮定時器內部會影響到組件的生命週期函數的執行順序以及邏輯。就按照修改狀態的順序來便可。
    • 綁定事件: 在給DOM綁定二級事件的時候,自己也是異步的,在觸發某個操做去執行綁定的操做,因此此時的修改狀態也不會去影響到組件正常的生命週期函數的執行邏輯。
  • setState【修改狀態】過程=> shouldComponentUpdate -> WillUpdate -> render(從新渲染:從新構建虛擬的DOM對象->DOM DIFF(補丁包)->差別渲染) -> DidUpdate

  • 【partialState】 第一個參數部分狀態對象【partialState】,修改初始化中的哪一個狀態,在setState中改誰便可,React在處理的時候把以前的狀態和傳遞的partialState進行合併並替換,使用原生Object.assign(this.state,partialState)

  • 【callback】 在視圖從新渲染完成以後執行。

3.批量更新狀態

基於setState修改狀態時的同步異步操做,React把當前異步修改狀態的操做任務放到隊列裏,若是有來個任務修改同一個狀態,則會以最後一個任務爲主。和瀏覽器渲染更新機制一個道理。爲了不沒必要要的渲染,提升性能。

4.this.forceUpdate(): 強制更新,執行該方法就不會再走shouldComponentUpdate,直接修改狀態

5.數據驅動機制比較

  • 【React】 :基於setState/forceUpate來手動添加修改並通知render從新渲染
  • 【vue】 :基於內置的Object.defineProperty/Proxy(3.0版本)的setter通知render執行。
this.state = {
    time: new Date().toLocaleString()
};
render(){
    return <div>
       <p>{this.state.time}</p>
    </div>
}
componentDidMount(){
    //第一次加載組件渲染完畢 等價於 VUE中的MOUNT
    //這種狀況不會通知組件從新渲染
    //this.state.time = "1970年1月1日 0點0分0秒" 
    //每一次修改狀態基於:setState方法 
   setInterval(()=>{
        this.setState({
        time: new Date().toLocaleString();
    },()=>{console.log("疫情很快結束!")})
   },1000);
}
複製代碼

6.refs :是一個對象,對象裏面存儲着有屬性爲ref的元素DOM,從而得到DOM來進行操做。 有倆種寫法: 設置屬性ref=xxXxx,基於this.refs.xxXxx獲取DOM元素。 經過函數的模式,傳遞的參數是當前的DOM元素,而後將DOM元素直接掛載到實例上,後續經過設置的屬性名來獲取相應的DOM元素,進行操做DOM。

<p ref="timeBox">{new Date().toLoacaleString}</p>
//&
<p ref=DOM=>{
    this.timeBox = DOM
}>{new Date().toLoacaleString}</p>
複製代碼
  • React中的事件綁定

    • 原生DOM事件綁定: 基於refs或者經過直接掛載到實例上DOM元素在componentDidMount鉤子函數中綁定事件
    • JSX自帶的合成事件: onXxx

    React爲了處理事件的兼容以及移動端事件的處理,全部的事件都是合成的(事件對象也是本身合成的)=》原理:事件委託,把每一種事件類型都在document上委託一下。

7.類組件的生命週期函數

  • getDefaultProps 獲取屬性的默認值和校驗傳遞屬性的類型
  • constructor getInitialState 在執行getInitialState以前,若是當前類組件有構造函數,則先執行該構造函數,從而初始化狀態,把屬性等信息掛載到實例上
  • componentWillMount 第一次渲染以前,能夠在這裏提早從服務器獲取數據,進而把獲取的數據重賦值給狀態或者存放到redux中。
  • render渲染組件
  • componentDidMount() 第一次組件渲染完成,而後處於運行當中。能夠獲取DOM元素了。

【state改變】

  • shouldComponentUpdate 狀態改變觸發該鉤子函數,此處能夠判斷該更新是否有必要,避免沒必要要的更新,從而獲得優化,返回true或者false來控制是否執行下一個鉤子函數,可是不論返回的哪一個,狀態都已經改了
  • componentWillUpdate在上一個鉤子函數返回true執行,作一系列的更新前準備
  • render再次渲染,渲染狀態改變的部分
  • componentDidUpdate狀態更新完成後,到再次監聽屬性或者狀態的改變。

【props改變】

  • componentWillReceiveProps屬性值改變觸發該鉤子函數,獲取到某一個具體屬性的改變,而後觸發shouldComponentUpdate,看看究竟是否能夠更新。

【卸載】

  • componentWillUnmount 組件卸載以前,作最後一步的收尾工做,而後組件的整個生命週期結束。

代碼示意以及各個周期函數的參數

class Clock extends React.Component {
    static defaultProps = {
        time:"1970年1月1日 0點0分0秒"
    }
    static  propTypes = {
        time:"instanceOf(Date)"
    }
    constructor(props){
        super(props);
        this.state = {t:0}
    }
    componentWillMount(){
        console.log("--- 執行componentWillMount---")
    }
    render(){
        console.log("--- 執行render---")
        return <div>
            <h2>回溯到{this.state.time}</h2>
            <button Onclick={()=>{
                this.setState({t:this.state.t+1})
            }}>{this.state.t}</button>
        </div>
    }
    componentDidMount(){
        console.log("--- 執行componentDidMount---")
    }
    shouldComponentUpdate (nextProps,nextState) {
        console.log("--- 執行shouldComponentUpdate ---",nextProps,nextState)
    }
    componentWillUpdate(){
         console.log("--- 執行componentWillUpdate---")
    }
    //render從新渲染
    componentDidUpdate(){
         console.log("--- 執行componentDidUpdate---")
    }
    componentWillUnmount(){
        console.log("--- 執行componentWillUnmount---")
    }
}
React.render(<section>
<Clock></Clock>
</section>,document.getElementById("root"),_=>{alert("奧利給")})
複製代碼

總結

總結的比較細,相對於初學者更容易理解,若是哪裏有不對的地方,但願各位江湖人士加以指正,哪裏有不懂得地方能夠留言,一塊兒討論。後續不斷更新文章,謝謝各位!!

相關文章
相關標籤/搜索