buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

9.组件的生命周期 #9

Open buxuku opened 3 years ago

buxuku commented 3 years ago

前面已经实现了组件的更新,在组件的更新过程,类组件还提供了一系列的钩子函数,也就是生命周期.从组件的挂载,更新,卸载的不同生命周期里面可以执行一系列我们想执行的方法.

老版本生命周期

先来看看一张生命周期图

img

初始化阶段

defaultProps

这个阶段设置props和state,因为现在是采用的是es6的写法,所以这个阶段都是在constructor里面进行完成了.这里我们只是需要处理一下组件的defaultProps即可,因为组件有可能会设置一些默认的props属性.

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里面创建一个组件

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导入这个组件,它应该就能正常渲染出propsstate值了.

import React from './react';
import ReactDOM from './react-dom';
import LifeCycle from './components/LifeCycle';

ReactDOM.render(
    <LifeCycle />,
    document.getElementById('root')
);

挂载阶段

componentWillMount

在挂载阶段,render之前会执行componentWillMount方法,这个基本比较简单,在mountClassComponent方法里面,调用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

componentDidMount不也一样吗,在render这个方法之后调用一下就可以了吧,但要注意的是,render只是生成了虚拟Dom,并还没把虚拟Dom挂载到真实的节点上面,而这个方法是需要在真实Dom渲染完毕之后再执行.那这里我们可以先把这个函数挂载到dom上面,当节点插入到真实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组件,模拟几种触发组件完成挂载的场景.

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,查看控制台输出是否如下,

1.constructor
2.componentWillMount
childOther componentDidMount
3.componentDidMount
childOne componentDidMount
childTwo componentDidMount
childOther componentDidMount

更新阶段

componentWillReceiveProps

对于props的更新,会先触发一个componentWillReceiveProps的方法,把新的props值传入,这个方法只需要要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方法,点击试试控制台的打印输出.

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>)
    }
}
shouldComponentUpdate

在接受到新的props或者state之后,会触发shouldComponentUpdate,它接受最新的propsstate,返回true或者false来控制是否需要进行组件的更新.这也是在这里做优化的一个地方,通过对比props或者state来控制是否真的需要更新组件.

修改src/react/Updater.js里面的updateComponent方法, 增加一个shouldUpdate方法来做判断.这里需要注意的就是,即使shouldComponentUpdate返回false阻止了组件的渲染,但它的propsstate还是会更新为最新的值的.

    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>)
    }
}

点击试试,

componentWillUpdate

如果上一步shouldComponentUpdate返回了true,则会触发componentWillUpdate方法,

在之前的shouldUpdate里面,执行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

组件更新完成之后,会立即调用componentDidUpdate这个方法,它会接受上一次的propsstate.

componentWillUpdate相对的,componentWillUpdate发生在forceUpdate之前,而componentDidUpdate发生在forceUpdate之后.因为它要接受上一次的propsstate值,所以需要先缓存一下.这里有可能会调用forceUpdate这个方法,所以我们给forceUpdate增加一个参数triggerFromUpdate来识别是否是由Updater触发的更新.如果不是,则需要单独再调用一次componentDidUpdate方法.

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方法

-    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);
+            }
+        }
    }

继续修改LifeCycleChildOther组件

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,它发生在组件将要卸载之前.组件的卸载更多的要考虑对子组件了影响,因为卸载的组件下面可能也挂有很多的子组件.比如有这样一颗组件树

image-20210609170732020

假如每个组件上面都挂有componentWillUnmount方法,当A组件卸载时,将会按照什么顺序来执行每个组件上面的componentWillUnmount方法呢?

新建一个src/components/ComponentWillUnmount.js文件,用原生的React来测试一下上面这个场景.

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方法

/**
 * 采用深度优先遍历的方式,依次执行组件及子组件上面的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方法.

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里面去.

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方法

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看看表现是否一致.

同时也修改一下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.而且极少情况下才会用到,先来看一张图:

img

getDerivedStateFromProps

正如其名一样,它可以通过props来映射state,它充当了componentWillReceiveProps的角色,但它的执行周期却不一样,并且它是一个静态方法,不能访问实例,只能通过输入来输出,返回值会被映射为state,或者返回一个null来代表不变更state,如上图,它会在创建时,更新时都会进行触发.

在创建时,修改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文件

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}
getSnapshotBeforeUpdate

这个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文件,实现一个组件

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元素进行一定的操作即可了.

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>
        )
    }
}

这样,当我们滚动滚动条之后,在不管怎么点击+号,可视区域内的内容就都不会变化了.