buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

10.createContext #10

Open buxuku opened 3 years ago

buxuku commented 3 years ago

在React里,对于数据的管理,除了propsstate,还有一个content的API,它提供了跨组件的通信能力.这在一些小的项目里面,它也可以充当一个redux的角色.

React提供了一个createContext的方法,它接受一个默认值的入参,返回一个ProviderConsumer的组件,Provider组件接受一个valueprops属性,同时在Consumer组件的children为一个函数,它的入参便是该value值.

基于此,在src/react/index.js里面实现这个createContext方法.

+function createContext(value){
+    let context = {
+        _value: value,
+        Provider,
+        Consumer,
+    };
+    function Provider({value, children}){ // Provider接收一个value的props
+        context._value = value;
+        return children;
+    }
+    function Consumer({children}){ // Consumer的children是一个函数
+        return children(context._value)
+    }
+    return context;
+}

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

在类组件中,我们一般是通过声明一个静态属性contextType来使用,当声明了该属性之后,该类的实例上面就会挂载一个context的属性,其值便是Provider的上面的value值.

修改src/react-dom/index.js里面的mountClassComponent方法,如果类组件上面存在该静态方法,便将value值挂载到context上面.

function mountClassComponent(vdom) {
    const {type, props, ref} = vdom;
    const defaultProps = type.defaultProps || {};
    const combinedProps = {...defaultProps, ...props};
    const classInstance = new type(combinedProps);
+    if(type.contextType){
+        classInstance.context = type.contextType._value;
+    }
    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;
}

同时修改src/react/Component.js里面的forceUpdate方法,因为在更新阶段,也要考虑Provider上面的value有更新了,所以同时也需要更新组件实例上面的context值.

    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;
        }
+       if(this.constructor.contextType){
+            this.context = this.constructor.contextType._value;
+        }
        const newVdom = 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, extraArgs);
            }else{
                this.componentDidUpdate(this.props, this.state, extraArgs);
            }
        }
    }

新建src/components/CreateContext.js文件,写一个通过context传递数据和方法的组件,包括类组件和函数组件里面的使用.

import React from '../react';

const ThemeContext = React.createContext();

class Parent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            color: 'gray',
        }
    }

    changeColor = (color) => {
        this.setState({color})
    }

    render() {
        const value = {color: this.state.color, changeColor: this.changeColor}
        return <ThemeContext.Provider value={value}>
            <Child/>
        </ThemeContext.Provider>
    }
}

class Child extends React.Component {
    static contextType = ThemeContext;

    render() {
        return <div>{this.context.color}
            <ChangeButton/>
        </div>
    }
}

function ChangeButton() {
    return <ThemeContext.Consumer>
        {value => <button onClick={() => value.changeColor('red')}>change</button>}
    </ThemeContext.Consumer>
}

export default Parent;

可以看到,父组件上面的color属性和changeColor方法,都可以在子组件,子子组件中通过context来获取到,而无须通过props的层层传递.

buxuku commented 3 years ago

React有一条思想:一切皆组件,上面的方法虽然是功能上实现了,但createContext返回的ProviderConsumer并不是一个组件.对于React.createContext({color: 'red'});这个返回值 ,我们先看看原版返回的是什么.

image-20210616112012897

可以看到,它返回的类似于之前的虚拟Dom,通过$$typeof标识类型,ProviderConnsumer上面同时挂载了一个_context的循环引用.最外层的_currentValue_currentValue2挂载了对应的value值,挂载两个主要用于主渲染和辅助渲染使用,这里我们关心一个即可.其它更多属性是在开发环境下增加了,也可以无须关注.

首先在src/constants/index.js里面新增两个节点类型来做标识

+export const REACT_CONTEXT = Symbol('REACT_CONTEXT');
+export const REACT_PROVIDER = Symbol('REACT_PROVIDER');

按照官方生成的对象修改createContext方法

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

-function createContext(value){
-    let context = {
-        _value: value,
-        Provider,
-        Consumer,
-    };
-    function Provider({value, children}){ // Provider接收一个value的props
-        context._value = value;
-        return children;
-    }
-    function Consumer({children}){ // Consumer的children是一个函数
-        return children(context._value)
-    }
-    return context;
-}

+function createContext(value){
+    const context = {$$typeof: REACT_CONTEXT, _currentValue: null};
+    context.Provider = {
+        $$typeof: REACT_PROVIDER,
+        _context: context,
+    };
+    context.Consumer = {
+        $$typeof: REACT_CONTEXT,
+        _context: context,
+    }
+    return context;
+}

这样就可以返回一个基本上和官方主体内容一致的对象了.

这样改造之后,就需要在渲染的时候,增加这两种类型组件的判断.

修改src/react-dom/index.js,增加mountProviderComponentmountContextComponent方法, 在createDom里面增加这两种组件类型的判断,同时需要注意这一句vdom.oldVdom = renderVdom;,我们同样需要把它们子元素生成的虚拟Dom挂载在上面,以便后续更新的时候进行dom-diff;

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

function createDom(vdom) {
    if (isNotNeedRender(vdom)) return
    let dom;
    const {type, props, ref} = 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;
}

+/**
+ * Provider组件的渲染,先更新Provider上面传递的value属性,然后渲染它的children元素
+ * @param vdom
+ * @returns {Text|*|HTMLElement|Text|HTMLElement}
+ */
+function mountProviderComponent(vdom){
+    const {type, props} = vdom;
+    type._context._currentValue = props.value;
+    const renderVdom = props.children;
+    vdom.oldVdom = renderVdom;
+    return createDom(renderVdom);
+}
+
+/**
+ * Consumer组件的渲染,获取到context上面的value,作为children函数的入参来获取返回的虚拟Dom
+ * @param vdom
+ * @returns {Text|*|HTMLElement|Text|HTMLElement}
+ */
+function mountContextComponent(vdom){
+    const {type, props} = vdom;
+    const renderVdom = props.children(type._context._currentValue);
+    vdom.oldVdom = renderVdom;
+    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_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);
        }
    }
}

+/**
+ * 更新Provider组件,先更新value值,然后把children的虚拟dom和之前挂载的老的虚拟dom进行对比更新
+ * @param oldVdom
+ * @param newVdom
+ */
+function updateProviderComponent(oldVdom, newVdom){
+    const parentDom = findDom(oldVdom);
+    const {type, props} = newVdom;
+    type._context._currentValue = props.value;
+    compareTwoVdoms(oldVdom.oldVdom, newVdom.props.children, parentDom);
+    newVdom.oldVdom = newVdom.props.children;
+}
+
+/**
+ * 更新Consumer组件,通过新的context上面的value值生成新的虚拟dom,和老的虚拟dom进行对比更新
+ * @param oldVdom
+ * @param newVdom
+ */
+function updateConsumerComponent(oldVdom, newVdom){
+    const parentDom = findDom(oldVdom);
+    const {type, props} = newVdom;
+    const renderVdom = props.children(type._context._currentValue);
+    compareTwoVdoms(oldVdom.oldVdom, renderVdom, parentDom);
+    newVdom.oldVdom = renderVdom;
+}
+

注意上面用到的findDom方法,之前只涉及到原生的HTML元素,类组件,函数组件的判断.这里也需要进行修改一下

function findDom(vdom) {
    if (!vdom || typeof vdom !== 'object') return;
    const {type} = vdom;
    let dom;
    /**
     * 比如render(){return <Demo />}这里面render出来的还不是最终的虚拟dom;
     */
-    if (typeof type === 'function') {
+    if (typeof type === 'string' || type === REACT_TEXT) {
+        dom = vdom.dom;
+    } else {
        if(vdom.classInstance){
            dom = findDom(vdom.classInstance.oldVdom);
        }else{
            // vdom可能是一个函数或者类组件,需要继续递归查找真实的DOM节点.
            dom = findDom(vdom.oldVdom);
        }
-    } else {
-        dom = vdom.dom;
    }
    return dom;
}

修改src/react-dom/index.js里面mountClassComponent方法里面的contextType变量名

    if(type.contextType){
-        classInstance.context = type.contextType._value;
+        classInstance.context = type.contextType._currentValue;
    }

以及src/react/Component.js里面的forceUpdate方法里面的

if(this.constructor.contextType){
-  this.context = this.constructor.contextType._value;
+  this.context = this.constructor.contextType._currentValue;
}

再来测试一下前面的CreatContext组件,不出意外应该也是能够正常运行的.