王鹏飞

Blog

Tutorial

About

React

2021年5月18日

React hook的神秘面纱

众所周知,React有类组件和函数组件两种形式的组件,开发者可以使用类组件和函数组件达到相同的目的,构建完全一样的页面。自从去年八月份接触React以来,我一直使用的就是函数组件,因为公司推荐使用函数组件。不得不说函数组件比类组件好用很多。使用函数组件,不用去考虑复杂的组件生命周期,因为组件的生命周期都被hook替代了,你可以使用useEffect实现类组件的componentDidUpdatecomponentDidMountcomponentWillUnmount。在感叹hook强大的同时,我也经常在想React到底是怎么实现useEffect和useState hook的呢?函数组件每次渲染,都仅仅是将组件函数执行一遍,函数不是instance(无this指针),那么组件的状态是如何记录和更新的呢?可能你也和我一样,都会猜到函数组件的状态是通过闭包(closure)实现的,那么恭喜你,你猜的是对的,hook就是利用了闭包的思想。对闭包不了解的伙伴,请先移步MDN,学习下闭包的概念,在来到这里继续阅读。

本篇文章时长大约15分钟,相信你看完后必定有所收获。 文章内容主要是通过自己手动实现一个模拟的React加深了解hook的工作原理。包括最常用的useEffect和useState的实现。我参考了一些博客和资料,然后自己手动整理实践。

本篇文章代码github: https://github.com/pengfeiw/smallJsCodeDemo/blob/master/realizeReactHook.js

一.基础

在模拟实现React之前,我们必须了解一些概念。

1. JSX

JSX就是一个普通的javascript对象,Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。 例如下面两段代码,是等价的。

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

// 经过babel转译后
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React.createElement返回的就是一个普通的javascript Object.

2. 渲染实际就是执行函数

函数组件每次重新渲染,其实就是组件的函数执行一次。

例如下面的App组件每次重新渲染,其实就是执行App(props)。

const App = (props) => {
    ...
};

二. 模拟React

1. 一个简单的React

首先我们自己实现一个最简单的,只有render函数的React。render函数接受一个Component(组件)作为参数,exampleProps表示传递给组件的Props。

const React = {
    render: Component => {
        const exampleProps = {
            unit: "likes"
        };
        const compo = Component(exampleProps);
        compo.render();
        return compo;
    }
}

为了测试这个简易版的React,我们需要一个组件,组件的render函数输出了一些信息。

const Component = (props) => {
    return {
        render: () => {
            console.log("render", {
                type: "div",
                inner: `${props.unit}`
            })
        }
    };
}

接着实际应用看下效果。

let App = React.render(Component); // log: render {type: "div", inner: "likes"}
App = React.render(Component); // log: render {type: "div", inner: "likes"}

我们Component渲染了两次,两次渲染都打印了log信息。

2. useState

接下来,我们扩展React,实现useState。useState可以返回一个值和一个可以更新该值的dispatcher,useState接受一个初始值参数,可以设定state的初始值。

Returns a stateful value, and a function to update it.

函数组件通过useState,来创建和更新组件的状态,是react中最常用hook之一。下面我们自己实现useState。

const React = {
    index: 0,
    state: [],
    useState: defaultProp => {
        const cachedIndex = React.index;
        if (!React.state[cachedIndex]) {
            React.state[cachedIndex] = defaultProp;
        }
        const currentState = React.state[cachedIndex];
        const currentSetter = newValue => {
            React.state[cachedIndex] = newValue;
        };
        React.index++;

        return [currentState, currentSetter];
    },
    render: Component => {
        const exampleProps = {
            unit: "likes"
        };

        const compo = Component(exampleProps);
        compo.render();

        React.index = 0; // reset index

        return compo;
    }
}

这里用到了闭包的功能。我们把state值存储到React.state数组中,然后创建了一个currentSetter,用来更新state值。最后增加index,用于存储新的state值。由于闭包的特性,在组件函数内有多个useState值时,每个cachedIndex都是独立的,所以每次currentSetter对应的state也是独立的。还有一点需要注意的是在render函数中必须重置React.index = 0,因为组件函数每次执行,index都必须为0,因为hook也是每次都执行的。

更新Component,为其添加状态。

const Component = (props) => {
    const [count, setCount] = React.useState(0);
    const [name, setName] = React.useState("Steve");

    return {
        click: () => setCount(count + 1),
        personArrived: person => setName(person),
        render: () => {
            console.log("render", {
                type: "div",
                inner: `${count} ${props.unit} for ${name}`
            })
        }
    };
}

测试组件渲染。

let App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}
App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}

App.click();
App = React.render(Component);  // log: render {type: "div", inner: "1 likes for Steve"}

App.click();
App.personArrived("Peter");
App = React.render(Component); // log: render {type: "div", inner: "2 likes for Peter"}

我们模拟的useState功能符合预期。虽然与实际的React中的useState相差很大,但是足够帮助我们理解了。

3. useEffect

在之前的文章我有讲解过useEffect和useLayoutEffect的区别。useEffect是异步执行的,它在组件函数执行后再执行。文档是这样描述useEffect的,我摘抄了几点最重要的特性。

  1. The function passed to useEffect will run after the render is committed to the screen.
  2. By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.
  3. the function passed to useEffect may return a clean-up function.

第一点就是表示useEffect是异步的,第二点表示我们可以给useEffect传递第二个参数(依赖)控制useEffect中的第一个函数参数是否执行,第三点就是我们传递的函数可以返回一个函数,作为cleanup或者unsubscribe函数。

我们就按照上面三个特性,实现自己的useEffect。

const React = {
    index: 0,
    state: [],
    useState: defaultProp => {
        const cachedIndex = React.index;
        if (!React.state[cachedIndex]) {
            React.state[cachedIndex] = defaultProp;
        }

        const currentState = React.state[cachedIndex];
        const currentSetter = newValue => {
            React.state[cachedIndex] = newValue;
        };
        React.index++;

        return [currentState, currentSetter];
    },
    useEffect: (callback, dependencies) => {
        const cachedIndex = React.index;
        const hasChanged = dependencies !== React.state[cachedIndex];
        if (dependencies === undefined || hasChanged) {
            callback();
            React.state[cachedIndex] = dependencies;
        }

        React.index++;
        return () => console.log("unsubscribed effect");
    },
    render: Component => {
        const exampleProps = {
            unit: "likes"
        };

        const compo = Component(exampleProps);
        compo.render();

        React.index = 0; // reset index

        return compo;
    }
}

将dependencies存储在React.state中,通过比较dependencies与React.state[cachedIndex](之前存储的dependencies)来判断依赖是否改变。如果改变了就执行callback,并更新React.state中存储的dependencies,用于下次执行判断。

同样我们更新下Component,加入useEffect hook。

const Component = (props) => {
    const [count, setCount] = React.useState(0);
    const [name, setName] = React.useState("Steve");

    const exitThis = React.useEffect(() => {
        console.log("Effect ran");
    }, name);

    return {
        click: () => setCount(count + 1),
        personArrived: person => setName(person),
        unsubscribe: () => exitThis(),
        render: () => {
            console.log("render", {
                type: "div",
                inner: `${count} ${props.unit} for ${name}`
            })
        }
    };
}

最后返回对象包含了一个unsubscribe,用于模拟useEffect的cleanup工作。

测试useEffect功能。

let App = React.render(Component);
// log: Effect ran
//      render {type: "div", inner: "0 likes for Steve"}

App = React.render(Component); // log: render {type: "div", inner: "0 likes for Steve"}

App.click();
App = React.render(Component); // log: render {type: "div", inner: "1 likes for Steve"}

App.click();
App.personArrived("Peter");
App = React.render(Component);
// log: Effect ran
//      render {type: "div", inner: "2 likes for Steve"}

App.unsubscribe(); // log: unsubscribed effect

可以看到,useEffect的函数在组件首次加载时执行了,在name更改时,也执行了,最后调用unsubscribe执行清理工作。

到这里,内容差不多结束了,你们一定对hook有了更深入的了解。还有更多类似的hook,你们也可以尝试自己去模拟实现,例如useCallback。

附参考资料:

react官方文档:https://react.docschina.org/docs/getting-started.html

under-the-hood of react:https://itnext.io/under-the-hood-of-react-hooks-805dc68581c3

(完)

留言(0


发表评论

邮箱地址不会被公开。*表示必填项