1 基本概念:Component(組件)肯夏、instance(組件實(shí)例)惕橙、 element锐想、jsx、dom
Component(組件)
Component就是我們經(jīng)常實(shí)現(xiàn)的組件悍赢,可以是類組件(class component)或者函數(shù)式組件(functional component)
1.而類組件又可以分為普通類組件(React.Component)以及純類組件(React.PureComponent),總之這兩類都屬于類組件,只不過PureComponent基于shouldComponentUpdate做了一些優(yōu)化左权。
2.函數(shù)式組件則用來簡化一些簡單組件的實(shí)現(xiàn)皮胡,用起來就是寫一個(gè)函數(shù),
入?yún)⑹墙M件屬性props赏迟,出參與類組件的render方法返回值一樣屡贺,
是react element(注意這里已經(jīng)出現(xiàn)了接下來要介紹的element哦)。
下面我們分別按三種方式實(shí)現(xiàn)下Welcome組件:
// Component
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// PureComponent
class Welcome extends React.PureComponent {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// functional component
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
instance(組件實(shí)例)
熟悉面向?qū)ο缶幊?/code>的人肯定知道
的使用過程中并不會自己去實(shí)例化一個(gè)類
和實(shí)例
的關(guān)系锌杀,這里也是一樣的甩栈,組件實(shí)例
其實(shí)就是一個(gè)組件類
實(shí)例化的結(jié)果,概念雖然簡單糕再,但是在react
這里卻容易弄不明白量没,為什么這么說呢?因?yàn)榇蠹以?code>react組件實(shí)例
亿鲜,這個(gè)過程其實(shí)是react
內(nèi)部幫我們完成的允蜈,因此我們真正接觸組件實(shí)例
的機(jī)會并不多。我們更多接觸到的是下面要介紹的element
蒿柳,因?yàn)槲覀兺ǔ懙?code>jsx其實(shí)就是element
的一種表示方式而已(后面詳細(xì)介紹)饶套。雖然組件實(shí)例
用的不多,但是偶爾也會用到垒探,其實(shí)就是ref
妓蛮。ref
可以指向一個(gè)dom節(jié)點(diǎn)
或者一個(gè)類組件(class component)
的實(shí)例,但是不能用于函數(shù)式組件
圾叼,因?yàn)?code>函數(shù)式組件不能實(shí)例化
蛤克。這里簡單介紹下ref
,我們只需要知道ref
可以指向一個(gè)組件實(shí)例
即可夷蚊,更加詳細(xì)的介紹大家可以看react
官方文檔Refs and the DOM构挤。
前面已經(jīng)提到了element
,即類組件
的render方法
以及函數(shù)式組件
的返回值均為
element
惕鼓。那么這里的element到底是什么呢筋现?其實(shí)很簡單,就是一個(gè)純對象(plain object
)箱歧,而且這個(gè)純對象包含兩個(gè)屬性:type:(string|ReactClass)
和props:Object
矾飞,注意element并不是組件實(shí)例,而是一個(gè)純對象呀邢。雖然element不是組件實(shí)例洒沦,但是又跟組件實(shí)例有關(guān)系,element是對組件實(shí)例或者dom節(jié)點(diǎn)的描述
价淌。如果type是string
類型申眼,則表示dom
節(jié)點(diǎn)瞒津,如果type是function
或者class
類型,則表示組件實(shí)例括尸。比如下面兩個(gè)element分別描述了一個(gè)dom節(jié)點(diǎn)
和一個(gè)組件實(shí)例
:
// 描述dom節(jié)點(diǎn)
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
function Button(props){
// ...
}
// 描述組件實(shí)例
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
jsx
只要弄明白了element仲智,那么jsx就不難理解了,jsx只是換了一種寫法姻氨,方便我們來創(chuàng)建element而已钓辆,想想如果沒有jsx那么我們開發(fā)效率肯定會大幅降低,而且代碼肯定非常不利于維護(hù)肴焊。比如我們看下面這個(gè)jsx的例子
const foo = <div id="foo">Hello!</div>;
其實(shí)說白了就是定義了一個(gè)dom節(jié)點(diǎn)div前联,并且該節(jié)點(diǎn)的屬性集合是{id: 'foo'},children是Hello!娶眷,就這點(diǎn)信息量而已似嗤,因此完全跟下面這種純對象的表示是等價(jià)的:
{
type: 'div',
props: {
id: 'foo',
children: 'Hello!'
}
}
那么React
是如何將jsx
語法轉(zhuǎn)換為純對象的呢?其實(shí)就是利用Babel
編譯生成的届宠,我們只要在使用jsx
的代碼里加上個(gè)編譯指示(pragma)
即可烁落,可以參考這里Babel如何編譯jsx。比如我們將編譯指示
設(shè)置為指向createElement
函數(shù):/** @jsx createElement */
豌注,那么前面那段jsx
代碼就會編譯為:
var foo = createElement('div', {id:"foo"}, 'Hello!');
可以看出伤塌,jsx的編譯過程其實(shí)就是從<、>這種標(biāo)簽式寫法到函數(shù)調(diào)用式寫法的一種轉(zhuǎn)化而已轧铁。有了這個(gè)前提每聪,我們只需要簡單實(shí)現(xiàn)下createElement函數(shù)不就可以構(gòu)造出element了嘛,我們后面自己實(shí)現(xiàn)簡版react也會用到這個(gè)函數(shù):
function createElement(type, props, ...children) {
props = Object.assign({}, props);
props.children = [].concat(...children)
.filter(child => child != null && child !== false)
.map(child => child instanceof Object ? child : createTextElement(child));
return {type, props};
}
dom
dom我們這里也簡單介紹下齿风,作為一個(gè)前端研發(fā)人員药薯,想必大家對這個(gè)概念應(yīng)該再熟悉不過了。我們可以這樣創(chuàng)建一個(gè)dom節(jié)點(diǎn)div:
const divDomNode = window.document.createElement('div');
其實(shí)所有dom節(jié)點(diǎn)都是HTMLElement類的實(shí)例救斑,我們可以驗(yàn)證下:
window.document.createElement('div') instanceof window.HTMLElement;
// 輸出 true
關(guān)于HTMLElement
API可以參考這里:HTMLElement介紹童本。因此,dom
節(jié)點(diǎn)是HTMLElement類
的實(shí)例脸候;同樣的穷娱,在react
里面,組件實(shí)例
是組件類
的實(shí)例纪他,而element
又是對組件實(shí)例
和dom
節(jié)點(diǎn)的描述鄙煤,現(xiàn)在這些概念之間的關(guān)系大家應(yīng)該都清楚了吧晾匠。介紹完了這幾個(gè)基本概念茶袒,我們畫個(gè)圖來描述下這幾個(gè)概念之間的關(guān)系:
2 虛擬dom與diff算法
相信使用過react的同學(xué)都多少了解過這兩個(gè)概念:虛擬dom以及diff算法
。這里的虛擬dom其實(shí)就是前面介紹的element
凉馆,為什么說是虛擬dom呢薪寓,前面咱們已經(jīng)介紹過了亡资,element只是dom節(jié)點(diǎn)或者組件實(shí)例的一種純對象描述
而已,并不是真正的dom節(jié)點(diǎn)向叉,因此是虛擬dom锥腻。react給我們提供了聲明式的組件寫法
,當(dāng)組件的props或者state變化時(shí)組件自動更新
母谎。整個(gè)頁面其實(shí)可以對應(yīng)到一棵dom節(jié)點(diǎn)樹
瘦黑,每次組件props或者state變更首先會反映到虛擬dom樹
,然后最終反應(yīng)到頁面dom節(jié)點(diǎn)樹的渲染
奇唤。
那么虛擬dom跟diff算法又有什么關(guān)系呢幸斥?之所以有diff算法其實(shí)是為了提升渲染效率
,試想下咬扇,如果每次組件的state或者props變化后都把所有相關(guān)dom節(jié)點(diǎn)刪掉再重新創(chuàng)建
甲葬,那效率肯定非常低
,所以在react內(nèi)部存在兩棵虛擬dom樹
懈贺,分別表示現(xiàn)狀
以及下一個(gè)狀態(tài)
经窖,setState調(diào)用后就會觸發(fā)diff算法的執(zhí)行
,而好的diff算法肯定是盡可能復(fù)用已有的dom節(jié)點(diǎn)
梭灿,避免重新創(chuàng)建
的開銷画侣。我用下圖來表示虛擬dom和diff算法的關(guān)系:
react組件最初渲染到頁面后先生成第1幀虛擬dom
,這時(shí)current指針指向該第一幀堡妒。setState調(diào)用后會生成第2幀
虛擬dom棉钧,這時(shí)next指針指向第二幀
,接下來diff算法
通過比較第2幀和第1幀的異同來將更新應(yīng)用到真正的dom樹
以完成頁面更新涕蚤。
這里再次強(qiáng)調(diào)一下setState后具體怎么生成虛擬dom
宪卿,因?yàn)檫@點(diǎn)很重要,而且容易忽略万栅。其實(shí)剛剛已經(jīng)介紹過什么是虛擬dom了佑钾,其實(shí)就是element樹
而已。那element樹是怎么來的呢烦粒?其實(shí)就是render方法
返回的嘛休溶,下面的流程圖再加深下印象:
react組件最初渲染到頁面后先生成第1幀虛擬dom,這時(shí)current指針指向該第一幀扰她。setState調(diào)用后會生成第2幀虛擬dom兽掰,這時(shí)next指針指向第二幀,接下來diff算法通過比較第2幀和第1幀的異同來將更新應(yīng)用到真正的dom樹以完成頁面更新徒役。
這里再次強(qiáng)調(diào)一下setState后具體怎么生成虛擬dom孽尽,因?yàn)檫@點(diǎn)很重要,而且容易忽略忧勿。其實(shí)剛剛已經(jīng)介紹過什么是虛擬dom了杉女,其實(shí)就是element樹而已瞻讽。那element樹是怎么來的呢?其實(shí)就是render方法返回的嘛熏挎,下面的流程圖再加深下印象:
其實(shí)react官方對diff算法有另外一個(gè)稱呼速勇,大家肯定會在react相關(guān)資料中看到,叫Reconciliation
坎拐,我個(gè)人認(rèn)為這個(gè)詞有點(diǎn)晦澀難懂烦磁,不過后來又重新翻看了下詞典,發(fā)現(xiàn)其實(shí)跟diff算法一個(gè)意思:
可以看到reconcile有消除分歧哼勇、核對
的意思个初,在react語境下就是對比虛擬dom
異同的意思,其實(shí)就是說的diff算法猴蹂。這里強(qiáng)調(diào)下院溺,我們后面實(shí)現(xiàn)部實(shí)現(xiàn)reconcile
函數(shù),其實(shí)就是實(shí)現(xiàn)diff算法
磅轻。
3 生命周期與diff算法
生命周期與diff算法又有什么關(guān)系呢珍逸?這里我們以componentDidMount
、componentWillUnmount
聋溜、ComponentWillUpdate
以及componentDidUpdate
為例說明下二者的關(guān)系谆膳。我們知道,setState調(diào)用后會接著調(diào)用render生成新的虛擬dom樹
撮躁,而這個(gè)虛擬dom樹與上一幀可能會產(chǎn)生如下區(qū)別:
1.新增了某個(gè)組件漱病;
2.刪除了某個(gè)組件;
3.更新了某個(gè)組件的部分屬性把曼。
因此杨帽,我們在實(shí)現(xiàn)diff算法的過程會在相應(yīng)的時(shí)間節(jié)點(diǎn)
調(diào)用這些生命周期函數(shù)。
這里需要重點(diǎn)說明下前面提到的第1幀
嗤军,我們知道每個(gè)react應(yīng)用的入口都是:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
ReactDom.render
也會生成一棵虛擬dom樹
注盈,但是這棵虛擬dom樹是開天辟地生成的``第一幀,沒有前一幀用來做diff叙赚,因此這棵
虛擬dom樹對應(yīng)的所有組件都只會調(diào)用掛載期的生命周期函數(shù)老客,比如
componentDidMount,
componentWillUnmount`。
4 實(shí)現(xiàn)
掌握了前面介紹的這些概念震叮,實(shí)現(xiàn)一個(gè)簡版react也就不難了胧砰。首先看一下我們要實(shí)現(xiàn)哪些API,我們最終會以如下方式使用:
// 聲明編譯指示
/** @jsx DiyReact.createElement */
// 導(dǎo)入我們下面要實(shí)現(xiàn)的API
const DiyReact = importFromBelow();
// 業(yè)務(wù)代碼
const randomLikes = () => Math.ceil(Math.random() * 100);
const stories = [
{name: "DiyReact介紹", url: "http://google.com", likes: randomLikes()},
{name: "Rendering DOM elements ", url: "http://google.com", likes: randomLikes()},
{name: "Element creation and JSX", url: "http://google.com", likes: randomLikes()},
{name: "Instances and reconciliation", url: "http://google.com", likes: randomLikes()},
{name: "Components and state", url: "http://google.com", likes: randomLikes()}
];
class App extends DiyReact.Component {
render() {
return (
<div>
<h1>DiyReact Stories</h1>
<ul>
{this.props.stories.map(story => {
return <Story name={story.name} url={story.url} />;
})}
</ul>
</div>
);
}
componentWillMount() {
console.log('execute componentWillMount');
}
componentDidMount() {
console.log('execute componentDidMount');
}
componentWillUnmount() {
console.log('execute componentWillUnmount');
}
}
class Story extends DiyReact.Component {
constructor(props) {
super(props);
this.state = {likes: Math.ceil(Math.random() * 100)};
}
like() {
this.setState({
likes: this.state.likes + 1
});
}
render() {
const {name, url} = this.props;
const {likes} = this.state;
const likesElement = <span />;
return (
<li>
<button onClick={e => this.like()}>{likes}<b>??</b></button>
<a href={url}>{name}</a>
</li>
);
}
// shouldcomponentUpdate() {
// return true;
// }
componentWillUpdate() {
console.log('execute componentWillUpdate');
}
componentDidUpdate() {
console.log('execute componentDidUpdate');
}
}
// 將組件渲染到根dom節(jié)點(diǎn)
DiyReact.render(<App stories={stories} />, document.getElementById("root"));
我們在這段業(yè)務(wù)代碼里面使用了render苇瓣、createElement以及Component三個(gè)API尉间,因此后面的任務(wù)就是實(shí)現(xiàn)這三個(gè)API并包裝到一個(gè)函數(shù)importFromBelow內(nèi)即可。
4.1 實(shí)現(xiàn)createElement
createElement函數(shù)的功能跟jsx是緊密相關(guān)的,前面介紹jsx的部分已經(jīng)介紹過了乌妒,其實(shí)就是把類似html的標(biāo)簽式寫法轉(zhuǎn)化為純對象element,具體實(shí)現(xiàn)如下:
function createElement(type, props, ...children) {
props = Object.assign({}, props);
props.children = [].concat(...children)
.filter(child => child != null && child !== false)
.map(child => child instanceof Object ? child : createTextElement(child));
return {type, props};
}
// rootInstance用來緩存一幀虛擬dom
let rootInstance = null;
function render(element, parentDom) {
// prevInstance指向前一幀
const prevInstance = rootInstance;
// element參數(shù)指向新生成的虛擬dom樹
const nextInstance = reconcile(parentDom, prevInstance, element);
// 調(diào)用完reconcile算法(即diff算法)后將rooInstance指向最新一幀
rootInstance = nextInstance;
}
rende
r函數(shù)實(shí)現(xiàn)很簡單外邓,只是進(jìn)行了兩幀虛擬dom的對比(reconcile)
撤蚊,然后將rootInstance指向新的虛擬dom
。細(xì)心點(diǎn)會發(fā)現(xiàn)损话,新的虛擬dom為element侦啸,即最開始介紹的element,而reconcile
后的虛擬dom是instance
丧枪,不過這個(gè)instance并不是組件實(shí)例光涂,這點(diǎn)看后面instantiate
的實(shí)現(xiàn)∨》常總之render方法其實(shí)就是調(diào)用了reconcile
方法進(jìn)行了兩幀虛擬dom
的對比而已忘闻。
4.3 實(shí)現(xiàn)instantiate
那么前面的instance到底跟element有什么不同呢?其實(shí)instance指示簡單的是把element重新包了一層恋博,并把對應(yīng)的dom也給包了進(jìn)來齐佳,這也不難理解,畢竟我們調(diào)用reconcile進(jìn)行diff比較的時(shí)候需要把跟新應(yīng)用到真實(shí)的dom上债沮,因此需要跟dom關(guān)聯(lián)起來炼吴,下面實(shí)現(xiàn)的instantiate函數(shù)就干這個(gè)事的。注意由于element包括dom類型和Component類型(由type字段判斷疫衩,不明白的話可以回過頭看一下第一節(jié)的element相關(guān)介紹)硅蹦,因此需要分情況處理:
dom類型的element.type為string類型,對應(yīng)的instance結(jié)構(gòu)為{element, dom, childInstances}闷煤。
Component類型的element.type為ReactClass類型童芹,對應(yīng)的instance結(jié)構(gòu)為{dom, element, childInstance, publicInstance},注意這里的publicInstance就是前面介紹的組件實(shí)例鲤拿。
function instantiate(element) {
const {type, props = {}} = element;
const isDomElement = typeof type === 'string';
if (isDomElement) {
// 創(chuàng)建dom
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement ? document.createTextNode('') : document.createElement(type);
// 設(shè)置dom的事件辐脖、數(shù)據(jù)屬性
updateDomProperties(dom, [], element.props);
const children = props.children || [];
const childInstances = children.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = {element, dom, childInstances};
return instance;
} else {
const instance = {};
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
Object.assign(instance, {dom: childInstance.dom, element, childInstance, publicInstance});
return instance;
}
}
需要注意,由于dom節(jié)點(diǎn)
和組件實(shí)例
都可能有孩子節(jié)點(diǎn)皆愉,因此instantiate函數(shù)中有遞歸實(shí)例化的邏輯嗜价。
4.4 實(shí)現(xiàn)reconcile(diff算法)
重點(diǎn)來了,reconcile是react的核心幕庐,顯然如何將新設(shè)置的state
快速的渲染出來非常重要久锥,因此react
會盡量復(fù)用已有節(jié)點(diǎn),而不是每次都動態(tài)創(chuàng)建所有相關(guān)節(jié)點(diǎn)异剥。但是react
強(qiáng)大的地方還不僅限于此瑟由,react16
將reconcile
算法由之前的stack
架構(gòu)升級成了fiber
架構(gòu),更近一步做的性能優(yōu)化冤寿。fiber相關(guān)的內(nèi)容下一節(jié)再介紹歹苦,這里為了簡單易懂青伤,仍然使用類似stack架構(gòu)的算法來實(shí)現(xiàn),對于fiber
現(xiàn)在只需要知道其調(diào)度原理即可殴瘦,當(dāng)然后面有時(shí)間可以再實(shí)現(xiàn)一版基于fiber架構(gòu)的狠角。
首先看一下整個(gè)reconcile
算法的處理流程
可以看到,我們會根據(jù)不同的情況做不同的處理:
1.如果是新增instance
蚪腋,那么需要實(shí)例化一個(gè)instance并且appendChild
丰歌;
2.如果是不是新增instance
,而是刪除instance
屉凯,那么需要removeChild
立帖;
3.如果既不是新增也不是刪除instance
,那么需要看instance
的type
是否變化悠砚,如果有變化晓勇,那節(jié)點(diǎn)就無法復(fù)用了,也需要實(shí)例化instance
灌旧,然后replaceChild
宵蕉;
4.如果type
沒變化就可以復(fù)用已有節(jié)點(diǎn)了,這種情況下要判斷是原生dom
節(jié)點(diǎn)還是我們自定義實(shí)現(xiàn)的react
節(jié)點(diǎn)节榜,兩種情況下處理方式不同羡玛。
大流程了解后,我們只需要在對的時(shí)間點(diǎn)執(zhí)行生命周期函數(shù)即可宗苍,下面看具體實(shí)現(xiàn)
function reconcile(parentDom, instance, element) {
if (instance === null) {
const newInstance = instantiate(element);
// componentWillMount
newInstance.publicInstance
&& newInstance.publicInstance.componentWillMount
&& newInstance.publicInstance.componentWillMount();
parentDom.appendChild(newInstance.dom);
// componentDidMount
newInstance.publicInstance
&& newInstance.publicInstance.componentDidMount
&& newInstance.publicInstance.componentDidMount();
return newInstance;
} else if (element === null) {
// componentWillUnmount
instance.publicInstance
&& instance.publicInstance.componentWillUnmount
&& instance.publicInstance.componentWillUnmount();
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type !== element.type) {
const newInstance = instantiate(element);
// componentDidMount
newInstance.publicInstance
&& newInstance.publicInstance.componentDidMount
&& newInstance.publicInstance.componentDidMount();
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === 'string') {
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
if (instance.publicInstance
&& instance.publicInstance.shouldcomponentUpdate) {
if (!instance.publicInstance.shouldcomponentUpdate()) {
return;
}
}
// componentWillUpdate
instance.publicInstance
&& instance.publicInstance.componentWillUpdate
&& instance.publicInstance.componentWillUpdate();
instance.publicInstance.props = element.props;
const newChildElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
const newChildInstance = reconcile(parentDom, oldChildInstance, newChildElement);
// componentDidUpdate
instance.publicInstance
&& instance.publicInstance.componentDidUpdate
&& instance.publicInstance.componentDidUpdate();
instance.dom = newChildInstance.dom;
instance.childInstance = newChildInstance;
instance.element = element;
return instance;
}
}
function reconcileChildren(instance, element) {
const {dom, childInstances} = instance;
const newChildElements = element.props.children || [];
const count = Math.max(childInstances.length, newChildElements.length);
const newChildInstances = [];
for (let i = 0; i < count; i++) {
newChildInstances[i] = reconcile(dom, childInstances[i], newChildElements[i]);
}
return newChildInstances.filter(instance => instance !== null);
}
看完reconcile算法后肯定有人會好奇稼稿,為什么這種算法叫做stack
算法,這里簡單解釋一下讳窟。從前面的實(shí)現(xiàn)可以看到让歼,每次組件的state
更新都會觸發(fā)reconcile
的執(zhí)行,而reconcile
的執(zhí)行也是一個(gè)遞歸過程丽啡,而且一開始直到遞歸執(zhí)行完所有節(jié)點(diǎn)才停止谋右,因此成為stack算法。由于是個(gè)遞歸過程补箍,因此該diff算法一旦開始就必須執(zhí)行完改执,因此可能會阻塞線程,又由于js是單線程的坑雅,因此這時(shí)就可能會影響用戶的輸入或者ui的渲染幀頻辈挂,降低用戶體驗(yàn)。不過react16中升級為了fiber
架構(gòu)裹粤,這一問題得到了解決终蒂。
把前面實(shí)現(xiàn)的所有這些代碼組合起來就是完整的簡版react,不到200行代碼,希望大家多度指教