• 从0实现一个React,个人总结


    原文:

    https://github.com/hujiulong/blog/issues/4

    个人总结::

    一、JSX和虚拟DOM

    import React from 'react';   
    import ReactDOM from 'react-dom';


    先说'react',react内部主要实现了创建react元素createElement这个方法,
    再说'react-dom',react-dom主要实现了将虚拟DOM转换成实体DOM然后挂载的功能

     

    二、组件和生命周期

    React定义组件的方式可以分为两种:函数和类,函数定义可以看做是类定义的一种简单形式。

    组件基类React.Component

    通过类的方式定义组件,我们需要继承React.Component

    。。。

    Componet

    React.Component包含了一些预先定义好的变量和方法,我们来一步一步地实现它:
    先定义一个Component类:

    class Component {}
    。。。

    state & props

    通过继承React.Component定义的组件有自己的私有状态state,可以通过this.state获取到。同时也能通过this.props来获取传入的数据。
    所以在构造函数中,我们需要初始化stateprops

    。。。

    setState

    组件内部的state和渲染结果相关,当state改变时通常会触发渲染,为了让React知道我们改变了state,我们只能通过setState方法去修改数据。我们可以通过Object.assign来做一个简单的实现。
    在每次更新state后,我们需要调用renderComponent方法来重新渲染组件,renderComponent方法的实现后文会讲到。

    render

    上一篇文章中实现的render方法只支持渲染原生DOM元素,我们需要修改ReactDOM.render方法,让其支持渲染组件。

    。。。

    我们需要在其中加一段用来渲染组件的代码:

    function _render( vnode, container ) {
    
        // ...
    
        if ( typeof vnode.tag === 'function' ) {
    
            const component = createComponent( vnode.tag, vnode.attrs );
    
            setComponentProps( component, vnode.attrs );
    
            return component.base;
        }
        
        // ...
    }

    组件渲染和生命周期

    在上面的方法中用到了createComponentsetComponentProps两个方法,组件的生命周期方法也会在这里面实现。

    。。。

    三、diff算法

    对比策略

    在前面两篇文章后,我们实现了一个render方法,它能将虚拟DOM渲染成真正的DOM,我们现在就需要改进它,让它不要再傻乎乎地重新渲染整个DOM树,而是找出真正变化的部分。

    这部分很多类React框架实现方式都不太一样,有的框架会选择保存上次渲染的虚拟DOM,然后对比虚拟DOM前后的变化,得到一系列更新的数据,然后再将这些更新应用到真正的DOM上。

    但也有一些框架会选择直接对比虚拟DOM和真实DOM,这样就不需要额外保存上一次渲染的虚拟DOM,并且能够一边对比一边更新,这也是我们选择的方式。

    不管是DOM还是虚拟DOM,它们的结构都是一棵树,完全对比两棵树变化的算法时间复杂度是O(n^3),但是考虑到我们很少会跨层级移动DOM,所以我们只需要对比同一层级的变化。

    总而言之,我们的diff算法有两个原则:

    • 对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM
    • 只对比同一层级的变化

    。。。

    实现diff算法可以说性能有了很大的提升,但是在别的地方仍然后很多改进的空间:每次调用setState后会立即调用renderComponent重新渲染组件,但现实情况是,我们可能会在极短的时间内多次调用setState。
    假设我们在上文的Counter组件中写出了这种代码

    onClick() {
        for ( let i = 0; i < 100; i++ ) {
            this.setState( { num: this.state.num + 1 } );
        }
    }

    那以目前的实现,每次点击都会渲染100次组件,对性能肯定有很大的影响。
    下一篇文章我们就要来改进setState方法

    React显然也遇到了这样的问题,所以针对setState做了一些特别的优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新,举个栗子:

    所以,这篇文章的目标也明确了,我们要实现以下两个功能:

    1. 异步更新state,将短时间内的多个setState合并成一个
    2. 为了解决异步更新导致的问题,增加另一种形式的setState:接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态

    setState队列

    为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件。

    队列是一种数据结构,它的特点是“先进先出”,可以通过js数组的push和shift方法模拟
    然后需要定义一个”入队“的方法,用来将更新添加进队列。

    const queue = [];
    function enqueueSetState( stateChange, component ) {
        queue.push( {
            stateChange,
            component
        } );
    }

    然后修改组件的setState方法,不再直接更新state和渲染组件,而是添加进更新队列。

    setState( stateChange ) {
        enqueueSetState( stateChange, this );
    }

    现在队列是有了,怎么清空队列并渲染组件呢?

    清空队列

    我们定义一个flush方法,它的作用就是清空队列

    function flush() {
        let item;
        // 遍历
        while( item = setStateQueue.shift() ) {
    
            const { stateChange, component } = item;
    
            // 如果没有prevState,则将当前的state作为初始的prevState
            if ( !component.prevState ) {
                component.prevState = Object.assign( {}, component.state );
            }
    
            // 如果stateChange是一个方法,也就是setState的第二种形式
            if ( typeof stateChange === 'function' ) {
                Object.assign( component.state, stateChange( component.prevState, component.props ) );
            } else {
                // 如果stateChange是一个对象,则直接合并到setState中
                Object.assign( component.state, stateChange );
            }
    
            component.prevState = component.state;
    
        }
    }

    这只是实现了state的更新,我们还没有渲染组件。渲染组件不能在遍历队列时进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件。

    我们在enqueueSetState时,就可以做这件事

    const queue = [];
    const renderQueue = [];
    function enqueueSetState( stateChange, component ) {
        queue.push( {
            stateChange,
            component
        } );
        // 如果renderQueue里没有当前组件,则添加到队列中
        if ( !renderQueue.some( item => item === component ) ) {
            renderQueue.push( component );
        }
    }

    在flush方法中,我们还需要遍历renderQueue,来渲染每一个组件

    function flush() {
        let item, component;
        while( item = queue.shift() ) {
            // ...
        }
        // 渲染每一个组件
        while( component = renderQueue.shift() ) {
            renderComponent( component );
        }
    
    }

    延迟执行

    现在还有一件最重要的事情:什么时候执行flush方法。
    我们需要合并一段时间内所有的setState,也就是在一段时间后才执行flush方法来清空队列,关键是这个“一段时间“怎么决定。

    一个比较好的做法是利用js的事件队列机制。

    先来看这样一段代码:

    setTimeout( () => {
        console.log( 2 );
    }, 0 );
    Promise.resolve().then( () => console.log( 1 ) );
    console.log( 3 );

    你可以打开浏览器的调试工具运行一下,它们打印的结果是:

    3
    1
    2
    

    具体的原理可以看阮一峰的这篇文章,这里就不再赘述了。

    我们可以利用事件队列,让flush在所有同步任务后执行

    function enqueueSetState( stateChange, component ) {
        // 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
        if ( queue.length === 0 ) {
            defer( flush );
        }
        queue.push( {
            stateChange,
            component
        } );
        if ( !renderQueue.some( item => item === component ) ) {
            renderQueue.push( component );
        }
    }

    定义defer方法,利用刚才题目中出现的Promise.resolve

    function defer( fn ) {
        return Promise.resolve().then( fn );
    }

    这样在一次“事件循环“中,最多只会执行一次flush了,在这个“事件循环”中,所有的setState都会被合并,并只渲染一次组件。

    别的延迟执行方法

    除了用Promise.resolve().then( fn ),我们也可以用上文中提到的setTimeout( fn, 0 ),setTimeout的时间也可以是别的值,例如16毫秒。

    16毫秒的间隔在一秒内大概可以执行60次,也就是60帧,人眼每秒只能捕获60幅画面

    另外也可以用requestAnimationFrame或者requestIdleCallback

    function defer( fn ) {
        return requestAnimationFrame( fn );
    }

    试试效果

    就试试渲染上文中用React渲染的那两个例子:

    class App extends Component {
        constructor() {
            super();
            this.state = {
                num: 0
            }
        }
        componentDidMount() {
            for ( let i = 0; i < 100; i++ ) {
                this.setState( { num: this.state.num + 1 } );
                console.log( this.state.num ); 
            }
        }
        render() {
            return (
                <div className="App">
                    <h1>{ this.state.num }</h1>
                </div>
            );
        }
    }

    效果和React完全一样
    1
    同样,用第二种方式调用setState:

    componentDidMount() {
        for ( let i = 0; i < 100; i++ ) {
            this.setState( prevState => {
                console.log( prevState.num );
                return {
                    num: prevState.num + 1
                }
            } );
        }
    }

    结果也完全一样:
    1

  • 相关阅读:
    BackgroundWorker原理剖析
    委托异步调用时BeginInvoke的陷阱处理
    线程静态在对象缓存中的妙用
    值得珍藏的.NET源码,不保存就没机会了
    .NET 4.5.1 参考源码索引
    .NET 4.5 参考源码索引
    .NET 4.0 参考源码索引
    WWF3.5SP1 参考源码索引
    WCF3.5 SP1 参考源码索引
    .NET 3.5.1 参考源码索引
  • 原文地址:https://www.cnblogs.com/eret9616/p/9039977.html
Copyright © 2020-2023  润新知