buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

11.PureComponent and React.memo #11

Open buxuku opened 3 years ago

buxuku commented 3 years ago

在类组件中,shouldComponentUpdate默认返回的是true,React为我们提供了一个PureComponent的父类,它会自动帮我们对propsstate进行浅对比,如果值一样,则返回false以来优化render.

PureComponent的实现,其实就是实现shouldComponentUpdate这个方法.

src/react/index.js里添加一个PureComponent的父类

-import {wrapToVdom, flatten} from '../utils';
+import {wrapToVdom, flatten, shallowEqual} from '../utils';

+class PureComponnent extends Component{
+    shouldComponentUpdate(nextProps, nextState){
+        return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
+    }
+}

const React = {
    createElement,
    Component,
+    PureComponnent,
    createRef,
    createContext,
    forwardRef,
}

src/utils/index.js里面实现shallowEqual这个方法

/**
 * 浅比较对比两个对象
 * @param obj1
 * @param obj2
 * @returns {boolean}
 */
export function shallowEqual(obj1, obj2){
    debugger
    if(obj1 === obj2) return true;
    if(typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null){
        return false;
    }
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if(obj1Keys.length !== obj2Keys.length){
        return false;
    }
    for(let key of obj1Keys){
        if(!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]){
            return false;
        }
    }
    return true;
}

新建src/components/PureComponent.js, 这个组件点+1会更新state,点nothing不会更新state,这个时候可以看到只有state值产生变化了之后才会进行render操作.

import React from '../react';

class PureComponnent extends React.PureComponnent {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
        }
    }

    handleAdd = (num = 1) => {
        this.setState({count: this.state.count + num})
    }

    render() {
        console.log('PureComponnent render');
        return (
            <div>
                <p>count: {this.state.count}</p>
                <button onClick={() => this.handleAdd(1)}>+1</button>
                <button onClick={() => this.handleAdd(0)}>nothing</button>
            </div>
        )
    }
}

export default PureComponnent;

而对于函数组件, React提供了一个React.memo的方法,被React.memo包裹了的函数组件,会自动对props属性进行浅对比,有更新,才重新渲染该组件.

createContext一样,React.memo返回的也是一个虚拟Dom的对象.

image-20210621110303559

其中有一个$$typeof的标识,compare表示传入的自定义的对比方法,type指向的是函数组件本身.

src/constant/index.js新增一个memo的类型

+export const REACT_MEMO = Symbol('REACT_MEMO');

src/react/index.js实现memo这个方法

-import {REACT_FORWARD_COMPONENT, REACT_CONTEXT, REACT_PROVIDER} from "../constants";
+import {REACT_FORWARD_COMPONENT, REACT_CONTEXT, REACT_PROVIDER, REACT_MEMO} from "../constants";

+function memo(type, compare = shallowEqual){
+    return {
+        $$typeof: REACT_MEMO,
+        compare,
+        type,
+    }
+}

const React = {
    createElement,
    Component,
    PureComponnent,
+    memo,
    createRef,
    createContext,
    forwardRef,
}

createContext一样,在src/react-dom/index.js增加mountMemoComponentupdateMemoComponent方法.需要注意的是,函数组件每次都是独立执行的,所以我们需要在虚拟Dom上面挂载一个prevProps的属性来记录props以方便更新的时候对props对比对比操作.

-import {REACT_FORWARD_COMPONENT, REACT_TEXT, REACT_CONTEXT, REACT_PROVIDER, MOVE, REMOVE, INSERT} from '../constants';
+import {REACT_FORWARD_COMPONENT, REACT_TEXT, REACT_CONTEXT, REACT_PROVIDER, MOVE, REMOVE, INSERT, REACT_MEMO} from '../constants';

function createDom(vdom) {
    if (isNotNeedRender(vdom)) return
    let dom;
    const {type, props, ref} = vdom;
+    if (type && type.$$typeof === REACT_MEMO) {
+        return mountMemoComponent(vdom);
+    }
    if (type && type.$$typeof === REACT_FORWARD_COMPONENT) {
        return mountForwardComponent(vdom);
    }
    if (type === REACT_TEXT) {
        dom = document.createTextNode(props.content);
    }
    if (type && type.$$typeof === REACT_PROVIDER){
        return mountProviderComponent(vdom);
    }
    if (type && type.$$typeof === REACT_CONTEXT){
        return mountContextComponent(vdom);
    }
    if (typeof type === 'string') {
        dom = document.createElement(type);
        renderAttributes(dom, props);
    }
    if (typeof type === 'function') {
        if (type.isReactComponent) { // 是一个类组件
            return mountClassComponent(vdom);
        }
        let renderVdom = wrapToVdom(type(props));
        vdom.oldVdom = renderVdom;
        // 让type执行,返回虚拟DOM,继续处理返回的虚拟DOM
        return createDom(renderVdom);
    }
    if (props) {
        const {children} = props;
        if (Array.isArray(children)) {
            reconcileChildren(children, dom);
        } else {
            reconcileChildren([children], dom);
        }
    }
    vdom.dom = dom;
    if (ref) {
        ref.current = dom;
    }
    return dom;
}

+function mountMemoComponent(vdom){
+    const {type, props} = vdom;
+    const renderVdom = type.type(props);
+    vdom.oldVdom = renderVdom;
+    vdom.prevProps = props; // 缓存当前props用于更新时做对比.
+    return createDom(renderVdom);
+}

function updateElement(oldVdom, newVdom) {
    if (oldVdom.type === REACT_TEXT && newVdom.type === REACT_TEXT ) {
        const dom = newVdom.dom = findDom(oldVdom);
        if(oldVdom.props.content !== newVdom.props.content){ // 当文本内容有变化才进行更新
            dom.textContent = newVdom.props.content;
        }
    }
+    if(oldVdom.type && oldVdom.type.$$typeof === REACT_MEMO){
+        updateMemoComponent(oldVdom, newVdom);
+    }
    if (oldVdom.type && oldVdom.type.$$typeof === REACT_PROVIDER){
        updateProviderComponent(oldVdom, newVdom);
    }
    if (oldVdom.type && oldVdom.type.$$typeof === REACT_CONTEXT){
        updateConsumerComponent(oldVdom, newVdom);
    }
    if (typeof oldVdom.type === 'string') { // 原生的HTML元素
        const currentDom = newVdom.dom = findDom(oldVdom); // 把老的Dom节点直接复制过来
        renderAttributes(currentDom, newVdom.props, oldVdom.props); // 更新节点属性
        updateChildren(currentDom, oldVdom.props.children, newVdom.props.children);
    } else if(typeof oldVdom.type === 'function'){
        if (oldVdom.type.isReactComponent) {
            updateClassComponent(oldVdom, newVdom)
        }else{
            updateFunctionComponent(oldVdom, newVdom);
        }
    }
}

+function updateMemoComponent(oldVdom, newVdom){
+    const {type, props} = newVdom;
+    if(!type.compare(oldVdom.prevProps, props)){
+        const renderVdom = type.type(props);
+        const parentDom = findDom(oldVdom);
+        compareTwoVdoms(oldVdom.oldVdom, renderVdom, parentDom);
+        newVdom.oldVdom = renderVdom;
+    } else {
+        newVdom.oldVdom = oldVdom.oldVdom
+    }
+    newVdom.prevProps = props;
}

修改前面那个PureComponennt组件,增加一个函数组件

import React from '../react';

function Child(props) {
    console.log('Child render');
    return <div>
        Child: {props.name}
    </div>
}

const ChildMemo = React.memo(Child);

class PureComponnent extends React.PureComponnent {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
            name: 'hello',
        }
    }

    handleAdd = (num = 1) => {
        this.setState({count: this.state.count + num})
    }
    changeName = () => {
        this.setState({
            name: this.state.name + '.'
        })
    }

    render() {
        console.log('PureComponnent render');
        return (
            <div>
                <p>count: {this.state.count}</p>
                <ChildMemo name={this.state.name}/>
                <button onClick={() => this.handleAdd(1)}>+1</button>
                <button onClick={() => this.handleAdd(0)}>nothing</button>
                <button onClick={this.changeName}>nothing</button>
            </div>
        )
    }
}

export default PureComponnent;

现在可以发现,当只修改count值的时候,将不会触发Child组件的render.