衆所周知,React 經過聲明式的渲染機制把複雜的 DOM 操做抽象成爲簡單的 state 與 props 操做,一時圈粉無數,一晚上間將前端工程師從麪條式的 DOM 操做中拯救出來。儘管咱們一再強調在 React 開發中儘可能避免 DOM 操做,但在一些場景中仍然沒法避免。固然 React 並無把路堵死,它提供了 ref 用於訪問在 render 方法中建立的 DOM 元素或者是 React 組件實例。javascript
在 React v16.3 以前,ref 經過字符串(string ref)或者回調函數(callback ref)的形式進行獲取,在 v16.3 中,經 0017-new-create-ref 提案引入了新的 React.createRef API。前端
注意:本文如下代碼示例以及源碼均基於或來源於 React v16.3.2 release 版本。java
// string ref
class MyComponent extends React.Component {
componentDidMount() {
this.refs.myRef.focus();
}
render() {
return <input ref="myRef" />;
}
}
// callback ref
class MyComponent extends React.Component {
componentDidMount() {
this.myRef.focus();
}
render() {
return <input ref={(ele) => {
this.myRef = ele;
}} />;
}
}
// React.createRef
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
this.myRef.current.focus();
}
render() {
return <input ref={this.myRef} />;
}
}
複製代碼
在 React.createRef 出現以前,string ref 就已被詬病已久,React 官方文檔直接提出 string ref 將會在將來版本被移出,建議用戶使用 callback ref 來代替,爲什麼須要這麼作呢?主要緣由集中於如下幾點:react
function coerceRef( returnFiber: Fiber, current: Fiber | null, element: ReactElement, ) {
...
const stringRef = '' + element.ref;
// 從 fiber 中獲得實例
let inst = ownerFiber.stateNode;
// ref 閉包函數
const ref = function(value) {
const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
if (value === null) {
delete refs[stringRef];
} else {
refs[stringRef] = value;
}
};
ref._stringRef = stringRef;
return ref;
...
}
複製代碼
class MyComponent extends Component {
renderRow = (index) => {
// string ref 會掛載在 DataTable this 上
return <input ref={'input-' + index} />;
// callback ref 會掛載在 MyComponent this 上
return <input ref={input => this['input-' + index] = input} />;
}
render() {
return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}
複製代碼
/** string ref **/
class Parent extends React.Component {
componentDidMount() {
// 可獲取到 this.refs.childRef
console.log(this.refs);
}
render() {
const { children } = this.props;
return React.cloneElement(children, {
ref: 'childRef',
});
}
}
class App extends React.Component {
componentDidMount() {
// this.refs.child 沒法獲取到
console.log(this.refs);
}
render() {
return (
<Parent>
<Child ref="child" />
</Parent>
);
}
}
/** callback ref **/
class Parent extends React.Component {
componentDidMount() {
// 能夠獲取到 child ref
console.log(this.childRef);
}
render() {
const { children } = this.props;
return React.cloneElement(children, {
ref: (child) => {
this.childRef = child;
children.ref && children.ref(child);
}
});
}
}
class App extends React.Component {
componentDidMount() {
// 能夠獲取到 child ref
console.log(this.child);
}
render() {
return (
<Parent>
<Child ref={(child) => {
this.child = child;
}} />
</Parent>
);
}
}
複製代碼
ReactDOM.render(<App ref="app" />, document.getElementById('main')); 複製代碼
對於靜態類型較不友好,當使用 string ref 時,必須顯式聲明 refs 的類型,沒法完成自動推導。git
編譯器沒法將 string ref 與其 refs 上對應的屬性進行混淆,而使用 callback ref,可被混淆。github
對比新的 createRef 與 callback ref,並無壓倒性的優點,只是但願成爲一個便捷的特性,在性能上會會有微小的優點,callback ref 採用了組件 render 過程當中在閉包函數中分配 ref 的模式,而 createRef 則採用了 object ref。前端工程師
createRef 顯得更加直觀,相似於 string ref,避免了 callback ref 的一些理解問題,對於 callback ref 咱們一般會使用內聯函數的形式,那麼每次渲染都會從新建立,因爲 react 會清理舊的 ref 而後設置新的(見下圖,commitDetachRef -> commitAttachRef),所以更新期間會調用兩次,第一次爲 null,若是在 callback 中帶有業務邏輯的話,可能會出錯,固然能夠經過將 callback 定義成類成員函數並進行綁定的方式避免。閉包
class App extends React.Component {
state = {
a: 1,
};
componentDidMount() {
this.setState({
a: 2,
});
}
render() {
return (
<div ref={(dom) => { // 輸出 3 次 // <div data-reactroot></div> // null // <div data-reactroot></div> console.log(dom); }}></div>
);
}
}
class App extends React.Component {
state = {
a: 1,
};
constructor(props) {
super(props);
this.refCallback = this.refCallback.bind(this);
}
componentDidMount() {
this.setState({
a: 2,
});
}
refCallback(dom) {
// 只輸出 1 次
// <div data-reactroot></div>
console.log(dom);
}
render() {
return (
<div ref={this.refCallback}></div>
);
}
}
複製代碼
不過不得不認可,createRef 在能力上仍遜色於 callback ref,例如上一節提到的組合問題,createRef 也是無能爲力的。在 React v16.3 中,string ref/callback ref 與 createRef 的處理略有差異,讓咱們來看一下 ref 整個構建流程。app
// markRef 前會進行新舊 ref 的引用比較
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
// effectTag 基於位操做,其中有 ref 的變動標誌位
function markRef(workInProgress: Fiber) {
workInProgress.effectTag |= Ref;
}
// effectTag 與 Ref 的 & 操做表示當前 fiber 有 ref 變動
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
// 當前 Host 環境爲 DOM 環境,HostComponent 即爲 DOM 元素,須要藉助實例獲取原生 DOM 元素
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
// 對於 ClassComponent 等而言,直接返回實例便可
default:
instanceToUse = instance;
}
// string ref 與 callback 都會去執行 ref 閉包函數
// createRef 會直接掛在 object ref 的 current 上
if (typeof ref === 'function') {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
複製代碼
以上會涉及 react fiber 的一些概念與細節,好比:fiber 對象含義,fiber tree 構建更新過程,effectTag 的含義與收集過程等等,若是讀者對上述細節不熟悉,可暫時跳過此段內容,不影響對於 ref 的掌握與理解。dom
除了 createRef 之外,React16 還另外提供了一個關於 ref 的 API React.forwardRef,主要用於穿過父元素直接獲取子元素的 ref。在提到 forwardRef 的使用場景以前,咱們先來回顧一下,HOC(higher-order component)在 ref 使用上的問題,HOC 的 ref 是沒法經過 props 進行傳遞的,所以沒法直接獲取被包裹組件(WrappedComponent),須要進行中轉。
function HOCProps(WrappedComponent) {
class HOCComponent extends React.Component {
constructor(props) {
super(props);
this.setWrappedInstance = this.setWrappedInstance.bind(this);
}
getWrappedInstance() {
return this.wrappedInstance;
}
// 實現 ref 的訪問
setWrappedInstance(ref) {
this.wrappedInstance = ref;
}
render() {
return <WrappedComponent ref={this.setWrappedInstance} {...this.props} />;
}
}
return HOCComponent;
}
const App = HOCProps(Wrap);
<App ref={(dom) => {
// 只能獲取到 HOCComponent
console.log(dom);
// 經過中轉後能夠獲取到 WrappedComponent
console.log(dom.getWrappedInstance());
}} />
複製代碼
在擁有 forwardRef 以後,就不須要再經過 getWrappedInstance 了,利用 forwardRef 能直接穿透 HOCComponent 獲取到 WrappedComponent。
function HOCProps(WrappedComponent) {
class HOCComponent extends React.Component {
render() {
const { forwardedRef, ...rest } = this.props;
return <WrappedComponent ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <HOCComponent forwardedRef={ref} {...props} />;
});
}
const App = HOCProps(Wrap);
<App ref={(dom) => {
// 能夠直接獲取 WrappedComponent
console.log(dom);
}} />
複製代碼
React.forwardRef 的原理其實很是簡單,forwardRef 會生成 react 內部一種較爲特殊的 Component。當進行建立更新操做時,會將 forwardRef 組件上的 props 與 ref 直接傳遞給提早注入的 render 函數,來生成 children。
const nextChildren = render(workInProgress.pendingProps, workInProgress.ref);
複製代碼
React refs 到此就所有介紹完了,在 React16 新版本中,新引入了 React.createRef 與 React.forwardRef 兩個 API,有計劃移除老的 string ref,使 ref 的使用更加便捷與明確。若是你的應用已經升級到 React16.3+ 版本,那就放心大膽使用 React.createRef 吧,若是暫時沒有的話,建議使用 callback ref 來代替 string ref。
咱們團隊目前正在深刻研究 React16,歡迎社區小夥伴和咱們一塊兒探討與前行,若是想加入咱們,歡迎私聊或投遞簡歷到 dancang.hj@alibaba-inc.com。