Open buxuku opened 3 years ago
前面已经实现了组件的更新,在组件的更新过程,类组件还提供了一系列的钩子函数,也就是生命周期.从组件的挂载,更新,卸载的不同生命周期里面可以执行一系列我们想执行的方法.
先来看看一张生命周期图
这个阶段设置props和state,因为现在是采用的是es6的写法,所以这个阶段都是在constructor里面进行完成了.这里我们只是需要处理一下组件的defaultProps即可,因为组件有可能会设置一些默认的props属性.
constructor
defaultProps
props
在src/react-dom里面的mountClassComponent方法里面
src/react-dom
mountClassComponent
function mountClassComponent(vdom) { const {type, props, ref} = vdom; - const classInstance = new type(props); + const defaultProps = type.defaultProps || {}; + const classInstance = new type({...defaultProps, ...props}); if(classInstance.componentWillMount) classInstance.componentWillMount(); const classInstanceVdom = wrapToVdom(classInstance.render()); classInstance.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面; vdom.classInstance = classInstance; if (ref) { ref.current = classInstance; } const dom = createDom(classInstanceVdom); if(classInstance.componentDidMount) dom.componentDidMount = classInstance.componentDidMount.bind(classInstance); return dom; }
在src/components/LifeCycle.js里面创建一个组件
src/components/LifeCycle.js
import React from "../react"; class LifeCycle extends React.Component { static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } render() { const { count } = this.state; return (<div>{this.props.name}: {count}</div>) } } export default LifeCycle;
在src/index.js导入这个组件,它应该就能正常渲染出props和state值了.
src/index.js
state
import React from './react'; import ReactDOM from './react-dom'; import LifeCycle from './components/LifeCycle'; ReactDOM.render( <LifeCycle />, document.getElementById('root') );
在挂载阶段,render之前会执行componentWillMount方法,这个基本比较简单,在mountClassComponent方法里面,调用render之前判断实例上面是否添加了componentWillMount方法,有的话,执行一下即可.
render
componentWillMount
function mountClassComponent(vdom) { const {type, props, ref} = vdom; const defaultProps = type.defaultProps || {}; const classInstance = new type({...defaultProps, props}); + if(classInstance.componentWillMount) classInstance.componentWillMount(); const classInstanceVdom = wrapToVdom(classInstance.render()); classInstance.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面; vdom.classInstance = classInstance; if (ref) { ref.current = classInstance; } return createDom(classInstanceVdom);; }
那componentDidMount不也一样吗,在render这个方法之后调用一下就可以了吧,但要注意的是,render只是生成了虚拟Dom,并还没把虚拟Dom挂载到真实的节点上面,而这个方法是需要在真实Dom渲染完毕之后再执行.那这里我们可以先把这个函数挂载到dom上面,当节点插入到真实Dom中之后,再执行这个方法.
componentDidMount
dom
继续修改mountClassComponent这个方法,将componentDidMount挂载到dom上面.
function mountClassComponent(vdom) { const {type, props, ref} = vdom; const defaultProps = type.defaultProps || {}; const classInstance = new type({...defaultProps, props}); if(classInstance.componentWillMount) classInstance.componentWillMount(); const classInstanceVdom = wrapToVdom(classInstance.render()); classInstance.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面; vdom.classInstance = classInstance; if (ref) { ref.current = classInstance; } - return createDom(classInstanceVdom); + const dom = createDom(classInstanceVdom); + if(classInstance.componentDidMount) dom.componentDidMount = classInstance.componentDidMount.bind(classInstance); + return dom; }
这个方法只会在组件第一次挂载完成后执行,那哪些时机会触发组件的挂载完成呢?
基于以上几种场景,我们分别在对应的时机节点进行调用处理即可.
function render(vdom, container) { if (isNotNeedRender(vdom)) return // 如果vdom不存在,则不需要创建真实dom; const dom = createDom(vdom); container.appendChild(dom); + if(dom.componentDidMount) dom.componentDidMount(); } export function compareTwoVdoms(oldVdom, newVdom, parentDom, nextDom) { if (!oldVdom && !newVdom) return; if (oldVdom && newVdom && (oldVdom.type !== newVdom.type)) { // 如果节点类型变了,直接进行全量更新 const oldDom = findDom(oldVdom); const newDom = createDom(newVdom); oldDom.parentNode.replaceChild(newDom, oldDom); + if(newVdom.componentDidMount) newVdom.componentDidMount(); } else if (oldVdom && !newVdom) { const oldDom = findDom(oldVdom); oldDom.parentNode.removeChild(oldDom); } else if (!oldVdom && newVdom) { const newDom = createDom(newVdom); if (nextDom) { // 如果后面有节点,则应该进行插入操作 parentDom.insertBefore(newDom, nextDom); } else { parentDom.appendChild(newDom); } + if(newVdom.componentDidMount) newVdom.componentDidMount(); } else { updateElement(oldVdom, newVdom); } } function path(diffQueue){ // 1.删除要删除的 let deleteMap = {}; let deleteChildren = []; diffQueue.forEach((item) => { const {type, fromIndex, toIndex} = item; if(type === MOVE || type === REMOVE){ const oldChild = item.parentDom.childNodes[fromIndex]; deleteMap[fromIndex] = oldChild; deleteChildren.push(oldChild); } }); deleteChildren.forEach(item => { item.parentNode.removeChild(item); }); diffQueue.forEach((item) => { const { type, fromIndex, toIndex, parentDom, dom} = item; if(type === INSERT){ insertChildAt(parentDom, dom, toIndex) + if(dom.componentDidMount) dom.componentDidMount(); } if(type === MOVE){ insertChildAt(parentDom, deleteMap[fromIndex], toIndex) } }) }
再次调整之前的LifeCycle组件,模拟几种触发组件完成挂载的场景.
LifeCycle
import React from "../react"; class ChildOne extends React.Component{ componentDidMount(){ console.log('childOne componentDidMount'); } render(){ return <p>childOne</p> } } class ChildTwo extends React.Component{ componentDidMount(){ console.log('childTwo componentDidMount'); } render(){ return <p>childTwo</p> } } class ChildOther extends React.Component{ componentDidMount(){ console.log('childOther componentDidMount'); } render(){ return <p>childOther</p> } } class LifeCycle extends React.Component { static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } handleClick = () => { this.setState({ count: this.state.count + 1, }); } componentWillMount() { console.log('2.componentWillMount'); } componentDidMount() { console.log('3.componentDidMount',); } render() { const { count } = this.state; return (<div>{this.props.name}: {count} {count === 1 && <ChildOne/>} {count === 2 ? <ChildTwo /> : <ChildOther />} <button onClick={this.handleClick}>+</button> </div>) } } export default LifeCycle;
点击按钮,count由0变至3,查看控制台输出是否如下,
count
1.constructor 2.componentWillMount childOther componentDidMount 3.componentDidMount childOne componentDidMount childTwo componentDidMount childOther componentDidMount
对于props的更新,会先触发一个componentWillReceiveProps的方法,把新的props值传入,这个方法只需要要updateClassComponent里面判断执行一下即可.
componentWillReceiveProps
updateClassComponent
function updateClassComponent(oldVdom, newVdom) { const classInstance = newVdom.classInstance = oldVdom.classInstance; + if(classInstance.componentWillReceiveProps) classInstance.componentWillReceiveProps(newVdom.props); classInstance.updater.emitUpdate(newVdom.props); }
修改LifeCycle组件,在ChildOther里面新增一个componentWillReceiveProps方法,点击试试控制台的打印输出.
ChildOther
class ChildOther extends React.Component{ componentDidMount(){ console.log('childOther componentDidMount'); } + componentWillReceiveProps(nextProps){ + console.log('childOther componentWillReceiveProps', nextProps ); + } render(){ - return <p>childOther</p> + return <p>childOther: {this.props.count}</p> } } class LifeCycle extends React.Component { static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } handleClick = () => { this.setState({ count: this.state.count + 1, }); } componentWillMount() { console.log('2.componentWillMount'); } componentDidMount() { console.log('3.componentDidMount',); } render() { const { count } = this.state; return (<div>{this.props.name}: {count} {count === 1 && <ChildOne/>} - {count === 2 ? <ChildTwo /> : <ChildOther count={count} />} + {count === 2 ? <ChildTwo /> : <ChildOther count={count} />} <button onClick={this.handleClick}>+</button> </div>) } }
在接受到新的props或者state之后,会触发shouldComponentUpdate,它接受最新的props和state,返回true或者false来控制是否需要进行组件的更新.这也是在这里做优化的一个地方,通过对比props或者state来控制是否真的需要更新组件.
shouldComponentUpdate
true
false
修改src/react/Updater.js里面的updateComponent方法, 增加一个shouldUpdate方法来做判断.这里需要注意的就是,即使shouldComponentUpdate返回false阻止了组件的渲染,但它的props和state还是会更新为最新的值的.
src/react/Updater.js
updateComponent
shouldUpdate
updateComponent() { const {componentInstance, pendingState, nextProps} = this; if (pendingState.length || nextProps) { - componentInstance.state = this.getState(); - componentInstance.props = nextProps; - componentInstance.forceUpdate(); + shouldUpdate(componentInstance, nextProps, this.getState()) } this.batchTracking = false; } +function shouldUpdate(componentInstance, nextProps, nextState){ + let willUpdate = true; // 默认需要更新 + if(componentInstance.shouldComponentUpdate){ + willUpdate = componentInstance.shouldComponentUpdate(nextProps, nextState); + } + // 不管组件是否需要更新,实例上面的props和state值都需要更新为最新状态 + componentInstance.props = nextProps; + componentInstance.state = nextState; + if(willUpdate) componentInstance.forceUpdate(); +}
在LifeCycle组件里面加入shouldComponentUpdate方法
class ChildOther extends React.Component{ componentDidMount(){ console.log('childOther componentDidMount'); } componentWillReceiveProps(nextProps){ console.log('childOther componentWillReceiveProps', nextProps ); } + shouldComponentUpdate(nextProps, nextState) { + console.log('childOther shouldComponentUpdate'); + return nextProps.count % 2 === 0; + } render(){ return <p>childOther: {this.props.count}</p> } } class LifeCycle extends React.Component { static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } handleClick = () => { this.setState({ count: this.state.count + 1, }); } componentWillMount() { console.log('2.componentWillMount'); } componentDidMount() { console.log('3.componentDidMount',); } + shouldComponentUpdate(nextProps, nextState) { + console.log('4.shouldComponentUpdate'); + return nextState.count !== 1; + } render() { const { count } = this.state; return (<div>{this.props.name}: {count} {count === 1 && <ChildOne/>} {count === 2 ? <ChildTwo /> : <ChildOther count={count} />} <button onClick={this.handleClick}>+</button> </div>) } }
点击试试,
ChildTwo
如果上一步shouldComponentUpdate返回了true,则会触发componentWillUpdate方法,
componentWillUpdate
在之前的shouldUpdate里面,执行forceUpdate之前来插入该方法的执行
forceUpdate
function shouldUpdate(componentInstance, nextProps, nextState){ let willUpdate = true; // 默认需要更新 if(componentInstance.shouldComponentUpdate){ willUpdate = componentInstance.shouldComponentUpdate(nextProps, nextState); } // 不管组件是否需要更新,实例上面的props和state值都需要更新为最新状态 componentInstance.props = nextProps; componentInstance.state = nextState; - if(willUpdate) componentInstance.forceUpdate(); + if (willUpdate) { + if (componentInstance.componentWillUpdate) componentInstance.componentWillUpdate(); + componentInstance.forceUpdate(); + } }
在LifeCycle组件和ChildOther组件里面加入这个函数,测试一下仅在组件需要更新渲染里才会执行该方法.
class ChildOther extends React.Component{ componentDidMount(){ console.log('childOther componentDidMount'); } componentWillReceiveProps(nextProps){ console.log('childOther componentWillReceiveProps', nextProps ); } shouldComponentUpdate(nextProps, nextState) { console.log('childOther shouldComponentUpdate'); return nextProps.count % 2 === 0; } + componentWillUpdate(){ + console.log('childOther componentWillUpdate'); + } render(){ return <p>childOther: {this.props.count}</p> } } class LifeCycle extends React.Component { static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } handleClick = () => { this.setState({ count: this.state.count + 1, }); } componentWillMount() { console.log('2.componentWillMount'); } componentDidMount() { console.log('3.componentDidMount',); } shouldComponentUpdate(nextProps, nextState) { console.log('4.shouldComponentUpdate'); return nextState.count !== 1; } + componentWillUpdate(){ + console.log('5.componentWillUpdate'); + } render() { const { count } = this.state; return (<div>{this.props.name}: {count} {count === 1 && <ChildOne/>} {count === 2 ? <ChildTwo /> : <ChildOther count={count} />} <button onClick={this.handleClick}>+</button> </div>) } }
组件更新完成之后,会立即调用componentDidUpdate这个方法,它会接受上一次的props和state.
componentDidUpdate
和componentWillUpdate相对的,componentWillUpdate发生在forceUpdate之前,而componentDidUpdate发生在forceUpdate之后.因为它要接受上一次的props和state值,所以需要先缓存一下.这里有可能会调用forceUpdate这个方法,所以我们给forceUpdate增加一个参数triggerFromUpdate来识别是否是由Updater触发的更新.如果不是,则需要单独再调用一次componentDidUpdate方法.
triggerFromUpdate
Updater
function shouldUpdate(componentInstance, nextProps, nextState){ + const prevProps = componentInstance.props; + const prevState = componentInstance.state; let willUpdate = true; // 默认需要更新 if(componentInstance.shouldComponentUpdate){ willUpdate = componentInstance.shouldComponentUpdate(nextProps, nextState); } // 不管组件是否需要更新,实例上面的props和state值都需要更新为最新状态 componentInstance.props = nextProps; componentInstance.state = nextState; if (willUpdate) { if (componentInstance.componentWillUpdate) componentInstance.componentWillUpdate(); - componentInstance.forceUpdate(true); + componentInstance.forceUpdate(true, prevProps, prevState); } }
修改Component里面的forceUpdate方法
Component
- forceUpdate() { + forceUpdate(triggerFromUpdate = false, prevProps, prevState) { const oldVdom = this.oldVdom; const newVdom = wrapToVdom(this.render()); compareTwoVdoms(oldVdom, newVdom) this.oldVdom = newVdom; // 将更新后的虚拟DOM更新到原来的oldVdom上面 + if(this.componentDidUpdate){ + if(triggerFromUpdate){ + this.componentDidUpdate(prevState, prevState); + }else{ + this.componentDidUpdate(this.props, this.state); + } + } }
继续修改LifeCycle和ChildOther组件
class ChildOther extends React.Component{ componentDidMount(){ console.log('childOther componentDidMount'); } componentWillReceiveProps(nextProps){ console.log('childOther componentWillReceiveProps', nextProps ); } shouldComponentUpdate(nextProps, nextState) { console.log('childOther shouldComponentUpdate'); return nextProps.count % 2 === 0; } componentWillUpdate(){ console.log('childOther componentWillUpdate'); } + componentDidUpdate(preProps, preState){ + console.log('childOther componentDidUpdate', preProps); + } render(){ return <p>childOther: {this.props.count}</p> } } class LifeCycle extends React.Component { static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } handleClick = () => { this.setState({ count: this.state.count + 1, }); } componentWillMount() { console.log('2.componentWillMount'); } componentDidMount() { console.log('3.componentDidMount',); } shouldComponentUpdate(nextProps, nextState) { console.log('4.shouldComponentUpdate'); return nextState.count !== 1; } componentWillUpdate(){ console.log('5.componentWillUpdate'); } + componentDidUpdate(preProps, preState){ + console.log('6.componentDidUpdate', preState); + } render() { const { count } = this.state; return (<div>{this.props.name}: {count} {count === 1 && <ChildOne/>} {count === 2 ? <ChildTwo /> : <ChildOther count={count} />} <button onClick={this.handleClick}>+</button> </div>) } }
不断增加count值,测试一下打印输出是否符合预期?
卸载阶段只有一个方法componentWillUnmount,它发生在组件将要卸载之前.组件的卸载更多的要考虑对子组件了影响,因为卸载的组件下面可能也挂有很多的子组件.比如有这样一颗组件树
componentWillUnmount
假如每个组件上面都挂有componentWillUnmount方法,当A组件卸载时,将会按照什么顺序来执行每个组件上面的componentWillUnmount方法呢?
新建一个src/components/ComponentWillUnmount.js文件,用原生的React来测试一下上面这个场景.
src/components/ComponentWillUnmount.js
import React from 'react'; class A extends React.Component { componentWillUnmount() { console.log('A'); } render() { return ( <div> <B/> <C/> {this.props.children} </div> ) } } class B extends React.Component { componentWillUnmount() { console.log('B'); } render() { return ( <div> <D/> <E/> </div> ) } } class C extends React.Component { componentWillUnmount() { console.log('C'); } render() { return ( <div>C </div> ) } } class D extends React.Component { componentWillUnmount() { console.log('D'); } render() { return ( <div>D </div> ) } } class E extends React.Component { componentWillUnmount() { console.log('E'); } render() { return ( <div>E </div> ) } } class Wrapper extends React.Component { constructor(props) { super(props); this.state = { show: true, } } render() { return <div onClick={() => this.setState({show: !this.state.show})}>{this.state.show && <A />}</div> } } export default Wrapper;
可以看到,控制台依次输出的是A,B,D,E,F.也就是React会按照深度优先遍历的方式来从顶层依次执行每个组件上面的componentWillUnmount方法.
在src/react-dom/index.js里面添加的一个deepWillUnmount方法
src/react-dom/index.js
deepWillUnmount
/** * 采用深度优先遍历的方式,依次执行组件及子组件上面的componentWillUnmount方法 * @param vdom */ function deepWillUnmount(vdom){ if(isNotNeedRender(vdom)) return; const { classInstance } = vdom; if(classInstance && classInstance.componentWillUnmount){ classInstance.componentWillUnmount(); } if(classInstance && classInstance.oldVdom){ // 考虑类组件返回另一个类组件 deepWillUnmount(classInstance.oldVdom) } else if(vdom.props.children) { const children = Array.isArray(vdom.props.children) ? vdom.props.children : [vdom.props.children] children.forEach(item => deepWillUnmount(item)) } }
组件的挂载一个触发在compareTwoVdom时就发生了旧元素的卸载,那就需要在卸载之前调用deepWillUnmount方法.
compareTwoVdom
export function compareTwoVdoms(oldVdom, newVdom, parentDom, nextDom) { if (!oldVdom && !newVdom) return; if (oldVdom && newVdom && (oldVdom.type !== newVdom.type)) { // 如果节点类型变了,直接进行全量更新 const oldDom = findDom(oldVdom); const newDom = createDom(newVdom); + deepWillUnmount(oldVdom); oldDom.parentNode.replaceChild(newDom, oldDom); if(newVdom.componentDidMount) newVdom.componentDidMount(); } else if (oldVdom && !newVdom) { const oldDom = findDom(oldVdom); + deepWillUnmount(oldVdom); oldDom.parentNode.removeChild(oldDom); } else if (!oldVdom && newVdom) { const newDom = createDom(newVdom); if (nextDom) { // 如果后面有节点,则应该进行插入操作 parentDom.insertBefore(newDom, nextDom); } else { parentDom.appendChild(newDom); } if(newVdom.componentDidMount) newVdom.componentDidMount(); } else { updateElement(oldVdom, newVdom); } }
另一个触发就是在compareTwoVdom里面继续执行到updateElement里面之后,在更新子组件时,发生了子组件的卸载.为了执行实例上面的方法,我们需要把虚拟Dom缓存到之前的diffQueue里面去.
updateElement
diffQueue
在diff方法里面把vdmo挂载上
diff
vdmo
for(let key in oldChildrenMap){ const oldElement = oldChildrenMap[key]; const notWithNew = !newChildrenMap.hasOwnProperty(key); // 新节点里面不存在该老元素 const notSame = newChildrenMap[key] !== oldElement; // 新节点该元素的类型变了 if(!isNotNeedRender(oldElement) && (notWithNew || notSame)){ diffQueue.push({ parentDom, type: REMOVE, + vdom: oldElement, fromIndex: oldElement._mountIndex }) } }
在接下来应用补丁包的方法path里面,当删除旧元素时,执行deepWillUnmount方法
path
function path(diffQueue){ // 1.删除要删除的 let deleteMap = {}; let deleteChildren = []; diffQueue.forEach((item) => { - const {type, fromIndex, toIndex} = item; + const {type, fromIndex, toIndex, vdom} = item; if(type === MOVE || type === REMOVE){ const oldChild = item.parentDom.childNodes[fromIndex]; deleteMap[fromIndex] = oldChild; - deleteChildren.push(oldChild); + deleteChildren.push({dom: oldChild, type, vdom}); } }); deleteChildren.forEach(item => { - item.parentNode.removeChild(item); + const {dom, type, vdom} = item; + if(type === REMOVE){ + deepWillUnmount(vdom); + } + dom.parentNode.removeChild(dom); }); diffQueue.forEach((item) => { const { type, fromIndex, toIndex, parentDom, dom} = item; if(type === INSERT){ insertChildAt(parentDom, dom, toIndex) if(dom.componentDidMount) dom.componentDidMount(); } if(type === MOVE){ insertChildAt(parentDom, deleteMap[fromIndex], toIndex) } }) }
现在用我们写的版本来测试一下之前的ComponentWillUnmount看看表现是否一致.
ComponentWillUnmount
同时也修改一下LifeCycle组件,在ChildOther深入嵌套几层,测试一下组件的卸载过程.
class ChildOther extends React.Component{ componentDidMount(){ console.log('childOther componentDidMount'); } componentWillReceiveProps(nextProps){ console.log('childOther componentWillReceiveProps', nextProps ); } shouldComponentUpdate(nextProps, nextState) { console.log('childOther shouldComponentUpdate'); return nextProps.count % 2 === 0; } componentWillUpdate(){ console.log('childOther componentWillUpdate'); } componentDidUpdate(preProps, preState){ console.log('childOther componentDidUpdate', preProps); } + componentWillUnmount(){ + console.log('childOther componentWillUnmount'); + } render(){ - return <p>childOther: {this.props.count}</p> + return this.props.count % 2 === 0 ? <Even count={this.props.count} /> : <Singular count={this.props.count} /> } } +class Singular extends React.Component{ + componentWillUnmount(){ + console.log('Singular componentWillUnmount'); + } + render(){ + return <p>Singular: {this.props.count}</p> + } +} +class Even extends React.Component{ + componentWillUnmount(){ + console.log('even componentWillUnmount'); + } + render(){ + return <p>Even: {this.props.count} <Even1 /></p> + } +} +class Even1 extends React.Component{ + componentWillUnmount(){ + console.log('even1 compwillUnmount'); + } + render(){ + return (<div><Even2 /></div>) + } +} + +class Even2 extends React.Component{ + componentWillUnmount(){ + console.log('even2 compwillUnmount'); + } + render(){ + return 'even2' + } +}
将count一直递增到6,不出意外,控制台应该输出如下,并且应该和原生的React表现是一致的.
1.constructor 2.componentWillMount childOther componentDidMount 3.componentDidMount 4.shouldComponentUpdate 4.shouldComponentUpdate 5.componentWillUpdate childOther componentWillUnmount even componentWillUnmount even1 componentWillUnmount even2 componentWillUnmount childTwo componentDidMount 6.componentDidUpdate {count: 1} 4.shouldComponentUpdate 5.componentWillUpdate childOther componentDidMount 6.componentDidUpdate {count: 2} 4.shouldComponentUpdate 5.componentWillUpdate childOther componentWillReceiveProps {count: 4} childOther shouldComponentUpdate childOther componentWillUpdate Singular componentWillUnmount childOther componentDidUpdate {count: 3} 6.componentDidUpdate {count: 3} 4.shouldComponentUpdate 5.componentWillUpdate childOther componentWillReceiveProps {count: 5} childOther shouldComponentUpdate 6.componentDidUpdate {count: 4} 4.shouldComponentUpdate 5.componentWillUpdate childOther componentWillReceiveProps {count: 6} childOther shouldComponentUpdate childOther componentWillUpdate childOther componentDidUpdate {count: 5} 6.componentDidUpdate {count: 5}
老版本的生命周期,基本上已经覆盖了大多的应用场景,在React新版本的生命周期里,也仅增加了两个API.而且极少情况下才会用到,先来看一张图:
正如其名一样,它可以通过props来映射state,它充当了componentWillReceiveProps的角色,但它的执行周期却不一样,并且它是一个静态方法,不能访问实例,只能通过输入来输出,返回值会被映射为state,或者返回一个null来代表不变更state,如上图,它会在创建时,更新时都会进行触发.
null
在创建时,修改mountClassComponent方法
function mountClassComponent(vdom) { const {type, props, ref} = vdom; const defaultProps = type.defaultProps || {}; - const classInstance = new type({...defaultProps, ...props}); + const combinedProps = {...defaultProps, ...props}; + const classInstance = new type(combinedProps); if(classInstance.componentWillMount) classInstance.componentWillMount(); + if(type.getDerivedStateFromProps){ + classInstance.state = type.getDerivedStateFromProps(combinedProps, classInstance.state) || classInstance.state; + } const classInstanceVdom = wrapToVdom(classInstance.render()); classInstance.oldVdom = classInstanceVdom; // 将虚拟dom挂载到当前组件实例上面.接下来的真实dom会挂到classInstanceVdom和classInstance.oldVdom上面; vdom.classInstance = classInstance; if (ref) { ref.current = classInstance; } const dom = createDom(classInstanceVdom); if(classInstance.componentDidMount) dom.componentDidMount = classInstance.componentDidMount.bind(classInstance); return dom; }
更新阶段,它可能是由props,state触发的更新
function shouldUpdate(componentInstance, nextProps, nextState){ const prevProps = componentInstance.props; const prevState = componentInstance.state; let willUpdate = true; // 默认需要更新 if(componentInstance.shouldComponentUpdate){ willUpdate = componentInstance.shouldComponentUpdate(nextProps, nextState); } // 不管组件是否需要更新,实例上面的props和state值都需要更新为最新状态 componentInstance.props = nextProps; - componentInstance.state = nextState; + if(componentInstance.constructor.getDerivedStateFromProps){ + componentInstance.state = componentInstance.constructor.getDerivedStateFromProps(nextProps, nextState) || nextState; + } else { + componentInstance.state = nextState; + } if (willUpdate) { if (componentInstance.componentWillUpdate) componentInstance.componentWillUpdate(); componentInstance.forceUpdate(true); if(componentInstance.componentDidUpdate) componentInstance.componentDidUpdate(prevProps, prevState); } }
同样,它也有可能会直接被调用forceUpdate来触发
修改Component父类里面的forceUpdate方法
forceUpdate(triggerFromUpdate = false, prevProps, prevState) { const oldVdom = this.oldVdom; + if(!triggerFromUpdate && this.constructor.getDerivedStateFromProps){ + this.state = this.constructor.getDerivedStateFromProps(this.props, this.state) || this.state; + } const newVdom = wrapToVdom(this.render()); compareTwoVdoms(oldVdom, newVdom) this.oldVdom = newVdom; // 将更新后的虚拟DOM更新到原来的oldVdom上面 if(this.componentDidUpdate){ if(triggerFromUpdate){ this.componentDidUpdate(prevState, prevState, extraArgs); }else{ this.componentDidUpdate(this.props, this.state, extraArgs); } } }
新建src/components/GetDerivedStateFromProps.js文件
src/components/GetDerivedStateFromProps.js
import React from '../react'; class GetDerivedStateFromProps extends React.Component{ static defaultProps = { name: 'demo', } constructor(props) { super(props); this.state = {count: 0}; console.log('1.constructor'); } handleClick = () => { this.setState({ count: this.state.count + 1, }); } componentDidMount() { console.log('5.componentDidMount',); } shouldComponentUpdate(nextProps, nextState) { console.log('3.shouldComponentUpdate'); return nextState.count !== 1; } componentDidUpdate(preProps, preState){ console.log('4.componentDidUpdate', preState); } static getDerivedStateFromProps(props, state){ console.log('2.getDerivedStateFromProps', props, state); return { count: 4 } } handleForceUpdate = () =>{ this.forceUpdate() } render() { const { count } = this.state; return (<div>{this.props.name}: {count} <button onClick={this.handleClick}>+</button> <button onClick={this.handleForceUpdate}>forceUpdate</button> </div>) } } export default GetDerivedStateFromProps;
导入这个组件,渲染它,依次点击+号和forceUpdate按钮,它将输出如下:
+
1.constructor 2.getDerivedStateFromProps {name: "demo"} {count: 0} 5.componentDidMount 2.getDerivedStateFromProps {name: "demo"} {count: 5} 3.shouldComponentUpdate 4.componentDidUpdate {count: 4} 2.getDerivedStateFromProps {name: "demo"} {count: 4} 4.componentDidUpdate {count: 4}
这个API给了我们在render之前获取更新前真实Dom的一些相关信息,然后将返回值传递给componentDidUpdate以来进一步应用相关的变化.它相比componentWillUpdate更加安全.它能保证读取的Dom是与componentDidUpdate中一致.常见的应用就是处理滚动条的问题,当元素内在最前面插入了新元素,通过它就可以来保证滚动条正确滚动在之前要显示内容的位置 .
它的触发时机为更新阶段的render之后,正式渲染之前.
所以修改Component父类里面的forceUpdate方法
forceUpdate(triggerFromUpdate = false, prevProps, prevState) { const oldVdom = this.oldVdom; if(!triggerFromUpdate && this.constructor.getDerivedStateFromProps){ this.state = this.constructor.getDerivedStateFromProps(this.props, this.state) || this.state; } const newVdom = wrapToVdom(this.render()); + let extraArgs; + if(this.getSnapshotBeforeUpdate){ + extraArgs = this.getSnapshotBeforeUpdate(); + } compareTwoVdoms(oldVdom, newVdom) this.oldVdom = newVdom; // 将更新后的虚拟DOM更新到原来的oldVdom上面 if(this.componentDidUpdate){ if(triggerFromUpdate){ - this.componentDidUpdate(prevState, prevState); + this.componentDidUpdate(prevState, prevState, extraArgs); }else{ - this.componentDidUpdate(this.props, this.state); + this.componentDidUpdate(this.props, this.state, extraArgs); } } }
新建src/components/GetSnapshotBeforeUpdate.js文件,实现一个组件
src/components/GetSnapshotBeforeUpdate.js
import React from '../react'; let id = 0; class GetSnapshotBeforeUpdate extends React.Component { constructor(props) { super(props); this.state = { message: [] } } handleClick = () => { this.setState({ message: [id++, ...this.state.message], }) } render() { return ( <div> <div style={{height: '100px', overflow: 'scroll', border: '1px solid red'}}> {this.state.message.map(item => <p key={item}>{item}</p>)} </div> <button onClick={this.handleClick}>+</button> </div> ) } } export default GetSnapshotBeforeUpdate;
这个组件就是每点一次+号就会在最前面新增插入一个p元素.如果我们多点几次,出现了滚动条之后,滚动一下滚动条,然后再点+号,因为最顶部插入了元素,就会在原来的可视区域内,把内容往下挤.(这个效果现在可能需要在Safari里面才能看到).如果为了保证可视区域里面的内容保持不变,我们就需要在更新完成之后,去调整滚动条的位置,将原来的内容正常显示出来.这在之前,我们这处理这样的情况,就相对是比较麻烦的.现在借助getSnapshotBeforeUpdate这个API我们就可以在render之前先获取到快照,更新完成之后,对真实Dom元素进行一定的操作即可了.
p
getSnapshotBeforeUpdate
class GetSnapshotBeforeUpdate extends React.Component { constructor(props) { super(props); this.state = { message: [] } + this.wrapper = React.createRef(); } handleClick = () => { this.setState({ message: [id++, ...this.state.message], }) } + getSnapshotBeforeUpdate(){ + return { + prevScrollTop: this.wrapper.current.scrollTop, //更新前的滚动条位置 + prevScrollHeight: this.wrapper.current.scrollHeight, //更新前容器的高度 + } + } + componentDidUpdate(prevProps, prevState, {prevScrollTop, prevScrollHeight}){ + this.wrapper.current.scrollTop = prevScrollTop + (this.wrapper.current.scrollHeight - prevScrollHeight); + } render() { return ( <div> - <div style={{height: '100px', overflow: 'scroll', border: '1px solid red'}}> + <div style={{height: '100px', overflow: 'scroll', border: '1px solid red'}} ref={this.wrapper}> {this.state.message.map(item => <p key={item}>{item}</p>)} </div> <button onClick={this.handleClick}>+</button> </div> ) } }
这样,当我们滚动滚动条之后,在不管怎么点击+号,可视区域内的内容就都不会变化了.
前面已经实现了组件的更新,在组件的更新过程,类组件还提供了一系列的钩子函数,也就是生命周期.从组件的挂载,更新,卸载的不同生命周期里面可以执行一系列我们想执行的方法.
老版本生命周期
先来看看一张生命周期图
初始化阶段
defaultProps
这个阶段设置props和state,因为现在是采用的是es6的写法,所以这个阶段都是在
constructor
里面进行完成了.这里我们只是需要处理一下组件的defaultProps
即可,因为组件有可能会设置一些默认的props
属性.在
src/react-dom
里面的mountClassComponent
方法里面在
src/components/LifeCycle.js
里面创建一个组件在
src/index.js
导入这个组件,它应该就能正常渲染出props
和state
值了.挂载阶段
componentWillMount
在挂载阶段,
render
之前会执行componentWillMount
方法,这个基本比较简单,在mountClassComponent
方法里面,调用render
之前判断实例上面是否添加了componentWillMount
方法,有的话,执行一下即可.componentDidMount
那
componentDidMount
不也一样吗,在render
这个方法之后调用一下就可以了吧,但要注意的是,render
只是生成了虚拟Dom,并还没把虚拟Dom挂载到真实的节点上面,而这个方法是需要在真实Dom渲染完毕之后再执行.那这里我们可以先把这个函数挂载到dom
上面,当节点插入到真实Dom中之后,再执行这个方法.继续修改
mountClassComponent
这个方法,将componentDidMount
挂载到dom上面.这个方法只会在组件第一次挂载完成后执行,那哪些时机会触发组件的挂载完成呢?
render
基于以上几种场景,我们分别在对应的时机节点进行调用处理即可.
再次调整之前的
LifeCycle
组件,模拟几种触发组件完成挂载的场景.点击按钮,
count
由0变至3,查看控制台输出是否如下,更新阶段
componentWillReceiveProps
对于
props
的更新,会先触发一个componentWillReceiveProps
的方法,把新的props
值传入,这个方法只需要要updateClassComponent
里面判断执行一下即可.修改
LifeCycle
组件,在ChildOther
里面新增一个componentWillReceiveProps
方法,点击试试控制台的打印输出.shouldComponentUpdate
在接受到新的
props
或者state
之后,会触发shouldComponentUpdate
,它接受最新的props
和state
,返回true
或者false
来控制是否需要进行组件的更新.这也是在这里做优化的一个地方,通过对比props
或者state
来控制是否真的需要更新组件.修改
src/react/Updater.js
里面的updateComponent
方法, 增加一个shouldUpdate
方法来做判断.这里需要注意的就是,即使shouldComponentUpdate
返回false
阻止了组件的渲染,但它的props
和state
还是会更新为最新的值的.在
LifeCycle
组件里面加入shouldComponentUpdate
方法点击试试,
count
等于1,LifeCycle
组件不更新,子组件没有任何变化count
等于2,LifeCycle
组件更新,ChildTwo
组件挂载,ChildOther
卸载count
等于3,ChildTwo
组件卸载,ChildOther
挂载,props
里面的count
值也为3count
等于4,ChildOther
触发更新,render
出来的结果为4.count
等于5,ChildOther
阻止更新,render
出来的4保持不变.count
等于6,ChildOther
触发更新,render
出来的结果为6.componentWillUpdate
如果上一步
shouldComponentUpdate
返回了true
,则会触发componentWillUpdate
方法,在之前的
shouldUpdate
里面,执行forceUpdate
之前来插入该方法的执行在
LifeCycle
组件和ChildOther
组件里面加入这个函数,测试一下仅在组件需要更新渲染里才会执行该方法.componentDidUpdate
组件更新完成之后,会立即调用
componentDidUpdate
这个方法,它会接受上一次的props
和state
.和
componentWillUpdate
相对的,componentWillUpdate
发生在forceUpdate
之前,而componentDidUpdate
发生在forceUpdate
之后.因为它要接受上一次的props
和state
值,所以需要先缓存一下.这里有可能会调用forceUpdate
这个方法,所以我们给forceUpdate
增加一个参数triggerFromUpdate
来识别是否是由Updater
触发的更新.如果不是,则需要单独再调用一次componentDidUpdate
方法.修改
Component
里面的forceUpdate
方法继续修改
LifeCycle
和ChildOther
组件不断增加
count
值,测试一下打印输出是否符合预期?卸载阶段
componentWillUnmount
卸载阶段只有一个方法
componentWillUnmount
,它发生在组件将要卸载之前.组件的卸载更多的要考虑对子组件了影响,因为卸载的组件下面可能也挂有很多的子组件.比如有这样一颗组件树假如每个组件上面都挂有
componentWillUnmount
方法,当A组件卸载时,将会按照什么顺序来执行每个组件上面的componentWillUnmount
方法呢?新建一个
src/components/ComponentWillUnmount.js
文件,用原生的React来测试一下上面这个场景.可以看到,控制台依次输出的是A,B,D,E,F.也就是React会按照深度优先遍历的方式来从顶层依次执行每个组件上面的
componentWillUnmount
方法.在
src/react-dom/index.js
里面添加的一个deepWillUnmount
方法组件的挂载一个触发在
compareTwoVdom
时就发生了旧元素的卸载,那就需要在卸载之前调用deepWillUnmount
方法.另一个触发就是在
compareTwoVdom
里面继续执行到updateElement
里面之后,在更新子组件时,发生了子组件的卸载.为了执行实例上面的方法,我们需要把虚拟Dom缓存到之前的diffQueue
里面去.在
diff
方法里面把vdmo
挂载上在接下来应用补丁包的方法
path
里面,当删除旧元素时,执行deepWillUnmount
方法现在用我们写的版本来测试一下之前的
ComponentWillUnmount
看看表现是否一致.同时也修改一下
LifeCycle
组件,在ChildOther
深入嵌套几层,测试一下组件的卸载过程.将
count
一直递增到6,不出意外,控制台应该输出如下,并且应该和原生的React表现是一致的.新版本生命周期
老版本的生命周期,基本上已经覆盖了大多的应用场景,在React新版本的生命周期里,也仅增加了两个API.而且极少情况下才会用到,先来看一张图:
getDerivedStateFromProps
正如其名一样,它可以通过
props
来映射state
,它充当了componentWillReceiveProps
的角色,但它的执行周期却不一样,并且它是一个静态方法,不能访问实例,只能通过输入来输出,返回值会被映射为state
,或者返回一个null
来代表不变更state
,如上图,它会在创建时,更新时都会进行触发.在创建时,修改
mountClassComponent
方法更新阶段,它可能是由
props
,state
触发的更新同样,它也有可能会直接被调用
forceUpdate
来触发修改
Component
父类里面的forceUpdate
方法新建
src/components/GetDerivedStateFromProps.js
文件导入这个组件,渲染它,依次点击
+
号和forceUpdate
按钮,它将输出如下:getSnapshotBeforeUpdate
这个API给了我们在
render
之前获取更新前真实Dom的一些相关信息,然后将返回值传递给componentDidUpdate
以来进一步应用相关的变化.它相比componentWillUpdate
更加安全.它能保证读取的Dom是与componentDidUpdate
中一致.常见的应用就是处理滚动条的问题,当元素内在最前面插入了新元素,通过它就可以来保证滚动条正确滚动在之前要显示内容的位置 .它的触发时机为更新阶段的
render
之后,正式渲染之前.所以修改
Component
父类里面的forceUpdate
方法新建
src/components/GetSnapshotBeforeUpdate.js
文件,实现一个组件这个组件就是每点一次
+
号就会在最前面新增插入一个p
元素.如果我们多点几次,出现了滚动条之后,滚动一下滚动条,然后再点+
号,因为最顶部插入了元素,就会在原来的可视区域内,把内容往下挤.(这个效果现在可能需要在Safari里面才能看到).如果为了保证可视区域里面的内容保持不变,我们就需要在更新完成之后,去调整滚动条的位置,将原来的内容正常显示出来.这在之前,我们这处理这样的情况,就相对是比较麻烦的.现在借助getSnapshotBeforeUpdate
这个API我们就可以在render
之前先获取到快照,更新完成之后,对真实Dom元素进行一定的操作即可了.这样,当我们滚动滚动条之后,在不管怎么点击
+
号,可视区域内的内容就都不会变化了.