看源碼一個痛處是會陷進理不順主幹的困局中,本系列文章在實現一個 (x)react 的同時理順 React 框架的主幹內容(JSX/虛擬DOM/組件/生命週期/diff算法/setState/ref/...)html
而在現有 setState 邏輯實現中,每調用一次 setState 就會執行 render 一次。所以在以下代碼中,每次點擊增長按鈕,由於 click 方法裏調用了 10 次 setState 函數,頁面也會被渲染 10 次。而咱們但願的是每點擊一次增長按鈕只執行 render 函數一次。node
export default class B extends Component { constructor(props) { super(props) this.state = { count: 0 } this.click = this.click.bind(this) } click() { for (let i = 0; i < 10; i++) { this.setState({ // 在先前的邏輯中,沒調用一次 setState 就會 render 一次 count: ++this.state.count }) } } render() { console.log(this.state.count) return ( <div> <button onClick={this.click}>增長</button> <div>{this.state.count}</div> </div> ) } }
查閱 setState 的 api,其形式以下:react
setState(updater, [callback])
它能接收兩個參數,其中第一個參數 updater 能夠爲對象或者爲函數 ((prevState, props) => stateChange
),第二個參數爲回調函數;git
肯定優化思路爲:將屢次 setState 後跟着的值進行淺合併,並藉助事件循環等全部值合併好以後再進行渲染界面。github
let componentArr = [] // 異步渲染 function asyncRender(updater, component, cb) { if (componentArr.length === 0) { defer(() => render()) // 利用事件循環,延遲渲染函數的調用 } if (cb) defer(cb) // 調用回調函數 if (_.isFunction(updater)) { // 處理 setState 後跟函數的狀況 updater = updater(component.state, component.props) } // 淺合併邏輯 component.state = Object.assign({}, component.state, updater) if (componentArr.includes(component)) { component.state = Object.assign({}, component.state, updater) } else { componentArr.push(component) } } function render() { let component while (component = componentArr.shift()) { renderComponent(component) // rerender } } // 事件循環,關於 promise 的事件循環和 setTimeout 的事件循環後續會單獨寫篇文章。 const defer = function(fn) { return Promise.resolve().then(() => fn()) }
此時,每點擊一次增長按鈕 render 函數只執行一次了。算法
在 react 中並不建議使用 ref 屬性,而應該儘可能使用狀態提高,可是 react 仍是提供了 ref 屬性賦予了開發者操做 dom 的能力,react 的 ref 有 string
、callback
、createRef
三種形式,分別以下:api
// string 這種寫法將來會被拋棄 class MyComponent extends Component { componentDidMount() { this.refs.myRef.focus() } render() { return <input ref="myRef" /> } } // callback(比較通用) class MyComponent extends Component { componentDidMount() { this.myRef.focus() } render() { return <input ref={(ele) => { this.myRef = ele }} /> } } // react 16.3 增長,其它 react-like 框架尚未同步 class MyComponent extends Component { constructor() { super() { this.myRef = React.createRef() } } componentDidMount() { this.myRef.current.focus() } render() { return <input ref={this.myRef} /> } }
React ref 的前世此生 羅列了三種寫法的差別,下面對上述例子中的第二種寫法(比較通用)進行實現。promise
首先在 setAttribute 方法內補充上對 ref 的屬性進行特殊處理,框架
function setAttribute(dom, attr, value) { ... else if (attr === 'ref') { // 處理 ref 屬性 if (_.isFunction(value)) { value(dom) } } ... }
針對這個例子中 this.myRef.focus()
的 focus 屬性須要異步處理,由於調用 componentDidMount 的時候,界面上還未添加 dom 元素。處理 renderComponent 函數:dom
function renderComponent(component) { ... else if (component && component.componentDidMount) { defer(component.componentDidMount.bind(component)) } ... }
刷新頁面,能夠發現 input 框已爲選中狀態。
處理完普通元素的 ref 後,再來處理下自定義組件的 ref 的狀況。以前默認自定義組件上是沒屬性的,如今只要針對自定義組件的 ref 屬性作相應處理便可。稍微修改 vdomToDom 函數以下:
function vdomToDom(vdom) { if (_.isFunction(vdom.nodeName)) { // 此時是自定義組件 ... for (const attr in vdom.attributes) { // 處理自定義組件的 ref 屬性 if (attr === 'ref' && _.isFunction(vdom.attributes[attr])) { vdom.attributes[attr](component) } } ... } ... }
跑以下測試用例:
class A extends Component { constructor() { super() this.state = { count: 0 } this.click = this.click.bind(this) } click() { this.setState({ count: ++this.state.count }) } render() { return <div>{this.state.count}</div> } } class B extends Component { constructor() { super() this.click = this.click.bind(this) } click() { this.A.click() } render() { return ( <div> <button onClick={this.click}>加1</button> <A ref={(e) => { this.A = e }} /> </div> ) } }
效果以下:
本系列文章拜讀和借鑑了 simple-react,在此特別感謝 Jiulong Hu 的分享。