目录
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,渲染到页面
总结:
- 初始化阶段根据默认的属性以及初始化状态,调用 createElement 生成 ReactElement ,也就是我们常说的虚拟 Dom。其实就是一颗抽象语法树。
- render 函数会根据上一步生成的 ReactElement 的 type 字段来判断它的类型,生成相应的 ReactComponent 实例。类型有文本、原生dom、自定义组件三种。
- 每一种 ReactComponent 都有 mountComponent 函数,按照自己的规则来生成 html 结构
- 子节点 children 会挂在 props 属性上,循环处理时会根据子节点的类型从第 2 步开始走,如此递归下去直到没有子节点为止。
- 将递归处理出来的 html 结构拼装起来,innerHTML 到 container 中去
- 到此完成初始化渲染,触发 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);
}
}
原生节点的更新
原生节点的更新是比较复杂的,主要包括两个部分:
- 属性的更新,包括对特殊属性比如事件的处理
- 子节点的更新,拿新节点和老节点对比,找出差异,称之为 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);
}
首先来看属性的更新
- 先遍历老集合,不在新集合里的属性,需要删除。具体就是清除监听事件以及dom上的属性
- 在遍历新集合,对于事件属性需要先清除再绑定。对于普通属性需要挂载到当前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])
}
}
子节点的更新是最复杂的
- 首先,用一个 updateDepth 来记录当前节点的深度,用 diffQueue 更新队列来保存需要更新的内容
- 内部主要通过一个_diff 函数来找出差异,放入更新队列
- _diff 中会先调用 flattenChildren 把当前 children 数组转化成一个 map,如果子节点上设置了 key 就用 key,没设置就用当前位置 index 当做 key
- 老的 children map 转好了,再在 generateComponentChildren 中去转新的 children map。过程就是遍历新的 children,通过 key 去找老的 child,通过全局方法 _shouldUpdateReactComponent 判断是需要更新还是重新生成新的 component 实例。如果需要更新,就递归调用子节点的 receiveComponent。
- 现在新的 children map 也有了,先遍历新的 map,通过 key 去找到老的 child
- 如果老节点===新节点,代表同一个 component 实例,移动位置就可以了,push 到更新队列中
- 如果老节点存在但是!==新节点,代表 element 变了,老节点需要删除,push 到更新队列
- 将全新的节点 push 到更新队列中
- 再遍历老的 map,把老 map 中存在但是新 map 中不存在的节点删掉,push 到更新队列中
- 等到整个虚拟 Dom 数递归 receiveComponent 后,执行 patch 执行更新操作
- 在 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;
}
}
}
自定义组件的更新
- 首先,receiveComponent 内部会将最新的 state 与老的 state 进行合并。
- 触发 shouldComponentUpdate,判断组件是否需要更新,需要的话继续往下走。
- 触发 componentWillUpdate,表示组件即将更新
- 然后拿这个最新的 state 和 props 生成一个虚拟 Dom,与原来的的虚拟 Dom 进行结构比较。
- 如果判断不需要更新,如 key 变了,或者类型都变了,直用最新的 state 和 props mount 出新的 html,替换掉老的节点
- 如果判断需要更新,继续递归调用子节点的 receiveComponent
- 所有的子节点都处理完了,触发 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