轉(zhuǎn)載自:前端外刊評論-知乎專欄——《深入 React 技術棧》章節(jié)試讀
《深入 React 技術椚氖希》盡管并不是一本外文譯作讥耗,但 React 與 Redux 方面的經(jīng)驗都是從國外的著作、博客與文檔中汲取疹启,結(jié)合對它們的理解與實踐沉淀在 pure render 專欄上古程,所謂知識無國界,前端外刊評論亦是如此皮仁。@寸志 兄在本書寫作期間參與了試讀籍琳,并是本書的推薦人之一,在此非常感謝贷祈。 — 陳屹 @流形
1.6 React 與 DOM
前面已經(jīng)介紹完組件的組成部分了趋急,但還缺少最后一環(huán),那就是將組件渲染到真實 DOM 上势誊。從 React 0.14 版本開始呜达,React 將 React 中涉及 DOM 操作的部分剝離開,目的是為了抽象 React粟耻,同時適用于 Web 端和移動端查近。ReactDOM 的關注點在 DOM 上,因此只適用于 Web 端挤忙。
在 React 組件的開發(fā)實現(xiàn)中霜威,我們并不會用到 ReactDOM,只有在頂層組件以及由于 React 模型所限而不得不操作 DOM 的時候册烈,才會使用它戈泼。
1.6.1 ReactDOM
ReactDOM 中的 API 非常少,只有 findDOMNode
、unmountComponentAtNode
和 render
大猛,我們就以 API 的角度來講講它們的用法扭倾。
findDOMNode
上一節(jié)我們已經(jīng)講過組件的生命周期,DOM 真正被添加到 HTML 中的生命周期方法是 componentDidMount
和 componentDidUpdate
方法挽绩。在這兩個方法中膛壹,我們可以獲取真正的 DOM 元素。React 提供的獲取 DOM 元素的方法有兩種唉堪,其中一種就是 ReactDOM 提供的 findDOMNode
模聋。
DOMElement findDOMNode(ReactComponent component)
當組件被渲染到 DOM 中后,findDOMNode
返回該 React 組件實例相應的 DOM 節(jié)點巨坊。它可以被用于獲取表單的 value 以及 DOM 的測量上撬槽。例如,假設要在當前組件加載完時獲取當前 DOM趾撵,就可以使用 findDOMNode
:
import React, { Component } from 'react';
class App extends Component {
componentDidMount() { // this 為當前組件的實例
const dom = findDOMNode(this);
}
render() {}
}
如果在 render
中返回 null,那么 findDOMNode
也返回 null共啃。findDOMNode
只對已經(jīng)掛載的組件有效占调。
涉及到復雜操作時,還有非常多的原生 DOM API 可以用移剪。但是需要嚴格限制場景究珊,在使用之前多問自己為什么要操作 DOM。
render
為什么說只有在頂層組件我們不得不使用 ReactDOM纵苛,因為我們要把 React 渲染的 Virtual DOM 渲染到瀏覽器的 DOM 當中剿涮,這就要使用 render
方法了。
ReactComponent render( ReactElement element, DOMElement container, [function callback])
render
方法把元素掛載到 container 中攻人,并且返回 element 的實例(即 ref 引用)取试。當然,如果是無狀態(tài)組件怀吻,render 會返回 null瞬浓。當組件裝載完畢時,callback 就會被調(diào)用蓬坡。
當組件在初次渲染之后再次更新時猿棉,React 不會把整個組件重新渲染一次,而會用它高效的 DOM diff 算法做局部的更新屑咳。這也是 React 最大的亮點之一萨赁!
此外,與 render 相反兆龙,React 還提供了一個很少使用 unmountComponentAtNode
方法來做 unMount 的操作杖爽。
1.6.2 ReactDOM 不穩(wěn)定方法
ReactDOM 中有兩個不穩(wěn)定方法,其中一個方法與 render
方法頗為相似。講起它掂林,還得從我們常用的 Dialog
組件在 React 中的實現(xiàn)講起臣缀。
我們先來回憶一下 Dialog
組件的特點,它是不在文檔流中的彈出框泻帮,一般會絕對定位在屏幕的正中央精置,背后有一層半透明的遮罩。因此锣杂,它往往直接渲染在 document.body
下脂倦,然而我們并不知道如何在 React 組件外進行操作。這就要從實現(xiàn) Dialog 的思路以及涉及 DOM 部分的實現(xiàn)講起元莫。
這里我們引入 Portal
組件赖阻,這是一個經(jīng)典的實現(xiàn),最初的實現(xiàn)來源于 React Bootstrap 組件庫中的 Overlay Mixin
踱蠢,后來使用越來越廣泛火欧。我們截取關鍵部分的源碼:
import React from 'react';
import ReactDOM, {findDOMNode} from 'react-dom';
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations';
export default class Portal extends React.Component {
constructor() { // ... }
openPortal(props = this.props) {
this.setState({
active: true
});
this.renderPortal(props);
this.props.onOpen(this.node);
}
closePortal(isUnmounted = false) {
const resetPortalState = () => {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);
document.body.removeChild(this.node);
}
this.portal = null;
this.node = null;
if (isUnmounted !== true) {
this.setState({
active: false
});
}
};
if (this.state.active) {
if (this.props.beforeClose) {
this.props.beforeClose(this.node, resetPortalState);
} else {
resetPortalState();
}
this.props.onClose();
}
}
renderPortal(props) {
if (!this.node) {
this.node = document.createElement('div'); // 在節(jié)點增加到 DOM 之前,執(zhí)行 CSS 防止無效的重繪
this.applyClassNameAndStyle(props);
document.body.appendChild(this.node);
} else { // 當新的 props 傳下來的時候茎截,更新 CSS
this.applyClassNameAndStyle(props);
}
let children = props.children; // https://gist.github.com/jimfb/d99e0678e9da715ccf6454961ef04d1b
if (typeof props.children.type === 'function') {
children = React.cloneElement(props.children, {
closePortal: this.closePortal
});
}
this.portal = ReactDOM.unstable_renderSubtreeIntoContainer( this, children, this.node, this.props.onUpdate );
}
render() {
if (this.props.openByClickOn) {
return React.cloneElement(
this.props.openByClickOn, { onClick: this.handleWrapperClick });
}
return null;
}
}
從 Portal
組件看得出苇侵,我們實現(xiàn)了一個『殼』,包括觸發(fā)事件企锌,渲染的位置以及暴露的方法榆浓,它并不關心子組件的內(nèi)容。當我們使用它的時候撕攒,可以這么寫陡鹃。
<Portal ref="myPortal">
<Modal title="My modal"> Modal content </Modal>
</Portal>
這個組件可以說是 Dialog
實現(xiàn)的精髓,我們 Dialog
的行為抽象了 Portal
這個父組件抖坪。
當我們調(diào)用上述代碼時萍鲸,可以注意到在 componentDidMount
的時候最后調(diào)用了 this.renderPortal()
方法,這個方法把 children 里的內(nèi)容插入到 document.body
下柳击,這就實現(xiàn) children 不在標準文檔流的渲染猿推。
這之間就說到了 ReactDOM 中不穩(wěn)定的 API 方法 unstable_renderSubtreeIntoContainer
。它的作用很簡單捌肴,就是更新組件到傳入的 DOM 節(jié)點上蹬叭,我們在這里使用它完成了在組件內(nèi)實現(xiàn)跨組件的 DOM 操作。
這個方法與 render
是不是很相似状知,但 render 方法缺少了一個插件某一個節(jié)點的參數(shù)秽五。從最終 ReactDOM 方法實現(xiàn)的源代碼 react/src/renderers/dom/client/ReactMount.js
中了解 unstable_renderSubtreeIntoContainer
與 render
方法對應調(diào)用的方法區(qū)別是:
render: ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
unstable_renderSubtreeIntoContainer: ReactMount._renderSubtreeIntoContainer(parentComponent, nextElement, container, callback);
源代碼證明了我們的猜想,也就說明兩者區(qū)別在于是否傳入父節(jié)點饥悴。
另一個不穩(wěn)定方法 unstable_renderSubtreeIntoContainer 關于 setState 的更新策略坦喘,我們會在『$3.4 解密 setState』中詳細介紹盲再。
1.6.3 refs
剛才我們已經(jīng)詳述了 ReactDOM 的 render
方法,比如我們渲染了一個 App 組件到 root節(jié)點下了:
const myAppInstance = ReactDOM.render(<App />,
document.getElementById('root'));
myAppInstance.doSth();
我們利用 render
方法拿到了 App 組件的實例瓣铣,然后就可以對它做一些操作答朋。但在組件內(nèi),JSX 是不會返回一個組件的實例的棠笑!它只是一個 ReactElement梦碗,只是告訴 React 被掛載的組件應該長什么樣。
const myApp = <App />;
refs
就是為此而生的蓖救,它是 React 組件中非常特殊的 prop
洪规,可以附加到任何一個組件上。從字面意思來看循捺,efs
即 reference
斩例,組件被調(diào)用時會新建一個該組件的實例,而 refs
就會指向這個實例从橘。
它可以是一個回調(diào)函數(shù)念赶,這個回調(diào)函數(shù)會在組件被掛載后立即執(zhí)行。例如:
import React, { Component } from 'react';
class App extends Component {
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
if (this.myTextInput !== null) {
this.myTextInput.focus();
}
}
render() {
return (
<div>
<input type="text" ref={(ref) => this.myTextInput = ref} />
<input type="button" value="Focus the text input" onClick={this.handleClick} />
</div> );
}
}
在這個例子里恰力,我們拿到 input 的真正實例晶乔,所以我們可以在按鈕被按下后調(diào)用輸入框的 focus()
方法。這個例子把 refs
放到原生的 DOM 組件 input 中牺勾,我們可以通過 refs
得到 DOM 節(jié)點;而如果把 refs
放到 React 組件阵漏,比如 <TextInput />
驻民,我們獲得的就是 TextInput
的實例,因此我們可以調(diào)用 TextInput
的實例方法履怯。
refs
同樣支持字符串回还。對于 DOM 操作,我們不僅可以使用 findDOMNode
獲得該組件 DOM叹洲,還可以使用 ref
獲得組件內(nèi)部的 DOM柠硕。比如:
import React, { Component } from 'react';
class App extends Component {
componentDidMount() { // myComp 是 Comp 的一個實例,因此需要用 findDOMNode 轉(zhuǎn)換為相應的 DOM
const myComp = this.refs.myComp;
const dom = findDOMNode(myComp);
}
render() {
return (
<div>
<Comp ref="myComp" />
</div> );
}
}
要獲取一個 React 組件的引用运提,既可以使用 this 來獲取當前 React 組件蝗柔,也可以使用 refs
來獲取你擁有的子組件的引用。
我們回到 1.6.2 節(jié)中 Portal
組件里暴露的兩個方法 openPortal
和 closePortal
民泵。這兩個方法的調(diào)用方式為:
this.refs.myPortal.openPortal();
this.refs.myPortal.closePortal();
這種命令式調(diào)用的方式癣丧,盡管說并不是 React 推崇的,但我們?nèi)匀豢梢允褂谜蛔薄_@里我們原則上在組件狀態(tài)維護上不建議用這種方式胁编。
為了防止內(nèi)存泄露厢钧,當一個組件卸載的時候,組件里所有的 ref
就會變?yōu)?null嬉橙。
值得注意的是早直,findDOMNode
和 refs
都無法用于無狀態(tài)組件中,原因在前面已經(jīng)說過市框。無狀態(tài)組件掛載時只是方法調(diào)用霞扬,沒有新建實例。
對于 React 組件來說拾给,refs
會指向一個組件類的實例祥得,所以可以調(diào)用該類定義的任何方法。如果需要訪問該組件的真實 DOM蒋得,可以用 ReactDOM.findDOMNode
來找到 DOM 節(jié)點级及,但我們并不推薦這樣做。因為這在大部分情況下都打破了封裝性额衙,而且通常都能用更清晰的辦法在 React 中構(gòu)建代碼饮焦。
1.6.4 React 之外的 DOM 操作
DOM 操作可以歸納為對 DOM 的增刪改查。這里的『查』指的是對 DOM 屬性窍侧、樣式的查看县踢,比如查看 DOM 的位置、寬高信息伟件。而要對 DOM 進行增刪改查硼啤,就要先 query 到 DOM。
React 的聲明式渲染機制斧账,把復雜的 DOM 操作抽象為簡單的 state
和 props
的操作谴返,因此避免了很多直接的 DOM 操作。不過咧织,仍然有一些 DOM 操作是 React 無法避免或者正在努力避免的嗓袱。
舉一個明顯的例子,如果要調(diào)用 HTML5 Audio/Video 的 play
方法习绢,input 的 focus
方法渠抹,React 就無能為力了。這時我們只能使用相應的 DOM 方法來實現(xiàn)闪萄。
React 提供了事件綁定的功能梧却,但是仍然有一些特殊情況需要自行綁定事件。例如 Popup
等類似組件桃煎,當點擊組件其它區(qū)域可以收縮此類組件篮幢。這就要求我們對組件以外的區(qū)域(一般指 document
、body
)進行事件綁定为迈。例如:
componentDidUpdate(prevProps, prevState) {
if (!this.state.isActive && prevState.isActive) {
document.removeEventListener('click', this.hidePopup);
}
if (this.state.isActive && !prevState.isActive) {
document.addEventListener('click', this.hidePopup);
}
}
componentWillUnmount() {
document.removeEventListener('click', this.hidePopup);
}
hidePopup(e) {
if (!this.isMounted()) {
return false;
}
const node = ReactDOM.findDOMNode(this);
const target = e.target || e.srcElement;
const isInside = node.contains(target);
if (this.state.isActive && !isInside) {
this.setState({
isActive: false,
});
}
}
React 中使用 DOM 最多的還是計算 DOM 的尺寸(即位置信息)三椿。我們可以提供像 width
缺菌,或 height
這樣的工具函數(shù):
function width(el) {
const styles = el.ownerDocument.defaultView.getComputedStyle(el, null);
const width = parseFloat(styles.width.indexOf('px') !== -1 ? styles.width : 0);
const boxSizing = styles.boxSizing || 'content-box';
if (boxSizing === 'border-box') {
return width;
}
const borderLeftWidth = parseFloat(styles.borderLeftWidth);
const borderRightWidth = parseFloat(styles.borderRightWidth);
const paddingLeft = parseFloat(styles.paddingLeft);
const paddingRight = parseFloat(styles.paddingRight);
return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight;
}
但上述計算方法并不能完全覆蓋所有情況,這需要付出不少的成本去實現(xiàn)搜锰。值得高興的是伴郁,React 正在自己構(gòu)建一個 DOM 排列模型,來努力避免這些 React 之外的 DOM 操作蛋叼。我們相信在不久的將來焊傅,React 的使用者就可以完全拋棄掉 jQuery 等 DOM 操作庫。
可以說在 React 組件開發(fā)中狈涮,還有很多意料之外的情形狐胎。在這些情形中,應該如何運用 React 的方式優(yōu)雅地解決問題是我們需要一直思考的歌馍。