2017-05-23 11:03:41 Generon 阅读数 2092
  • 2019 react入门至高阶实战,含react hooks

    这是2019 react入门到高级新课程 学习react,不仅能带来技术提升,同时提高开发效率和体验,更能带来好的就业机会。 本课程主要分为以下几个部分:  一,前端工程化基础?  主要学习node和npm、yarn的基本用法  二,es6语法 学习必备的es6常用语法 。 三,react基础知识  学习如何搭建react项目,以及react组件,jsx语法、css处理方案、生命周期等基础知识。 并且根据这些知识开发一个个人网站。 四,react进阶知识?? 学习表单的处理,事件处理,Portals的使用,以及数据请求和API管理等进阶知识。 五,react高阶知识?? 学习react高级特性,react hooks,以及整个react生态体系的构成和应用 。 努力学习哟,带你精通react。

    1981 人正在学习 去看看 梁富城

原文地址:http://www.html-js.com/article/JS-analysis-of-the-single-row-from-zero-reactjs-source-first-rendering-principle%203154

前端的发展特别快,经历过jQuery一统天下的工具库时代后,现在各种框架又开始百家争鸣了。angular,ember,backbone,vue,avalon,ploymer还有reactjs,作为一个前端真是稍不留神就感觉要被淘汰了,就在去年大家还都是AngularJS的粉丝,到了今年又开始各种狂追reactjs了。前端都是喜新厌旧的,不知道最后这些框架由谁来一统天下,用句很俗的话说,这是最好的时代也是最坏的时代。作为一个前端,只能多学点,尽量多的了解他们的原理。

reactjs的代码非常绕,对于没有后台开发经验的前端来说看起来会比较吃力。其实reactjs的核心内容并不多,主要是下面这些:

  • 虚拟dom对象(Virtual DOM)
  • 虚拟dom差异化算法(diff algorithm)
  • 单向数据流渲染(Data Flow)
  • 组件生命周期
  • 事件处理

下面我们将一点点的来实现一个简易版的reactjs,实现上面的那些功能,最后用这个reactjs做一个todolist的小应用,看完这个,或者跟着敲一遍代码。希望让大家能够更好的理解reactjs的运行原理。

先从最简单的开始

我们先从渲染hello world开始吧。

我们看下面的代码:


<script type="text/javascript">
React.render('hello world',document.getElementById("container"))
</script>

/**
对应的html为

<div id="container"></div>


生成后的html为:

<div id="container">
    <span data-reactid="0">hello world</span>
</div>

*/

假定这一行代码,就可以把hello world渲染到对应的div里面。

我们来看看我们需要为此做些什么:


//component类,用来表示文本在渲染,更新,删除时应该做些什么事情
function ReactDOMTextComponent(text) {
    //存下当前的字符串
    this._currentElement = '' + text;
    //用来标识当前component
    this._rootNodeID = null;
}

//component渲染时生成的dom结构
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
    this._rootNodeID = rootID;
    return '<span data-reactid="' + rootID + '">' + this._currentElement + '</span>';
}


//component工厂  用来返回一个component实例
function instantiateReactComponent(node){
    if(typeof node === 'string' || typeof node === 'number'){
        return new ReactDOMTextComponent(node)
    }
}


React = {
    nextReactRootIndex:0,
    render:function(element,container){

        var componentInstance = instantiateReactComponent(element);
        var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
        $(container).html(markup);
        //触发完成mount的事件
        $(document).trigger('mountReady');    }
}

代码分为三个部分:

  1. React.render 作为入口负责调用渲染
  2. 我们引入了component类的概念,ReactDOMTextComponent是一个component类定义,定义对于这种文本类型的节点,在渲染,更新,删除时应该做什么操作,这边暂时只用到渲染,另外两个可以先忽略
  3. instantiateReactComponent用来根据element的类型(现在只有一种string类型),返回一个component的实例。其实就是个类工厂。

nextReactRootIndex作为每个component的标识id,不断加1,确保唯一性。这样我们以后可以通过这个标识找到这个元素。

可以看到我们把逻辑分为几个部分,主要的渲染逻辑放在了具体的componet类去定义。React.render负责调度整个流程,这里是调用instantiateReactComponent生成一个对应component类型的实例对象,然后调用此对象的mountComponent获取生成的内容。最后写到对应的Container节点中。

可能有人问,这么p大点功能,有必要这么复杂嘛,别急。往下看才能体会这种分层的好处。

引入基本elemetnt

我们知道reactjs最大的卖点就是它的虚拟dom概念,我们一般使用React.createElement来创建一个虚拟dom元素。

虚拟dom元素分为两种,一种是浏览器自带的基本元素比如 div p input form 这种,一种是自定义的元素。

这边需要说一下我们上节提到的文本节点,它不算虚拟dom,但是reacjs为了保持渲染的一致性。文本节点是在外面包了一层span标记,也给它配了个简化版component(ReactDOMTextComponent)。

这节我们先讨论浏览器的基本元素。

在reactjs里,当我们希望在hello world外面包一层div,并且带上一些属性,甚至事件时我们可以这么写:


//演示事件监听怎么用
function hello(){
    alert('hello')
}


var element = React.createElement('div',{id:'test',onclick:hello},'click me')

React.render(element,document.getElementById("container"))


/**

//生成的html为:

<div data-reactid="0" id="test">
    <span data-reactid="0.0">click me</span>
</div>


//点击文字,会弹出hello的对话框

*/

上面使用React.createElement创建了一个基本元素,我们来看看简易版本React.createElement的实现:


//ReactElement就是虚拟dom的概念,具有一个type属性代表当前的节点类型,还有节点的属性props
//比如对于div这样的节点type就是div,props就是那些attributes
//另外这里的key,可以用来标识这个element,用于优化以后的更新,这里可以先不管,知道有这么个东西就好了
function ReactElement(type,key,props){
  this.type = type;
  this.key = key;
  this.props = props;
}


React = {
    nextReactRootIndex:0,
    createElement:function(type,config,children){
        var props = {},propName;
        config = config || {}
        //看有没有key,用来标识element的类型,方便以后高效的更新,这里可以先不管
        var key = config.key || null;

        //复制config里的内容到props
        for (propName in config) {
            if (config.hasOwnProperty(propName) && propName !== 'key') {
                props[propName] = config[propName];
            }
        }

        //处理children,全部挂载到props的children属性上
        //支持两种写法,如果只有一个参数,直接赋值给children,否则做合并处理
        var childrenLength = arguments.length - 2;
        if (childrenLength === 1) {
            props.children = $.isArray(children) ? children : [children] ;
        } else if (childrenLength > 1) {
            var childArray = Array(childrenLength);
            for (var i = 0; i < childrenLength; i++) {
                childArray[i] = arguments[i + 2];
            }
            props.children = childArray;
        }

        return new ReactElement(type, key,props);

    },
    render:function(element,container){
        var componentInstance = instantiateReactComponent(element);
        var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
        $(container).html(markup);
        //触发完成mount的事件
        $(document).trigger('mountReady');
    }
}

createElement只是做了简单的参数修正,最终返回一个ReactElement实例对象也就是我们说的虚拟元素的实例。

这里注意key的定义,主要是为了以后更新时优化效率,这边可以先不管忽略。

好了有了元素实例,我们得把他渲染出来,此时render接受的是一个ReactElement而不是文本,我们先改造下instantiateReactComponent:


function instantiateReactComponent(node){
    //文本节点的情况
    if(typeof node === 'string' || typeof node === 'number'){
        return new ReactDOMTextComponent(node);
    }
    //浏览器默认节点的情况
    if(typeof node === 'object' && typeof node.type === 'string'){
        //注意这里,使用了一种新的component
        return new ReactDOMComponent(node);

    }
}

我们增加了一个判断,这样当render的不是文本而是浏览器的基本元素时。我们使用另外一种component来处理它渲染时应该返回的内容。这里就体现了工厂方法instantiateReactComponent的好处了,不管来了什么类型的node,都可以负责生产出一个负责渲染的component实例。这样render完全不需要做任何修改,只需要再做一种对应的component类型(这里是ReactDOMComponent)就行了。

所以重点我们来看看ReactDOMComponent的具体实现:


//component类,用来表示文本在渲染,更新,删除时应该做些什么事情
function ReactDOMComponent(element){
    //存下当前的element对象引用
    this._currentElement = element;
    this._rootNodeID = null;
}

//component渲染时生成的dom结构
ReactDOMComponent.prototype.mountComponent = function(rootID){
    //赋值标识
    this._rootNodeID = rootID;
    var props = this._currentElement.props;
    var tagOpen = '<' + this._currentElement.type;
    var tagClose = '</' + this._currentElement.type + '>';

    //加上reactid标识
    tagOpen += ' data-reactid=' + this._rootNodeID;

    //拼凑出属性
    for (var propKey in props) {

        //这里要做一下事件的监听,就是从属性props里面解析拿出on开头的事件属性的对应事件监听
        if (/^on[A-Za-z]/.test(propKey)) {
            var eventType = propKey.replace('on', '');
            //针对当前的节点添加事件代理,以_rootNodeID为命名空间
            $(document).delegate('[data-reactid="' + this._rootNodeID + '"]', eventType + '.' + this._rootNodeID, props[propKey]);
        }

        //对于children属性以及事件监听的属性不需要进行字符串拼接
        //事件会代理到全局。这边不能拼到dom上不然会产生原生的事件监听
        if (props[propKey] && propKey != 'children' && !/^on[A-Za-z]/.test(propKey)) {
            tagOpen += ' ' + propKey + '=' + props[propKey];
        }
    }
    //获取子节点渲染出的内容
    var content = '';
    var children = props.children || [];

    var childrenInstances = []; //用于保存所有的子节点的componet实例,以后会用到
    var that = this;
    $.each(children, function(key, child) {
        //这里再次调用了instantiateReactComponent实例化子节点component类,拼接好返回
        var childComponentInstance = instantiateReactComponent(child);
        childComponentInstance._mountIndex = key;

        childrenInstances.push(childComponentInstance);
        //子节点的rootId是父节点的rootId加上新的key也就是顺序的值拼成的新值
        var curRootId = that._rootNodeID + '.' + key;
        //得到子节点的渲染内容
        var childMarkup = childComponentInstance.mountComponent(curRootId);
        //拼接在一起
        content += ' ' + childMarkup;

    })

    //留给以后更新时用的这边先不用管
    this._renderedChildren = childrenInstances;

    //拼出整个html内容
    return tagOpen + '>' + content + tagClose;
}

我们增加了虚拟dom reactElement的定义,增加了一个新的componet类ReactDOMComponent。 这样我们就实现了渲染浏览器基本元素的功能了。

对于虚拟dom的渲染逻辑,本质上还是个递归渲染的东西,reactElement会递归渲染自己的子节点。可以看到我们通过instantiateReactComponent屏蔽了子节点的差异,只需要使用不同的componet类,这样都能保证通过mountComponent最终拿到渲染后的内容。

另外这边的事件也要说下,可以在传递props的时候传入{onClick:function(){}}这样的参数,这样就会在当前元素上添加事件,代理到document。由于reactjs本身全是在写js,所以监听的函数的传递变得特别简单。

这里很多东西没有考虑,比如一些特殊的类型input select等等,再比如img不需要有对应的tagClose等。这里为了保持简单就不再扩展了。另外reactjs的事件处理其实很复杂,实现了一套标准的w3c事件。这里偷懒直接使用jQuery的事件代理到document上了。

自定义元素

上面实现了基本的元素内容,我们下面实现自定义元素的功能。

随着前端技术的发展浏览器的那些基本元素已经满足不了我们的需求了,如果你对webcomponents有一定的了解,就会知道人们一直在尝试扩展一些自己的标记。

reactjs通过虚拟dom做到了类似的功能,还记得我们上面element.type只是个简单的字符串,如果是个类呢?如果这个类恰好还有自己的生命周期管理,那扩展性就很高了。

如果对生命周期等概念不是很理解的,可以看看我以前的另一片文章:javascript组件化

我们看下reactjs怎么使用自定义元素:


var HelloMessage = React.createClass({
  getInitialState: function() {
    return {type: 'say:'};
  },
  componentWillMount: function() {
    console.log('我就要开始渲染了。。。')
  },
  componentDidMount: function() {
    console.log('我已经渲染好了。。。')
  },
  render: function() {
    return React.createElement("div", null,this.state.type, "Hello ", this.props.name);
  }
});


React.render(React.createElement(HelloMessage, {name: "John"}), document.getElementById("container"));

/**
结果为:

html:
<div data-reactid="0">
    <span data-reactid="0.0">say:</span>
    <span data-reactid="0.1">Hello </span>
    <span data-reactid="0.2">John</span>
</div>

console:
我就要开始渲染了。。。
我已经渲染好了。。。

*/

React.createElement接受的不再是字符串,而是一个class。

React.createClass生成一个自定义标记类,带有基本的生命周期:

  • getInitialState 获取最初的属性值this.state
  • componentWillmount 在组件准备渲染时调用
  • componentDidMount 在组件渲染完成后调用

对reactjs稍微有点了解的应该都可以明白上面的用法。

我们先来看看React.createClass的实现:


//定义ReactClass类,所有自定义的超级父类
var ReactClass = function(){
}
//留给子类去继承覆盖
ReactClass.prototype.render = function(){}



React = {
    nextReactRootIndex:0,
    createClass:function(spec){
        //生成一个子类
        var Constructor = function (props) {
            this.props = props;
            this.state = this.getInitialState ? this.getInitialState() : null;
        }
        //原型继承,继承超级父类
        Constructor.prototype = new ReactClass();
        Constructor.prototype.constructor = Constructor;
        //混入spec到原型
        $.extend(Constructor.prototype,spec);
        return Constructor;

    },
    createElement:function(type,config,children){
        ...
    },
    render:function(element,container){
        ...
    }
}


可以看到createClass生成了一个继承ReactClass的子类,在构造函数里调用this.getInitialState获得最初的state。

为了演示方便,我们这边的ReactClass相当简单,实际上原始的代码处理了很多东西,比如类的mixin的组合继承支持,比如componentDidMount等可以定义多次,需要合并调用等等,有兴趣的去翻源码吧,不是本文的主要目的,这里就不详细展开了。

我们这里只是返回了一个继承类的定义,那么具体的componentWillmount,这些生命周期函数在哪里调用呢。

看看我们上面的两种类型就知道,我们是时候为自定义元素也提供一个componet类了,在那个类里我们会实例化ReactClass,并且管理生命周期,还有父子组件依赖。

好,我们老规矩先改造instantiateReactComponent

function instantiateReactComponent(node){
    //文本节点的情况
    if(typeof node === 'string' || typeof node === 'number'){
        return new ReactDOMTextComponent(node);
    }
    //浏览器默认节点的情况
    if(typeof node === 'object' && typeof node.type === 'string'){
        //注意这里,使用了一种新的component
        return new ReactDOMComponent(node);

    }
    //自定义的元素节点
    if(typeof node === 'object' && typeof node.type === 'function'){
        //注意这里,使用新的component,专门针对自定义元素
        return new ReactCompositeComponent(node);

    }
}

很简单我们增加了一个判断,使用新的component类形来处理自定义的节点。我们看下 ReactCompositeComponent的具体实现:


function ReactCompositeComponent(element){
    //存放元素element对象
    this._currentElement = element;
    //存放唯一标识
    this._rootNodeID = null;
    //存放对应的ReactClass的实例
    this._instance = null;
}

//用于返回当前自定义元素渲染时应该返回的内容
ReactCompositeComponent.prototype.mountComponent = function(rootID){
    this._rootNodeID = rootID;
    //拿到当前元素对应的属性值
    var publicProps = this._currentElement.props;
    //拿到对应的ReactClass
    var ReactClass = this._currentElement.type;
    // Initialize the public class
    var inst = new ReactClass(publicProps);
    this._instance = inst;
    //保留对当前comonent的引用,下面更新会用到
    inst._reactInternalInstance = this;

    if (inst.componentWillMount) {
        inst.componentWillMount();
        //这里在原始的reactjs其实还有一层处理,就是  componentWillMount调用setstate,不会触发rerender而是自动提前合并,这里为了保持简单,就略去了
    }
    //调用ReactClass的实例的render方法,返回一个element或者一个文本节点
    var renderedElement = this._instance.render();
    //得到renderedElement对应的component类实例
    var renderedComponentInstance = instantiateReactComponent(renderedElement);
    this._renderedComponent = renderedComponentInstance; //存起来留作后用

    //拿到渲染之后的字符串内容,将当前的_rootNodeID传给render出的节点
    var renderedMarkup = renderedComponentInstance.mountComponent(this._rootNodeID);

    //之前我们在React.render方法最后触发了mountReady事件,所以这里可以监听,在渲染完成后会触发。
    $(document).on('mountReady', function() {
        //调用inst.componentDidMount
        inst.componentDidMount && inst.componentDidMount();
    });

    return renderedMarkup;
}

实现并不难,ReactClass的render一定是返回一个虚拟节点(包括element和text),这个时候我们使用instantiateReactComponent去得到实例,再使用mountComponent拿到结果作为当前自定义元素的结果。

应该说本身自定义元素不负责具体的内容,他更多的是负责生命周期。具体的内容是由它的render方法返回的虚拟节点来负责渲染的。

本质上也是递归的去渲染内容的过程。同时因为这种递归的特性,父组件的componentWillMount一定在某个子组件的componentWillMount之前调用,而父组件的componentDidMount肯定在子组件之后,因为监听mountReady事件,肯定是子组件先监听的。

需要注意的是自定义元素并不会处理我们createElement时传入的子节点,它只会处理自己render返回的节点作为自己的子节点。不过我们在render时可以使用this.props.children拿到那些传入的子节点,可以自己处理。其实有点类似webcomponents里面的shadow dom的作用。

上面实现了三种类型的元素,其实我们发现本质上没有太大的区别,都是有自己对应component类来处理自己的渲染过程。

大概的关系是下面这样。

结构图

于是我们发现初始化的渲染流程都已经完成了。

虚拟dom差异化算法(diff algorithm)是reactjs最核心的东西,按照官方的说法。他非常快,非常高效。目前已经有一些分析此算法的文章,但是仅仅停留在表面。大部分小白看完并不能了解(博主就是 = =)。所以我们下面自己动手实现一遍,等你完全实现了,再去看那些文字图片流的介绍文章,就会发现容易理解多了。

实现更新机制

下面我们探讨下更新的机制。

一般在reactjs中我们需要更新时都是调用的setState。看下面的例子:


var HelloMessage = React.createClass({
  getInitialState: function() {
    return {type: 'say:'};
  },
  changeType:function(){
    this.setState({type:'shout:'})
  },
  render: function() {
    return React.createElement("div", {onclick:this.changeType},this.state.type, "Hello ", this.props.name);
  }
});


React.render(React.createElement(HelloMessage, {name: "John"}), document.getElementById("container"));



/**

//生成的html为:

<div data-reactid="0" id="test">
    <span data-reactid="0.0">hello world</span>
</div>

点击文字,say会变成shout

*/

点击文字,调用setState就会更新,所以我们扩展下ReactClass,看下setState的实现:


//定义ReactClass类
var ReactClass = function(){
}

ReactClass.prototype.render = function(){}

//setState
ReactClass.prototype.setState = function(newState) {

    //还记得我们在ReactCompositeComponent里面mount的时候 做了赋值
    //所以这里可以拿到 对应的ReactCompositeComponent的实例_reactInternalInstance
    this._reactInternalInstance.receiveComponent(null, newState);
}

可以看到setState主要调用了对应的component的receiveComponent来实现更新。所有的挂载,更新都应该交给对应的component来管理。

就像所有的component都实现了mountComponent来处理第一次渲染,所有的componet类都应该实现receiveComponent用来处理自己的更新。

自定义元素的receiveComponent

所以我们照葫芦画瓢来给自定义元素的对应component类(ReactCompositeComponent)实现一个receiveComponent方法:


//更新
ReactCompositeComponent.prototype.receiveComponent = function(nextElement, newState) {

    //如果接受了新的,就使用最新的element
    this._currentElement = nextElement || this._currentElement

    var inst = this._instance;
    //合并state
    var nextState = $.extend(inst.state, newState);
    var nextProps = this._currentElement.props;


    //改写state
    inst.state = nextState;


    //如果inst有shouldComponentUpdate并且返回false。说明组件本身判断不要更新,就直接返回。
    if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps, nextState) === false)) return;

    //生命周期管理,如果有componentWillUpdate,就调用,表示开始要更新了。
    if (inst.componentWillUpdate) inst.componentWillUpdate(nextProps, nextState);


    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    //重新执行render拿到对应的新element;
    var nextRenderedElement = this._instance.render();


    //判断是需要更新还是直接就重新渲染
    //注意这里的_shouldUpdateReactComponent跟上面的不同哦 这个是全局的方法
    if (_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
        //如果需要更新,就继续调用子节点的receiveComponent的方法,传入新的element更新子节点。
        prevComponentInstance.receiveComponent(nextRenderedElement);
        //调用componentDidUpdate表示更新完成了
        inst.componentDidUpdate && inst.componentDidUpdate();

    } else {
        //如果发现完全是不同的两种element,那就干脆重新渲染了
        var thisID = this._rootNodeID;
        //重新new一个对应的component,
        this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
        //重新生成对应的元素内容
        var nextMarkup = _renderedComponent.mountComponent(thisID);
        //替换整个节点
        $('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup);

    }

}

//用来判定两个element需不需要更新
//这里的key是我们createElement的时候可以选择性的传入的。用来标识这个element,当发现key不同时,我们就可以直接重新渲染,不需要去更新了。
var _shouldUpdateReactComponent = function(prevElement, nextElement){
    if (prevElement != null && nextElement != null) {
    var prevType = typeof prevElement;
    var nextType = typeof nextElement;
    if (prevType === 'string' || prevType === 'number') {
      return nextType === 'string' || nextType === 'number';
    } else {
      return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
    }
  }
  return false;
}

不要被这么多代码吓到,其实流程很简单。 它主要做了什么事呢?首先会合并改动,生成最新的state,props然后拿以前的render返回的element跟现在最新调用render生成的element进行对比(_shouldUpdateReactComponent),看看需不需要更新,如果要更新就继续调用对应的component类对应的receiveComponent就好啦,其实就是直接当甩手掌柜,事情直接丢给手下去办了。当然还有种情况是,两次生成的element差别太大,就不是一个类型的,那好办直接重新生成一份新的代码重新渲染一次就o了。

本质上还是递归调用receiveComponent的过程。

这里注意两个函数:

  • inst.shouldComponentUpdate是实例方法,当我们不希望某次setState后更新,我们就可以重写这个方法,返回false就好了。
  • _shouldUpdateReactComponent是一个全局方法,这个是一种reactjs的优化机制。用来决定是直接全部替换,还是使用很细微的改动。当两次render出来的子节点key不同,直接全部重新渲染一遍,替换就好了。否则,我们就得来个递归的更新,保证最小化的更新机制,这样可以不会有太大的闪烁。

另外可以看到这里还处理了一套更新的生命周期调用机制。

文本节点的receiveComponent

我们再看看文本节点的,比较简单:


ReactDOMTextComponent.prototype.receiveComponent = function(nextText) {
    var nextStringText = '' + nextText;
    //跟以前保存的字符串比较
    if (nextStringText !== this._currentElement) {
        this._currentElement = nextStringText;
        //替换整个节点
        $('[data-reactid="' + this._rootNodeID + '"]').html(this._currentElement);

    }
}

没什么好说的,如果不同的话,直接找到对应的节点,更新就好了。

基本元素element的receiveComponent

最后我们开始看比较复杂的浏览器基本元素的更新机制。 比如我们看看下面的html:


<div id="test" name="hello">
    <span></span>
    <span></span>
</div>

想一下我们怎么以最小代价去更新这段html呢。不难发现其实主要包括两个部分:

  1. 属性的更新,包括对特殊属性比如事件的处理
  2. 子节点的更新,这个比较复杂,为了得到最好的效率,我们需要处理下面这些问题:
    • 拿新的子节点树跟以前老的子节点树对比,找出他们之间的差别。我们称之为diff
    • 所有差别找出后,再一次性的去更新。我们称之为patch

所以更新代码结构如下:


ReactDOMComponent.prototype.receiveComponent = function(nextElement) {
    var lastProps = this._currentElement.props;
    var nextProps = nextElement.props;

    this._currentElement = nextElement;
    //需要单独的更新属性
    this._updateDOMProperties(lastProps, nextProps);
    //再更新子节点
    this._updateDOMChildren(nextElement.props.children);
}

整体上也不复杂,先是处理当前节点属性的变动,后面再去处理子节点的变动

我们一步步来,先看看,更新属性怎么变更:


ReactDOMComponent.prototype._updateDOMProperties = function(lastProps, nextProps) {
    var propKey;
    //遍历,当一个老的属性不在新的属性集合里时,需要删除掉。

    for (propKey in lastProps) {
        //新的属性里有,或者propKey是在原型上的直接跳过。这样剩下的都是不在新属性集合里的。需要删除
        if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
            continue;
        }
        //对于那种特殊的,比如这里的事件监听的属性我们需要去掉监听
        if (/^on[A-Za-z]/.test(propKey)) {
            var eventType = propKey.replace('on', '');
            //针对当前的节点取消事件代理
            $(document).undelegate('[data-reactid="' + this._rootNodeID + '"]', eventType, lastProps[propKey]);
            continue;
        }

        //从dom上删除不需要的属性
        $('[data-reactid="' + this._rootNodeID + '"]').removeAttr(propKey)
    }

    //对于新的属性,需要写到dom节点上
    for (propKey in nextProps) {
        //对于事件监听的属性我们需要特殊处理
        if (/^on[A-Za-z]/.test(propKey)) {
            var eventType = propKey.replace('on', '');
            //以前如果已经有,说明有了监听,需要先去掉
            lastProps[propKey] && $(document).undelegate('[data-reactid="' + this._rootNodeID + '"]', eventType, lastProps[propKey]);
            //针对当前的节点添加事件代理,以_rootNodeID为命名空间
            $(document).delegate('[data-reactid="' + this._rootNodeID + '"]', eventType + '.' + this._rootNodeID, nextProps[propKey]);
            continue;
        }

        if (propKey == 'children') continue;

        //添加新的属性,或者是更新老的同名属性
        $('[data-reactid="' + this._rootNodeID + '"]').prop(propKey, nextProps[propKey])
    }

}

属性的变更并不是特别复杂,主要就是找到以前老的不用的属性直接去掉,新的属性赋值,并且注意其中特殊的事件属性做出特殊处理就行了。

下面我们看子节点的更新,也是最复杂的部分。


ReactDOMComponent.prototype.receiveComponent = function(nextElement){
    var lastProps = this._currentElement.props;
    var nextProps = nextElement.props;

    this._currentElement = nextElement;
    //需要单独的更新属性
    this._updateDOMProperties(lastProps,nextProps);
    //再更新子节点
    this._updateDOMChildren(nextProps.children);
}

//全局的更新深度标识
var updateDepth = 0;
//全局的更新队列,所有的差异都存在这里
var diffQueue = [];

ReactDOMComponent.prototype._updateDOMChildren = function(nextChildrenElements){
    updateDepth++
    //_diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
    this._diff(diffQueue,nextChildrenElements);
    updateDepth--
    if(updateDepth == 0){
        //在需要的时候调用patch,执行具体的dom操作
        this._patch(diffQueue);
        diffQueue = [];
    }
}

就像我们之前说的一样,更新子节点包含两个部分,一个是递归的分析差异,把差异添加到队列中。然后在合适的时机调用_patch把差异应用到dom上。

那么什么是合适的时机,updateDepth又是干嘛的?

这里需要注意的是,_diff内部也会递归调用子节点的receiveComponent于是当某个子节点也是浏览器普通节点,就也会走_updateDOMChildren这一步。所以这里使用了updateDepth来记录递归的过程,只有等递归回来updateDepth为0时,代表整个差异已经分析完毕,可以开始使用patch来处理差异队列了。

所以我们关键是实现_diff_patch两个方法。

我们先看_diff的实现:

//差异更新的几种类型
var UPATE_TYPES = {
    MOVE_EXISTING: 1,
    REMOVE_NODE: 2,
    INSERT_MARKUP: 3
}


//普通的children是一个数组,此方法把它转换成一个map,key就是element的key,如果是text节点或者element创建时并没有传入key,就直接用在数组里的index标识
function flattenChildren(componentChildren) {
    var child;
    var name;
    var childrenMap = {};
    for (var i = 0; i < componentChildren.length; i++) {
        child = componentChildren[i];
        name = child && child._currentelement && child._currentelement.key ? child._currentelement.key : i.toString(36);
        childrenMap[name] = child;
    }
    return childrenMap;
}


//主要用来生成子节点elements的component集合
//这边注意,有个判断逻辑,如果发现是更新,就会继续使用以前的componentInstance,调用对应的receiveComponent。
//如果是新的节点,就会重新生成一个新的componentInstance,
function generateComponentChildren(prevChildren, nextChildrenElements) {
    var nextChildren = {};
    nextChildrenElements = nextChildrenElements || [];
    $.each(nextChildrenElements, function(index, element) {
        var name = element.key ? element.key : index;
        var prevChild = prevChildren && prevChildren[name];
        var prevElement = prevChild && prevChild._currentElement;
        var nextElement = element;

        //调用_shouldUpdateReactComponent判断是否是更新
        if (_shouldUpdateReactComponent(prevElement, nextElement)) {
            //更新的话直接递归调用子节点的receiveComponent就好了
            prevChild.receiveComponent(nextElement);
            //然后继续使用老的component
            nextChildren[name] = prevChild;
        } else {
            //对于没有老的,那就重新新增一个,重新生成一个component
            var nextChildInstance = instantiateReactComponent(nextElement, null);
            //使用新的component
            nextChildren[name] = nextChildInstance;
        }
    })

    return nextChildren;
}



//_diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
ReactDOMComponent.prototype._diff = function(diffQueue, nextChildrenElements) {
  var self = this;
  //拿到之前的子节点的 component类型对象的集合,这个是在刚开始渲染时赋值的,记不得的可以翻上面
  //_renderedChildren 本来是数组,我们搞成map
  var prevChildren = flattenChildren(self._renderedChildren);
  //生成新的子节点的component对象集合,这里注意,会复用老的component对象
  var nextChildren = generateComponentChildren(prevChildren, nextChildrenElements);
  //重新赋值_renderedChildren,使用最新的。
  self._renderedChildren = []
  $.each(nextChildren, function(key, instance) {
    self._renderedChildren.push(instance);
  })


  var nextIndex = 0; //代表到达的新的节点的index
  //通过对比两个集合的差异,组装差异节点添加到队列中
  for (name in nextChildren) {
    if (!nextChildren.hasOwnProperty(name)) {
      continue;
    }
    var prevChild = prevChildren && prevChildren[name];
    var nextChild = nextChildren[name];
    //相同的话,说明是使用的同一个component,所以我们需要做移动的操作
    if (prevChild === nextChild) {
      //添加差异对象,类型:MOVE_EXISTING
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $('[data-reactid=' + self._rootNodeID + ']'),
        type: UPATE_TYPES.MOVE_EXISTING,
        fromIndex: prevChild._mountIndex,
        toIndex: nextIndex
      })
    } else { //如果不相同,说明是新增加的节点
      //但是如果老的还存在,就是element不同,但是component一样。我们需要把它对应的老的element删除。
      if (prevChild) {
        //添加差异对象,类型:REMOVE_NODE
        diffQueue.push({
          parentId: self._rootNodeID,
          parentNode: $('[data-reactid=' + self._rootNodeID + ']'),
          type: UPATE_TYPES.REMOVE_NODE,
          fromIndex: prevChild._mountIndex,
          toIndex: null
        })

        //如果以前已经渲染过了,记得先去掉以前所有的事件监听,通过命名空间全部清空
        if (prevChild._rootNodeID) {
            $(document).undelegate('.' + prevChild._rootNodeID);
        }

      }
      //新增加的节点,也组装差异对象放到队列里
      //添加差异对象,类型:INSERT_MARKUP
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $('[data-reactid=' + self._rootNodeID + ']'),
        type: UPATE_TYPES.INSERT_MARKUP,
        fromIndex: null,
        toIndex: nextIndex,
        markup: nextChild.mountComponent() //新增的节点,多一个此属性,表示新节点的dom内容
      })
    }
    //更新mount的index
    nextChild._mountIndex = nextIndex;
    nextIndex++;
  }



  //对于老的节点里有,新的节点里没有的那些,也全都删除掉
  for (name in prevChildren) {
    if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
      //添加差异对象,类型:REMOVE_NODE
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $('[data-reactid=' + self._rootNodeID + ']'),
        type: UPATE_TYPES.REMOVE_NODE,
        fromIndex: prevChild._mountIndex,
        toIndex: null
      })
      //如果以前已经渲染过了,记得先去掉以前所有的事件监听
      if (prevChildren[name]._rootNodeID) {
        $(document).undelegate('.' + prevChildren[name]._rootNodeID);
      }
    }
  }
}

我们分析下上面的代码,咋一看好多,好复杂,不急我们从入口开始看。

首先我们拿到之前的component的集合,如果是第一次更新的话,这个值是我们在渲染时赋值的。然后我们调用generateComponentChildren生成最新的component集合。我们知道component是用来放element的,一个萝卜一个坑。

注意flattenChildren我们这里把数组集合转成了对象map,以element的key作为标识,当然对于text文本或者没有传入key的element,直接用index作为标识。通过这些标识,我们可以从类型的角度来判断两个component是否是一样的。

generateComponentChildren会尽量的复用以前的component,也就是那些坑,当发现可以复用component(也就是key一致)时,就还用以前的,只需要调用他对应的更新方法receiveComponent就行了,这样就会递归的去获取子节点的差异对象然后放到队列了。如果发现不能复用那就是新的节点,我们就需要instantiateReactComponent重新生成一个新的component。

这里的flattenChildren需要给予很大的关注,比如对于一个表格列表,我们在最前面插入了一条数据,想一下如果我们创建element时没有传入key,所有的key都是null,这样reactjs在generateComponentChildren时就会默认通过顺序(index)来一一对应改变前跟改变后的子节点,这样变更前与变更后的对应节点判断(_shouldUpdateReactComponent)其实是不合适的。也就是说对于这种列表的情况,我们最好给予唯一的标识key,这样reactjs找对应关系时会更方便一点。

当我们生成好新的component集合以后,我们需要做出对比。组装差异对象。

对比老的集合和新的集合。我们需要找出涵盖四种情况,包括三种类型(UPATE_TYPES)的变动:

类型 情况
MOVE_EXISTING 新的component类型在老的集合里也有,并且element是可以更新的类型,在generateComponentChildren我们已经调用了receiveComponent,这种情况下prevChild=nextChild,那我们就需要做出移动的操作,可以复用以前的dom节点。
INSERT_MARKUP 新的component类型不在老的集合里,那么就是全新的节点,我们需要插入新的节点
REMOVE_NODE 老的component类型,在新的集合里也有,但是对应的element不同了不能直接复用直接更新,那我们也得删除。
REMOVE_NODE 老的component不在新的集合里的,我们需要删除

所以我们找出了这三种类型的差异,组装成具体的差异对象,然后加到了差异队列里面。

比如我们看下面这个例子,假设下面这些是某个父元素的子元素集合,上面到下面代表了变动流程:

变动

数字我们可以理解为给element的key。

正方形代表element。圆形代表了component。当然也是实际上的dom节点的位置。

从上到下,我们的4 2 1里 2 ,1可以复用之前的component,让他们通知自己的子节点更新后,再告诉2和1,他们在新的集合里需要移动的位置(在我们这里就是组装差异对象加到队列)。3需要删除,4需要新增。

好了,整个的diff就完成了,这个时候当递归完成,我们就需要开始做patch的动作了,把这些差异对象实打实的反映到具体的dom节点上。

我们看下_patch的实现:



//用于将childNode插入到指定位置
function insertChildAt(parentNode, childNode, index) {
    var beforeChild = parentNode.children().get(index);
    beforeChild ? childNode.insertBefore(beforeChild) : childNode.appendTo(parentNode);
}

ReactDOMComponent.prototype._patch = function(updates) {
    var update;
    var initialChildren = {};
    var deleteChildren = [];
    for (var i = 0; i < updates.length; i++) {
        update = updates[i];
        if (update.type === UPATE_TYPES.MOVE_EXISTING || update.type === UPATE_TYPES.REMOVE_NODE) {
            var updatedIndex = update.fromIndex;
            var updatedChild = $(update.parentNode.children().get(updatedIndex));
            var parentID = update.parentID;

            //所有需要更新的节点都保存下来,方便后面使用
            initialChildren[parentID] = initialChildren[parentID] || [];
            //使用parentID作为简易命名空间
            initialChildren[parentID][updatedIndex] = updatedChild;


            //所有需要修改的节点先删除,对于move的,后面再重新插入到正确的位置即可
            deleteChildren.push(updatedChild)
        }

    }

    //删除所有需要先删除的
    $.each(deleteChildren, function(index, child) {
        $(child).remove();
    })


    //再遍历一次,这次处理新增的节点,还有修改的节点这里也要重新插入
    for (var k = 0; k < updates.length; k++) {
        update = updates[k];
        switch (update.type) {
            case UPATE_TYPES.INSERT_MARKUP:
                insertChildAt(update.parentNode, $(update.markup), update.toIndex);
                break;
            case UPATE_TYPES.MOVE_EXISTING:
                insertChildAt(update.parentNode, initialChildren[update.parentID][update.fromIndex], update.toIndex);
                break;
            case UPATE_TYPES.REMOVE_NODE:
                // 什么都不需要做,因为上面已经帮忙删除掉了
                break;
        }
    }
}

_patch主要就是挨个遍历差异队列,遍历两次,第一次删除掉所有需要变动的节点,然后第二次插入新的节点还有修改的节点。这里为什么可以直接挨个的插入呢?原因就是我们在diff阶段添加差异节点到差异队列时,本身就是有序的,也就是说对于新增节点(包括move和insert的)在队列里的顺序就是最终dom的顺序,所以我们才可以挨个的直接根据index去塞入节点。

但是其实你会发现这里有个问题,就是所有的节点都会被删除,包括复用以前的component类型为UPATE_TYPES.MOVE_EXISTING的,所以闪烁会很严重。其实我们再看看上面的例子,其实2是不需要记录到差异队列的。这样后面patch也是ok的。想想是为什么呢?

我们来改造下代码:


//_diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
ReactDOMComponent.prototype._diff = function(diffQueue, nextChildrenElements){
    。。。
    /**注意新增代码**/
    var lastIndex = 0;//代表访问的最后一次的老的集合的位置
    var nextIndex = 0;//代表到达的新的节点的index
    //通过对比两个集合的差异,组装差异节点添加到队列中
    for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue;
        }
        var prevChild = prevChildren && prevChildren[name];
        var nextChild = nextChildren[name];
        //相同的话,说明是使用的同一个component,所以我们需要做移动的操作
        if (prevChild === nextChild) {
          //添加差异对象,类型:MOVE_EXISTING
          。。。。
          /**注意新增代码**/
          prevChild._mountIndex < lastIndex && diffQueue.push({
                parentId:this._rootNodeID,
                parentNode:$('[data-reactid='+this._rootNodeID+']'),
                type: UPATE_TYPES.REMOVE_NODE,
                fromIndex: prevChild._mountIndex,
                toIndex:null
          })
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        } else {
          //如果不相同,说明是新增加的节点,
          if (prevChild) {
            //但是如果老的还存在,就是element不同,但是component一样。我们需要把它对应的老的element删除。
            //添加差异对象,类型:REMOVE_NODE
            。。。。。
            /**注意新增代码**/
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          }
          。。。
        }
        //更新mount的inddex
        nextChild._mountIndex = nextIndex;
        nextIndex++;
      }

      //对于老的节点里有,新的节点里没有的那些,也全都删除掉
      。。。
}

可以看到我们多加了个lastIndex,这个代表最后一次访问的老集合节点的最大的位置。 而我们加了个判断,只有_mountIndex小于这个lastIndex的才会需要加入差异队列。有了这个判断上面的例子2就不需要move。而程序也可以好好的运行,实际上大部分都是2这种情况。

这是一种顺序优化,lastIndex一直在更新,代表了当前访问的最右的老的集合的元素。 我们假设上一个元素是A,添加后更新了lastIndex。 如果我们这时候来个新元素B,比lastIndex还大说明当前元素在老的集合里面就比上一个A靠后。所以这个元素就算不加入差异队列,也不会影响到其他人,不会影响到后面的path插入节点。因为我们从patch里面知道,新的集合都是按顺序从头开始插入元素的,只有当新元素比lastIndex小时才需要变更。其实只要仔细推敲下上面那个例子,就可以理解这种优化手段了。

这样整个的更新机制就完成了。我们再来简单回顾下reactjs的差异算法:

首先是所有的component都实现了receiveComponent来负责自己的更新,而浏览器默认元素的更新最为复杂,也就是经常说的 diff algorithm。

react有一个全局_shouldUpdateReactComponent用来根据element的key来判断是更新还是重新渲染,这是第一个差异判断。比如自定义元素里,就使用这个判断,通过这种标识判断,会变得特别高效。

每个类型的元素都要处理好自己的更新:

  1. 自定义元素的更新,主要是更新render出的节点,做甩手掌柜交给render出的节点的对应component去管理更新。

  2. text节点的更新很简单,直接更新文案。

  3. 浏览器基本元素的更新,分为两块:

    • 先是更新属性,对比出前后属性的不同,局部更新。并且处理特殊属性,比如事件绑定。
    • 然后是子节点的更新,子节点更新主要是找出差异对象,找差异对象的时候也会使用上面的_shouldUpdateReactComponent来判断,如果是可以直接更新的就会递归调用子节点的更新,这样也会递归查找差异对象,这里还会使用lastIndex这种做一种优化,使一些节点保留位置,之后根据差异对象操作dom元素(位置变动,删除,添加等)。

整个reactjs的差异算法就是这个样子。最核心的两个_shouldUpdateReactComponent以及diff,patch算法。

小试牛刀

有了上面简易版的reaactjs,我们来实现一个简单的todolist吧。


var TodoList = React.createClass({
  getInitialState: function() {
    return {items: []};
  },
  add:function(){
    var nextItems = this.state.items.concat([this.state.text]);
    this.setState({items: nextItems, text: ''});
  },
  onChange: function(e) {
    this.setState({text: e.target.value});
  },
  render: function() {
    var createItem = function(itemText) {
      return React.createElement("div", null, itemText);
    };

    var lists = this.state.items.map(createItem);
    var input = React.createElement("input", {onkeyup: this.onChange.bind(this),value: this.state.text});
    var button = React.createElement("p", {onclick: this.add.bind(this)}, 'Add#' + (this.state.items.length + 1))
    var children = lists.concat([input,button])

    return React.createElement("div", null,children);
  }
});


React.render(React.createElement(TodoList), document.getElementById("container"));

效果如下:

todolist

整个的流程是这样:

  • 初次渲染时先使用ReactCompositeComponent渲染自定义元素TodoList,调用getInitialState拿到初始值,然后使用ReactDOMComponent渲染render返回的div基本元素节点。div基本元素再一层层的使用ReactDOMComponent去渲染各个子节点,包括input,还有p。
  • 在input框输入文字触发onchange事件,开始调用setState做出变更,直接变更render出来的节点,经过差异算法,一层层的往下。最后改变value值。
  • 点击按钮,触发add然后开始更新,经过差异算法,添加一个节点。同时更新按钮上面的文案。

基本上,整个流程都梳理清楚了

结语

这只是个玩具,但实现了reactjs最核心的功能,虚拟节点,差异算法,单向数据更新都在这里了。还有很多reactjs优秀的东西没有实现,比如对象生成时内存的线程池管理,批量更新机制,事件的优化,服务端的渲染,immutable data等等。这些东西受限于篇幅就不具体展开了。

reactjs作为一种解决方案,虚拟节点的想法比较新奇,不过个人还是不能接受这种别扭的写法。使用reactjs,就要使用他那一整套的开发方式,而他核心的功能其实只是一个差异算法,而这种其实已经有相关的库实现了。

最后再吐槽下前端真是苦命,各种新技术,各种新知识脑细胞不够用了。也难怪前端永远都缺人。

2018-01-13 15:56:56 lacsis 阅读数 1477
  • 2019 react入门至高阶实战,含react hooks

    这是2019 react入门到高级新课程 学习react,不仅能带来技术提升,同时提高开发效率和体验,更能带来好的就业机会。 本课程主要分为以下几个部分:  一,前端工程化基础?  主要学习node和npm、yarn的基本用法  二,es6语法 学习必备的es6常用语法 。 三,react基础知识  学习如何搭建react项目,以及react组件,jsx语法、css处理方案、生命周期等基础知识。 并且根据这些知识开发一个个人网站。 四,react进阶知识?? 学习表单的处理,事件处理,Portals的使用,以及数据请求和API管理等进阶知识。 五,react高阶知识?? 学习react高级特性,react hooks,以及整个react生态体系的构成和应用 。 努力学习哟,带你精通react。

    1981 人正在学习 去看看 梁富城

本文也同时发表在博客HACKERNOON


界面更新本质上就是数据的变化。通过把所有会动的东西收敛到状态(state),React提供了一个非常直观的前端框架。我也比较喜欢review基于React代码,因为我一般都是从数据结构开始看,这样可以在钻到细节代码之前建立对整个逻辑的初步理解。我也经常会好奇React的实现方式,然后就有了这篇文章。

阅读更多




2017-01-12 15:52:19 luke_up 阅读数 2095
  • 2019 react入门至高阶实战,含react hooks

    这是2019 react入门到高级新课程 学习react,不仅能带来技术提升,同时提高开发效率和体验,更能带来好的就业机会。 本课程主要分为以下几个部分:  一,前端工程化基础?  主要学习node和npm、yarn的基本用法  二,es6语法 学习必备的es6常用语法 。 三,react基础知识  学习如何搭建react项目,以及react组件,jsx语法、css处理方案、生命周期等基础知识。 并且根据这些知识开发一个个人网站。 四,react进阶知识?? 学习表单的处理,事件处理,Portals的使用,以及数据请求和API管理等进阶知识。 五,react高阶知识?? 学习react高级特性,react hooks,以及整个react生态体系的构成和应用 。 努力学习哟,带你精通react。

    1981 人正在学习 去看看 梁富城

 某年某月某日,某师兄说:学一个东西,不能只停留在表面,只知道怎么用是完全不够的,

要清楚的明白,为什么这么做,为什么不那样做,还得从源码开始,虽然起步可能会比较坎坷,毕竟知识储备有限尴尬

点到为止了,所以我也就尝试去看了 redux 以及 react-redux 源码,确实坎坷惊恐

在此对看过的一些资料做一个总结。


先讲讲其基本用法,最后附上源码解析大笑

其实简单的应用,甚至只是一个单页应用,那完全是用不到 redux 的,只会让代码量提升了却没什么好处,出力不讨好。

redux 不仅仅只限于和 react 搭配使用,它可说是一个数据(state)管理器,也可在其他场景中使用。

当然 react-redux 就顾名思义不是适用于任何场景了。

如果用过 FLUX 的同学,应该会对 redux 的主要思想比较容易理解:

单项数据流,但存在与 FLUX比较大的差别是redux 整个应用只有一个数据源,也就是只有一个 store,在复杂的应用中也统一管理所有的数据。

我觉得 store 是整个 redux 的核心,最为核心的就是 store 的四个 function:

微笑dispatch:分发 Action 到对应的 Reducer后,根据 Reducer 逻辑更改store 中的 state,之后触发 subscribe的 listener,

微笑getState:获取当前store 中的 state 数据,

微笑subscribe: 注册 listener,在 state 变化时触发,

微笑replaceReducer:替换 Reducer,修改 state 变化逻辑,不是很常用。

我们需要做的是创建 store,Action,Reducer,最基本,最简单的写法:


//actionType
export const ACTION_TYPE = 'ACTION_TYPE';
//actionCreator
let actionCreator = (config) => {
    return {
        type: ACTION_TYPE, // 必须定义 type
        config // 可定义任何属性,都会传递到 reducer,用于修改 state
    }
}

action 其实就是一个普通对象,当然你也可以直接写这个对象,但存在诸多劣势。

import { ACTION_A, ACTION_B } from '../actions';
let initialState = { ... }
function example(state = initialState, action) {
    switch(action.type) {
        case ACTION_A:
          return Object.assign({}, state, action.config)
        case ACTION_B:
          return Object.assign({}, state, action.config)
	default:
	  return state
    }
}

reducer 里只做修改state,纯函数

import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux'
import thunk from 'redux-thunk';
import reducers from './reducers';
let store = createStore(reducers);
ReactDOM.render((
  <Provider store={store}>
   // ...
  </Provider>
), document.querySelector('#app'));

创建一个 store,也是整个应用唯一的 store。

Provider 是 react-redux 中提供的。可以通过Provider将 store 传递到包含在它之内的所有子组件里,但需配合 connect 使用。

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actionA } from 'actions';
class ComponentA extends Component {
    constructor(props) {
        super(props);
    }
    componentDidMount() {
        let { dispatch } = this.props;
        dispatch(actionA())
    }
    render() {
//由于所用到的 state 都绑定到了 props 中,component 中用到的时候可在 props 中获取
	<div>{this.props.propA}</div>
    }
}let mapStateToProps = (state) => { 
// attention,会把最新的 state 数据绑定到组件 props 中。 
//只需绑定和本组件相关的 state 即可(state 是包含了整个应用的所有数据的。
let { reducerA, reducerB } = state; 
    return { 
        propA: reducerA.propA, 
        propB: reducerB.propB 
    }
};
export default connect(mapStateToProps)(ComponentA);
connect 也是 react-redux 中提供的,这里简单的介绍下,最基础的用法就是只传mapStateToProps 方法,

返回的是一个包含了调用 getState,subscribe 的原组件(这里即 ComponentA)。

这样就完成一整套简单的 redux 融合的 react 组件应用了。

但这样的数据交互都是同步的,无法支持 ajax 的异步请求。

如果想实现就需要加入 thunk中间件去完成,同时需改造 action,不仅仅是一个简单的普通 Object 即可完成的了。

微笑另一种方法即在 component 中创建store,在 component 内就能直接调用 store.dispatch,store.subscribe,store.getState 等方法,完成数据管理。

但这种写法有个坑,即比如在 componentDidMount中注册了 subscribe 的 listener 时会返回一个 unsubscribe 方法,

用于解绑的,必须在 componentWillUnmount 的时候执行该方法,不然会报警告,在组件卸载时仍在监听。

componentDidMount(){
    self.unsubscribe = subscribe(()=>{xxx});
}
componentWillUnmount(){
    self.unsubscribe();
}

需要异步请求时,需要对 store 和 action 进行改造加强:

export default function configureStore(initialState) {
  const store = createStore(rootReducer,initialState,applyMiddleware(
    thunkMiddleware,//支持异步操作
    createLogger()//输出 redux 的action 和 state相关 log
  ))
  return store;
}
action 需改成根据异步请求不同状态发出不同的 action,包装成一个方法来完成
export function  fetchAction() {
    return dispatch => {
        dispatch({
            type: 'REQUEST_POSTS'
        })
        return fetch(`xxxxx`, {
            method: 'GET',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        })
            .then(response => response.json())
            .then(function (json) {
                if (json.success) {
                    dispatch({
                        type: CATEGORIES_RECEIVE_POSTS,
                        data: json
                    })
                } else {
                    info('数据获取失败,请稍候再试!')
                }
            })
            .catch(e => {
                debugger;
            })
    }
}
直白的理解就是发起请求时发出一个 action(不真实改变 state,只改变请求状态),

请求返回成功一个action,失败一个 action 让 reducer 做出不同的对 state 的修改,即可完成异步请求。


源码解析接下一篇博客哟~




2018-12-07 11:31:12 as645788 阅读数 374
  • 2019 react入门至高阶实战,含react hooks

    这是2019 react入门到高级新课程 学习react,不仅能带来技术提升,同时提高开发效率和体验,更能带来好的就业机会。 本课程主要分为以下几个部分:  一,前端工程化基础?  主要学习node和npm、yarn的基本用法  二,es6语法 学习必备的es6常用语法 。 三,react基础知识  学习如何搭建react项目,以及react组件,jsx语法、css处理方案、生命周期等基础知识。 并且根据这些知识开发一个个人网站。 四,react进阶知识?? 学习表单的处理,事件处理,Portals的使用,以及数据请求和API管理等进阶知识。 五,react高阶知识?? 学习react高级特性,react hooks,以及整个react生态体系的构成和应用 。 努力学习哟,带你精通react。

    1981 人正在学习 去看看 梁富城

最近双11双12各种需求交杂在一起,忙得不可开交,近期好不容易空了一些下来,读完了《深入浅出React技术栈》,这本书的内容和书名如出一辙,重点在于介绍使用React过程中相关的一些技术点,例如函数式编程、Redux、React核心的diff算法的思想等相关东西,东西还是蛮多的,适合想要一窥react技术栈全貌的同学,所以这次写一下自己读完这本书的思考和部分精华内容摘记。

this.setState

this.setState相信是大家在写React时写的最多的代码,但这里面到底都发生了什么?为什么setState可以是异步的?React是如何实现异步的呢?为什么不能在componentWillUpdateshouldComponetUpdate中调用setState。让我们来一探究竟。

在弄清楚setState之前,首先我们要知道的是React的生命周期,示意图如下:
生命周期

这里我们可以注意到,为什么在componentWillUpdateshouldComponentUpdate 中是没有见到setState的身影呢?

首先我们看下setState的源码:
setState源码

这其中该方法传入两个参数partialState是新的state值,callBack后者是回调函数,updater是在构造函数中定义的一个变量,从方法名enqueueSetState中我们可以明白,传入的新的state被enqueue推入了一个栈中,并不是立即更新,随后我们继续跟踪代码。
enqueueSetState

getInternalInstanceReadyForUpdate方法获取了当前组件对象,并将其赋给internalInstance。接下来判断当前组件对象的state是否存在更新队列,若存在则把新的state值push到队列中,若不存在,则创建一个空的新队列。
enquueUpdate
这里的代码也很好理解,首先ensureInjected方法检查当前运行的代码是否处在一个事务(reconcile transaction)中,若不是则会抛出错误。且若batchingStrategy.isBatchingUpdates为false(可以简单理解为当前不是在一个批处理流程中),则进行batchedUpdates(批量更新),若为true,则推入dirtyComponents中,接下来我们跟踪并看下batchingStrategy的源码。
batchedUpdates

至于为什么要做batchUpdates(批量更新),用React自己的话来说,是“为了避免组件被不必要地更新”,而且在这里我们可以看到,React更新组件是有一套自己的规则,通过组件的状态来执行,查阅资料后得知,React内部存在着"状态机"这个概念,也就是说当组件处于不同的状态时,所执行的逻辑是不同的。具体来说,React以事务+状态的方法来对组件进行更新,那么,到底什么是事务呢?

事务

下面这张图来源于React官方源码对事务的解释:
事务
从图上我们看到,其实Transaction事务说白了就是在不改变原有方法的基础上,在执行方法的前后进行额外的操作。具体来说,就是一个方法会被wrapper包裹,且方法需要通过perform来调用,且在被包裹方法的前后分别执行initializeclose。下面我们看代码,举例说明普通函数和被wrapper包裹的函数执行时有什么不同:

function test(){
    console.log('test')
};
transaction.perform(test);
//执行initialize方法
//输出'test'
//执行close方法

这里可能有的人会有疑问,为什么React要引入Transaction事务这个概念呢?其实Transaction事务这个概念来源于面向切面编程,举个简单例子,有时候我们在做真正的业务之前,经常需要进行验证,授权,或者输出日志的操作,也就是在主要的逻辑代码之前或者之后插入一些代码,但我们又不希望对原有的代码做侵入,这时候就是Transaction发挥作用的时刻了。
对于React来说,主要有以下几个应用场景(文字翻译自React源码):

  1. 在Reconciliation调和之前/之后保留输入选择范围。 即使出现意外错误也可以恢复这个选择。
  2. 在重新排列DOM时停用事件,同时确保事后事件能被重新激活。

说了这么多关于事务的事儿,接下来让我们看下ReactDefaultBatchingStrategy中的transaction是如何实现的
transaction

我们可以看到这里定义了2个wrapper,其中RESET_BATCHED_UPDATES负责在close阶段重置ReactDefaultBatchingStrategyisBatchingUpdatesfalse。而FLUSH_BATCHED_UPDATES负责在close执行flushBatchedUpdates,在这个方法里包含了Virtual DOM到真实DOM的映射等其他操作,且此方法会清空dirtyComponents数组并执行runBatchedUpdate方法
runBatchedUpdates

我们看到这里dirtyComponents数组会进行一个排序操作,这里因为通常情况下,父组件更新后,子组件也会随之更新,所以这里进先进行排序,使得子组件在父组件之前被更新,同时将setState中传入的回调函数存入callbackQueue队列中,且performUpdateIfNecessary方法中执行了updateComponent方法,让我们看看这个方法都做了什么。
updateComponents

接下来我们重点看下这个_processPendingState方法:
_processPendingState

这个函数对state的做法就比较简明扼要了,它主要做了以下几件事:

  1. 如果更新队列为null,那么返回原来的state
  2. 如果更新队列有一个更新值,那么返回更新值;
  3. 如果更新队列有多个更新,那么通过for循环将它们合并;

也就是说,在一个生命周期所有的state变化都会被合并,并统一处理。接下来我们看看performUpdate做了什么,这个函数的功能其实也简单,就是在更新组件前后分别执行componentWillUpdatecomponentDidUpdate。而在负责更新的_updateRenderedComponent函数中,我们根据传入的新旧组件信息判断是否进行更新。若返回值为true,执行旧组件的更新,否则的话执行旧组件的卸载和新组件的挂载。

整个流程图如下:
整体流程

看完了这个,那么对于开头的“为什么不能在componentWillUpdateshouldComponetUpdate中调用setState”问题我们就可以进行解释了

也就是说,组件更新时,state值还没有合并,则this._pendingStateQueuetrue,使得setState会再次调用updateComponent,随后继续调用componentWillUpdateshouldComponetUpdate方法,导致死循环,而正常情况下,已经更新过的组件不会进入再次更新的流程。
performUpdateIfNecessary

看完了这些,那么我们再看一道经典的关于setState的题目:

class Test extends Component {
  state = { val: 0 }

componentDidMount() {
    this.setState({ val: this.state.val + 1 }); 
    console.log(this.state.val); 
    
    this.setState({ val: this.state.val + 1 }); 
    console.log(this.state.val); 
    
    setTimeout(() => {
      this.setState({ val: this.state.val + 1 }); 
      console.log(this.state.val); 
      
      this.setState({ val: this.state.val + 1 }); 
      console.log(this.state.val); 
    }, 0);
  }

  render() {
    return null;
 }
}

这道题的输出是:

0
0
2
3

这里简单来说,前2个setState处于一个事务中,所以不会立即更新,而是做了合并,所以前2次log都是0,而当setTimeout被执行时,因为主线程执行完毕,已经完成了一次事务,此时是不会触发事务状态的,所以这时就是调用一次setState就更新一次状态。

这也就解释了为什么React文档中既没有说setState是同步更新或者是异步更新,它只是说setState并不保证同步更新。这里引用一下React的核心成员Dan Abramov的一个回答来继续做一点引申。
extra

谢谢大家。:)

引用

  1. React中的事务
  2. React技术内幕之setState的秘密
  3. React源码解析
  4. React之高阶组件
2019-04-07 03:55:24 weixin_34194087 阅读数 12
  • 2019 react入门至高阶实战,含react hooks

    这是2019 react入门到高级新课程 学习react,不仅能带来技术提升,同时提高开发效率和体验,更能带来好的就业机会。 本课程主要分为以下几个部分:  一,前端工程化基础?  主要学习node和npm、yarn的基本用法  二,es6语法 学习必备的es6常用语法 。 三,react基础知识  学习如何搭建react项目,以及react组件,jsx语法、css处理方案、生命周期等基础知识。 并且根据这些知识开发一个个人网站。 四,react进阶知识?? 学习表单的处理,事件处理,Portals的使用,以及数据请求和API管理等进阶知识。 五,react高阶知识?? 学习react高级特性,react hooks,以及整个react生态体系的构成和应用 。 努力学习哟,带你精通react。

    1981 人正在学习 去看看 梁富城

阅读源码成了今年的学习目标之一,在选择 Vue 和 React 之间,我想先阅读 React 。 在考虑到读哪个版本的时候,我想先接触到源码早期的思想可能会更轻松一些,最终我选择阅读 0.3-stable 。 那么接下来,我将从几个方面来解读这个版本的源码。

  1. React 源码学习(一):HTML 元素渲染
  2. React 源码学习(二):HTML 子元素渲染
  3. React 源码学习(三):CSS 样式及 DOM 属性
  4. React 源码学习(四):事务机制
  5. React 源码学习(五):事件机制
  6. React 源码学习(六):组件渲染
  7. React 源码学习(七):生命周期
  8. React 源码学习(八):组件更新

那么关于生命周期, React 当中生命周期有 2 个。

一个是组件的生命周期 _lifeCycleState ,另一个是复合生命周期 _compositeLifeCycleState 用于复合组件。

组件生命周期

那么关于组件的生命周期:

// core/ReactComponent.js
/**
 * Every React component is in one of these life cycles.
 */
var ComponentLifeCycle = keyMirror({
  /**
   * Mounted components have a DOM node representation and are capable of
   * receiving new props.
   */
  // 已挂载
  MOUNTED: null,
  /**
   * Unmounted components are inactive and cannot receive new props.
   */
  // 未挂载
  UNMOUNTED: null
});
复制代码

那么我们来观测到, ReactComponentReactCompositeComponent 关于 ComponentLifeCycle 的状态变化:

// core/ReactComponent.js
var ReactComponent = {
  Mixin: {
    getDOMNode: function() {
      // 获取 DOM 节点时,组件必须为已挂载
      invariant(
        this._lifeCycleState === ComponentLifeCycle.MOUNTED,
        'getDOMNode(): A component must be mounted to have a DOM node.'
      );
    },
    construct: function(initialProps, children) {
      // All components start unmounted.
      // 实例化时,组件为未挂载
      this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
    },
    mountComponent: function(rootID, transaction) {
      // 挂载组件前检查组件应为未挂载
      invariant(
        this._lifeCycleState === ComponentLifeCycle.UNMOUNTED,
        'mountComponent(%s, ...): Can only mount an unmounted component.',
        rootID
      );
      // 挂载完后更新组件生命周期状态
      this._lifeCycleState = ComponentLifeCycle.MOUNTED;
      // Effectively: return '';
    },
    unmountComponent: function() {
      // 卸载前检查组件应为已挂载
      invariant(
        this._lifeCycleState === ComponentLifeCycle.MOUNTED,
        'unmountComponent(): Can only unmount a mounted component.'
      );
      // 卸载完后更新组件生命周期状态
      this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
    },
    receiveProps: function(nextProps, transaction) {
      // 更新 props 时,组件生命周期应为已挂载
      invariant(
        this._lifeCycleState === ComponentLifeCycle.MOUNTED,
        'receiveProps(...): Can only update a mounted component.'
      );
    },
  }
};
复制代码
// core/ReactCompositeComponent.js
var ReactCompositeComponentMixin = {
  mountComponent: function(rootID, transaction) {
    ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);

    // Unset `this._lifeCycleState` until after this method is finished.
    this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED;
    // ...
    this._lifeCycleState = ReactComponent.LifeCycle.MOUNTED;
  },
  replaceState: function(completeState) {
    var compositeLifeCycleState = this._compositeLifeCycleState;
    // 更新 state 时,组件生命周期必须为已挂载,或者复合组件生命周期为挂载中
    invariant(
      this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
      compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
      'replaceState(...): Can only update a mounted (or mounting) component.'
    );
  },
  _bindAutoBindMethod: function(method) {
    function autoBound(a, b, c, d, e, tooMany) {
      // 使用绑定上下文的方法时,组件生命周期必须为已挂载
      if (component._lifeCycleState === ReactComponent.LifeCycle.MOUNTED) {
        return method.call(component, a, b, c, d, e);
      }
    }
  }
};
复制代码

复合组件生命周期

那么接下来,我们来看看复合生命周期以及其状态变化:

// core/ReactCompositeComponent.js
/**
 * `ReactCompositeComponent` maintains an auxiliary life cycle state in
 * `this._compositeLifeCycleState` (which can be null).
 *
 * This is different from the life cycle state maintained by `ReactComponent` in
 * `this._lifeCycleState`.
 */
var CompositeLifeCycle = keyMirror({
  /**
   * Components in the process of being mounted respond to state changes
   * differently.
   */
  // 挂载中
  MOUNTING: null,
  /**
   * Components in the process of being unmounted are guarded against state
   * changes.
   */
  // 卸载中
  UNMOUNTING: null,
  /**
   * Components that are mounted and receiving new props respond to state
   * changes differently.
   */
  // 更新 props
  RECEIVING_PROPS: null,
  /**
   * Components that are mounted and receiving new state are guarded against
   * additional state changes.
   */
  // 更新 state
  RECEIVING_STATE: null
});

var ReactCompositeComponentMixin = {
  construct: function(initialProps, children) {
    // 实例化时置空复合生命周期
    this._compositeLifeCycleState = null;
  },
  mountComponent: function(rootID, transaction) {
    // 挂载前设置复合生命周期为挂载中
    this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
    // 挂载完成后置空复合生命周期
    // Done with mounting, `setState` will now trigger UI changes.
    this._compositeLifeCycleState = null;
  },
  unmountComponent: function() {
    // 卸载开始时设置复合生命周期为卸载中
    this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;
    if (this.componentWillUnmount) {
      this.componentWillUnmount();
    }
    // 经过生命周期函数 componentWillUnmount 后,置空复合生命周期
    this._compositeLifeCycleState = null;
  },
  receiveProps: function(nextProps, transaction) {
    // 更新 props 时设置复合生命周期为更新 props
    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
    if (this.componentWillReceiveProps) {
      this.componentWillReceiveProps(nextProps, transaction);
    }
    // 执行生命周期函数 componentWillReceiveProps 后,设置复合生命周期为更新 state
    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
    // ...
    // 操作 state 更新相关后,置空复合生命周期
    this._compositeLifeCycleState = null;
  },
  replaceState: function(completeState) {
    // 更新 state
    var compositeLifeCycleState = this._compositeLifeCycleState;
    // 仅限生命周期为挂载中或者复合生命周期为挂载中可以更新 state
    invariant(
      this._lifeCycleState === ReactComponent.LifeCycle.MOUNTED ||
      compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
      'replaceState(...): Can only update a mounted (or mounting) component.'
    );
    // 仅限复合生命周期不为更新 state 或者不为卸载中
    invariant(
      compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
      compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
      'replaceState(...): Cannot update while unmounting component or during ' +
      'an existing state transition (such as within `render`).'
    );

    this._pendingState = completeState;

    // Do not trigger a state transition if we are in the middle of mounting or
    // receiving props because both of those will already be doing this.
    // 如果我们正在安装或接收道具,请不要触发状态转换,因为这两个道具都已经在进行此操作了。
    // 仅限复合生命周期不为挂载中 或者不为更新 props
    if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
        compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
      // 更新复合生命周期为更新 state
      this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

      // ... 执行更新相关操作
      // 置空复合生命周期
      this._compositeLifeCycleState = null;
    }
  },
};
复制代码

生命周期函数方法

那么到此,实现生命周期功能。那么让我们来看看那些生命周期的钩子都在哪里:

// core/ReactCompositeComponent.js
var ReactCompositeComponentInterface = {
  mixins: SpecPolicy.DEFINE_MANY,
  props: SpecPolicy.DEFINE_ONCE,
  getInitialState: SpecPolicy.DEFINE_ONCE,
  render: SpecPolicy.DEFINE_ONCE,
  // ==== Delegate methods ====
  // **一下内容为 Google 翻译**
  // 最初创建组件并即将安装时调用。 这可能有副作用,但必须在 `componentWillUnmount` 中清除此方法创建的任何外部订阅或数据。
  componentWillMount: SpecPolicy.DEFINE_MANY,
  // 在组件已装入并具有DOM表示形式时调用。 但是,无法保证DOM节点位于文档中。 在第一次装入(初始化和渲染)组件时,将此作为操作DOM的机会。
  componentDidMount: SpecPolicy.DEFINE_MANY,
  // 在组件接收新道具之前调用。 使用此作为通过使用 `this.setState` 更新状态来对prop转换作出反应的机会。 目前的道具是通过 `this.props` 访问的。
  // 注意:没有等效的 `componentWillReceiveState` 。传入的道具转换可能会导致状态改变,但情况恰恰相反。如果你需要它,你可能正在寻找 `componentWillUpdate` 。
  componentWillReceiveProps: SpecPolicy.DEFINE_MANY,
  // 在决定是否应该因接收新的道具和状态而更新组件时调用。 当您确定转换到新的道具和状态不需要更新组件时,可以将此作为 `return false` 的机会。
  shouldComponentUpdate: SpecPolicy.DEFINE_ONCE,
  // 由于从 `this.props` 和 `this.state` 转换为 `nextProps` 和 `nextState` 而导致组件即将更新时调用。使用此作为在更新发生之前执行准备的机会。
  // 注意:您**不能**在此方法中使用 `this.setState()`  。
  componentWillUpdate: SpecPolicy.DEFINE_MANY,
  // 更新组件的DOM表示时调用。 将此作为在更新组件时对DOM进行操作的机会。
  componentDidUpdate: SpecPolicy.DEFINE_MANY,
  // 当组件即将从其父组件中删除并销毁其DOM表示时调用。 使用此作为释放任何外部资源的机会。 注意:没有 `componentDidUnmount`  ,因为您的组件将被该点销毁。
  componentWillUnmount: SpecPolicy.DEFINE_MANY,
  // 到此
  updateComponent: SpecPolicy.OVERRIDE_BASE
};
复制代码

生命周期图

来看一下生命周期图:

没有更多推荐了,返回首页