React 全家桶之 react

目录

 

react 生命周期

零、编译阶段

一、初始化阶段

二、运行中的状态

文本节点的更新

原生节点的更新

自定义组件的更新

三、销毁阶段


react 生命周期

react的生命周期大致分为三个时期:初始化、运行中、销毁。

零、编译阶段

通常我们在写 react 时会采用 jsx 法语,经过 babel 编译之后实际上调用的是 createElement。

ReactDOM.render(
    <h1 style={{"color":"blue"}}>hello world</h1>,
    document.getElementById('root')
);

编译后会是这个样子 

ReactDOM.render(
    React.createElement(
        'h1',
        { style: { "color": "blue" } },
        'hello world'
    ),
    document.getElementById('root')
);

 

一、初始化阶段

getDefaultProps:获取实例的默认属性

getInitialState:获取每个实例的初始化状态

componentWillMound:组件即将被挂载、渲染到页面上

render:组件在这里生产虚拟Dom节点

componentDidMound:生成真实Dom,渲染到页面

总结:

  1. 初始化阶段根据默认的属性以及初始化状态,调用 createElement 生成 ReactElement ,也就是我们常说的虚拟 Dom。其实就是一颗抽象语法树。
  2. render 函数会根据上一步生成的 ReactElement 的 type 字段来判断它的类型,生成相应的 ReactComponent 实例。类型有文本、原生dom、自定义组件三种。
  3. 每一种 ReactComponent 都有 mountComponent 函数,按照自己的规则来生成 html 结构
  4. 子节点 children 会挂在 props 属性上,循环处理时会根据子节点的类型从第 2 步开始走,如此递归下去直到没有子节点为止。
  5. 将递归处理出来的 html 结构拼装起来,innerHTML 到 container 中去
  6. 到此完成初始化渲染,触发 componentDidMound
这个 ReactElement 的结构 
    //节点的类型,string代表原生节点,如果是一个class代表自定义组件
    type: type, 
    //节点的唯一标识,更新的时候会用的到
    key: key,
    //节点的引用,通常为父组件所用,如this.refs.child
    ref: ref,
    //节点的属性
    props: props,

    // 注意这个owner是创建ReactElement时,根据这个元素的类型所创建的ReactComponent
    // 而这个ReactComponent是ReactElement的控制类,控制节点的挂载、更新、卸载等操作,它两也是一一对应的
    _owner: owner

 

二、运行阶段

 

componentWillReceiveProps:组件将要接收到属性的时候

shouldComponentUpdate:组件接收到新的状态或者属性的时候(如果返回false,后续的render流程将不再执行)

componentWillUpdate:组件即将更新,不能在改函数中修改属性和状态

render:组件重新构建虚拟Dom

componentDidUpdate:组件完成更新

总结:

在初始化阶段所有类型的 Component 都实现了 mountComponent 来处理第一次渲染。同理,所有的 Component 都实现了 receiveComponent 来处理更新。

文本节点的更新

  • 判断新的文本内容与老的是否一样,不一样就直接替换
ReactDOMTextComponent.prototype.receiveComponent = function(nextText) {
    var nextStringText = '' + nextText;
    //跟以前保存的字符串比较
    if (nextStringText !== this._currentElement) {
        this._currentElement = nextStringText;
        //替换整个节点
        $('[data-reactid="' + this._rootNodeID + '"]').html(this._currentElement);

    }
}

原生节点的更新

原生节点的更新是比较复杂的,主要包括两个部分:

  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);
}

首先来看属性的更新

  1. 先遍历老集合,不在新集合里的属性,需要删除。具体就是清除监听事件以及dom上的属性
  2. 在遍历新集合,对于事件属性需要先清除再绑定。对于普通属性需要挂载到当前dom上,对于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])
    }

}

子节点的更新是最复杂的

  1. 首先,用一个 updateDepth 来记录当前节点的深度,用 diffQueue 更新队列来保存需要更新的内容
  2. 内部主要通过一个_diff 函数来找出差异,放入更新队列
  3. _diff 中会先调用 flattenChildren 把当前 children 数组转化成一个 map,如果子节点上设置了 key 就用 key,没设置就用当前位置 index 当做 key
  4. 老的 children map 转好了,再在 generateComponentChildren 中去转新的 children map。过程就是遍历新的 children,通过 key 去找老的 child,通过全局方法 _shouldUpdateReactComponent 判断是需要更新还是重新生成新的 component 实例。如果需要更新,就递归调用子节点的 receiveComponent。
  5. 现在新的 children map 也有了,先遍历新的 map,通过 key 去找到老的 child
    1. 如果老节点===新节点,代表同一个 component 实例,移动位置就可以了,push 到更新队列中
    2. 如果老节点存在但是!==新节点,代表 element 变了,老节点需要删除,push 到更新队列
    3. 将全新的节点 push 到更新队列中
  6.  再遍历老的 map,把老 map 中存在但是新 map 中不存在的节点删掉,push 到更新队列中
  7. 等到整个虚拟 Dom 数递归 receiveComponent 后,执行 patch 执行更新操作
  8. 在 patch 中会遍历更新队列,处理不同差异类型的更新,包括移动、删除、插入三种

 

//全局的更新深度标识
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 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++;
      }



  //对于老的节点里有,新的节点里没有的那些,也全都删除掉
  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);
      }
    }
  }
}

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

//用于将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;
        }
    }
}

 

自定义组件的更新

  1. 首先,receiveComponent 内部会将最新的 state 与老的 state 进行合并。
  2. 触发 shouldComponentUpdate,判断组件是否需要更新,需要的话继续往下走。
  3. 触发 componentWillUpdate,表示组件即将更新
  4. 然后拿这个最新的 state 和 props 生成一个虚拟 Dom,与原来的的虚拟 Dom 进行结构比较。
  5. 如果判断不需要更新,如 key 变了,或者类型都变了,直用最新的 state 和 props mount 出新的 html,替换掉老的节点
  6. 如果判断需要更新,继续递归调用子节点的 receiveComponent 
  7. 所有的子节点都处理完了,触发 componentDidUpdate,表示更新完成
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;
}

三、销毁阶段

componentWillUnmound:组件即将销毁

 

下图是react中方法调用的整个链路

上下文 Context

Context API 可以说是 React中最有趣的一个特性了。一方面很多流行的框架(例如 react-redux、 mobx-react、 react-router等)都在使用它;另一方面官方文档中却不推荐我们使用它。

一、场景

在 react 中,我们在传递消息时,通常的做法是父节点通过 props 属性,一层一层的传递到目标子节点,当子节点层级较深时,代码写起来就显得相当繁琐。这时 context 就派上用场了,只要在最外层组件上将数据塞进 context,然后在任意层级的子节点都可以获取到,省去了所有不必要的的中间传递过程。

二、用法

老版本的用法

class Parent extends React.Component {
    getChildContext () {
        return { name: '张三' }
    }
}
Parent.childContextTypes = {
    name: ProtoTypes.string
}

然后在任意层级的子组件的即可使用

class Child extends React.Component {

    render () {
        return (
            <div>{ this.context.name }</div>
        )
    }
}
Child.contextTypes = {
    name: ProtoTypes.string
}

缺点:当中间节点在 shouldComponentUpdate 中 return false 的话,后续的子节点将不再更新,也就接收不到 context 中传递的消息了

新版本的用法

React 在版本 16.3-alpha 里引入了新的 Context API,主要由以下几部分组成:

  • React.createContext 方法用于创建一个 Context 对象,改对象包含 Provider 和 Consumer 两个属性,均为 React 组件
  • Provider 组件用在组件树的最外层,它接收一个属性 value,值可以是任意 js 类型
  • Consumer 用在 Provider 内部任意层级,它接收一个属性 children,值是一个函数,该函数的入参是第一步创建的 Context 对象,返回值是一个 React 元素
// createContext.js
export default React.createContext({
    name: '张三'
})

// parent.js
import mycontext from './createContext';
class Parent extends React.Component {
    render () {
        return (
            <mycontext.Provider value={{ name: '李四' }}>
                <child></child>
            </mycontext.Provider>
        )
    }
}
// child.js
import mycontext from './createContext';
class Child extends React.Component {
    render () {
        return (
            <mycontext.Consumer>
            {
                context => {
                    return <div>{ context.name }</div>
                }
            }
            </mycontext.Consumer>
        )
    }    
}

这里需要注意几点:

Provider 和 Consumer 必须来自同一次 createContext 调用

createContext 会接收一个默认的值做为参数,当 Consumer 外层没有 Provider 时就会使用该默认值

当 Provider 中的 value 变化时,Consumer 组件会接收到新值并触发 rerender,此过程不受 shouldComponentUpate 影响

Provider 组件利用 Object.js 检测 value 值是否有更新,注意 Object.js 和 === 并不完全相同

参考:

https://www.cnblogs.com/enoy/articles/react.html

https://segmentfault.com/a/1190000021178528?utm_source=tag-newest 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值