React.js核心原理实现:更新机制

前端之家收集整理的这篇文章主要介绍了React.js核心原理实现:更新机制前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

一、前言

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

二、实现更新机制

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

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

  1. var HelloMessage = React.createClass({
  2. getInitialState: function() {
  3. return {type: 'say:'};
  4. },changeType:function(){
  5. this.setState({type:'shout:'})
  6. },render: function() {
  7. return React.createElement("div",{onclick:this.changeType},this.state.type,"Hello ",this.props.name);
  8. }
  9. });
  10.  
  11.  
  12. React.render(React.createElement(HelloMessage,{name: "John"}),document.getElementById("container"));
  13.  
  14.  
  15.  
  16. /**
  17.  
  18. //生成的html为:
  19.  
  20. <div data-reactid="0" id="test">
  21. <span data-reactid="0.0">hello world</span>
  22. </div>
  23.  
  24. 点击文字,say会变成shout
  25.  
  26. */

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

  1. //定义ReactClass类
  2. var ReactClass = function(){
  3. }
  4.  
  5. ReactClass.prototype.render = function(){}
  6.  
  7. //setState
  8. ReactClass.prototype.setState = function(newState) {
  9.  
  10. //还记得我们在ReactCompositeComponent里面mount的时候 做了赋值
  11. //所以这里可以拿到 对应的ReactCompositeComponent的实例_reactInternalInstance
  12. this._reactInternalInstance.receiveComponent(null,newState);
  13. }

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

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

1、自定义元素的receiveComponent

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

  1. //更新
  2. ReactCompositeComponent.prototype.receiveComponent = function(nextElement,newState) {
  3.  
  4. //如果接受了新的,就使用最新的element
  5. this._currentElement = nextElement || this._currentElement
  6.  
  7. var inst = this._instance;
  8. //合并state
  9. var nextState = $.extend(inst.state,newState);
  10. var nextProps = this._currentElement.props;
  11.  
  12.  
  13. //改写state
  14. inst.state = nextState;
  15.  
  16.  
  17. //如果inst有shouldComponentUpdate并且返回false。说明组件本身判断不要更新,就直接返回。
  18. if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps,nextState) === false)) return;
  19.  
  20. //生命周期管理,如果有componentWillUpdate,就调用,表示开始要更新了。
  21. if (inst.componentWillUpdate) inst.componentWillUpdate(nextProps,nextState);
  22.  
  23.  
  24. var prevComponentInstance = this._renderedComponent;
  25. var prevRenderedElement = prevComponentInstance._currentElement;
  26. //重新执行render拿到对应的新element;
  27. var nextRenderedElement = this._instance.render();
  28.  
  29.  
  30. //判断是需要更新还是直接就重新渲染
  31. //注意这里的_shouldUpdateReactComponent跟上面的不同哦 这个是全局的方法
  32. if (_shouldUpdateReactComponent(prevRenderedElement,nextRenderedElement)) {
  33. //如果需要更新,就继续调用子节点的receiveComponent的方法,传入新的element更新子节点。
  34. prevComponentInstance.receiveComponent(nextRenderedElement);
  35. //调用componentDidUpdate表示更新完成了
  36. inst.componentDidUpdate && inst.componentDidUpdate();
  37.  
  38. } else {
  39. //如果发现完全是不同的两种element,那就干脆重新渲染了
  40. var thisID = this._rootNodeID;
  41. //重新new一个对应的component,
  42. this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
  43. //重新生成对应的元素内容
  44. var nextMarkup = _renderedComponent.mountComponent(thisID);
  45. //替换整个节点
  46. $('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup);
  47.  
  48. }
  49.  
  50. }
  51.  
  52. //用来判定两个element需不需要更新
  53. //这里的key是我们createElement的时候可以选择性的传入的。用来标识这个element,当发现key不同时,我们就可以直接重新渲染,不需要去更新了。
  54. var _shouldUpdateReactComponent function(prevElement,nextElement){
  55. if (prevElement != null && nextElement != null) {
  56. var prevType = typeof prevElement;
  57. var nextType = typeof nextElement;
  58. if (prevType === 'string' || prevType === 'number') {
  59. return nextType === 'string' || nextType === 'number';
  60. } else {
  61. return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
  62. }
  63. }
  64. return false;
  65. }

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

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

这里注意两个函数

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

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

2、文本节点的receiveComponent

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

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

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

3、基本元素element的receiveComponent

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

  1. <div id="test" name="hello">
  2. <span></span>
  3. <span></span>
  4. </div>

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

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

所以更新代码结构如下:

  1. ReactDOMComponent.prototype.receiveComponent = function(nextElement) {
  2. var lastProps = this._currentElement.props;
  3. var nextProps = nextElement.props;
  4.  
  5. this._currentElement = nextElement;
  6. //需要单独的更新属性
  7. this._updateDOMProperties(lastProps,nextProps);
  8. //再更新子节点
  9. this._updateDOMChildren(nextElement.props.children);
  10. }

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

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

  1. ReactDOMComponent.prototype._updateDOMProperties = function(lastProps,nextProps) {
  2. var propKey;
  3. //遍历,当一个老的属性不在新的属性集合里时,需要删除掉。
  4.  
  5. for (propKey in lastProps) {
  6. //新的属性里有,或者propKey是在原型上的直接跳过。这样剩下的都是不在新属性集合里的。需要删除
  7. if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
  8. continue;
  9. }
  10. //对于那种特殊的,比如这里的事件监听的属性我们需要去掉监听
  11. if (/^on[A-Za-z]/.test(propKey)) {
  12. var eventType = propKey.replace('on','');
  13. //针对当前的节点取消事件代理
  14. $(document).undelegate('[data-reactid="' + this._rootNodeID + '"]',eventType,lastProps[propKey]);
  15. continue;
  16. }
  17.  
  18. //从dom上删除不需要的属性
  19. $('[data-reactid="' + this._rootNodeID + '"]').removeAttr(propKey)
  20. }
  21.  
  22. //对于新的属性,需要写到dom节点上
  23. for (propKey in nextProps) {
  24. //对于事件监听的属性我们需要特殊处理
  25. if (/^on[A-Za-z]/.test(propKey)) {
  26. var eventType = propKey.replace('on','');
  27. //以前如果已经有,说明有了监听,需要先去掉
  28. lastProps[propKey] && $(document).undelegate('[data-reactid="' + this._rootNodeID + '"]',lastProps[propKey]);
  29. //针对当前的节点添加事件代理,以_rootNodeID为命名空间
  30. $(document).delegate('[data-reactid="' + this._rootNodeID + '"]',eventType + '.' + this._rootNodeID,nextProps[propKey]);
  31. continue;
  32. }
  33.  
  34. if (propKey == 'children') continue;
  35.  
  36. //添加新的属性,或者是更新老的同名属性
  37. $('[data-reactid="' + this._rootNodeID + '"]').prop(propKey,nextProps[propKey])
  38. }
  39.  
  40. }

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

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

  1. ReactDOMComponent.prototype.receiveComponent = function(nextElement){
  2. var lastProps = this._currentElement.props;
  3. var nextProps = nextElement.props;
  4.  
  5. this._currentElement = nextElement;
  6. //需要单独的更新属性
  7. this._updateDOMProperties(lastProps,nextProps);
  8. //再更新子节点
  9. this._updateDOMChildren(nextProps.children);
  10. }
  11.  
  12. //全局的更新深度标识
  13. var updateDepth = 0;
  14. //全局的更新队列,所有的差异都存在这里
  15. var diffQueue = [];
  16.  
  17. ReactDOMComponent.prototype._updateDOMChildren = function(nextChildrenElements){
  18. updateDepth++
  19. //_diff用来递归找出差别,组装差异对象,添加到更新队列diffQueue。
  20. this._diff(diffQueue,nextChildrenElements);
  21. updateDepth--
  22. if(updateDepth == 0){
  23. //在需要的时候调用patch,执行具体的dom操作
  24. this._patch(diffQueue);
  25. diffQueue = [];
  26. }
  27. }

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

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

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

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

我们先看_diff的实现:

  1. //差异更新的几种类型
  2. var UPATE_TYPES = {
  3. MOVE_EXISTING: 1,REMOVE_NODE: 2,INSERT_MARKUP: 3
  4. }
  5.  
  6.  
  7. //普通的children是一个数组,此方法把它转换成一个map,key就是element的key,如果是text节点或者element创建时并没有传入key,就直接用在数组里的index标识
  8. function flattenChildren(componentChildren) {
  9. var child;
  10. var name;
  11. var childrenMap = {};
  12. for (var i = 0; i < componentChildren.length; i++) {
  13. child = componentChildren[i];
  14. name = child && child._currentelement && child._currentelement.key ? child._currentelement.key : i.toString(36);
  15. childrenMap[name] = child;
  16. }
  17. return childrenMap;
  18. }
  19.  
  20.  
  21. //主要用来生成子节点elements的component集合
  22. //这边注意,有个判断逻辑,如果发现是更新,就会继续使用以前的componentInstance,调用对应的receiveComponent。
  23. //如果是新的节点,就会重新生成一个新的componentInstance,
  24. function generateComponentChildren(prevChildren,nextChildrenElements) {
  25. var nextChildren = {};
  26. nextChildrenElements = nextChildrenElements || [];
  27. $.each(nextChildrenElements,function(index,element) {
  28. var name = element.key ? element.key : index;
  29. var prevChild = prevChildren && prevChildren[name];
  30. var prevElement = prevChild && prevChild._currentElement;
  31. var nextElement = element;
  32.  
  33. //调用_shouldUpdateReactComponent判断是否是更新
  34. if (_shouldUpdateReactComponent(prevElement,nextElement)) {
  35. //更新的话直接递归调用子节点的receiveComponent就好了
  36. prevChild.receiveComponent(nextElement);
  37. //然后继续使用老的component
  38. nextChildren[name] = prevChild;
  39. } else {
  40. //对于没有老的,那就重新新增一个,重新生成一个component
  41. var nextChildInstance = instantiateReactComponent(nextElement,null);
  42. //使用新的component
  43. nextChildren[name] = nextChildInstance;
  44. }
  45. })
  46.  
  47. return nextChildren;
  48. }
  49.  
  50.  
  51.  
  52. //_diff用来递归找出差别,添加到更新队列diffQueue。
  53. ReactDOMComponent.prototype._diff = function(diffQueue,nextChildrenElements) {
  54. var self = this;
  55. //拿到之前的子节点的 component类型对象的集合,这个是在刚开始渲染时赋值的,记不得的可以翻上面
  56. //_renderedChildren 本来是数组,我们搞成map
  57. var prevChildren = flattenChildren(self._renderedChildren);
  58. //生成新的子节点的component对象集合,这里注意,会复用老的component对象
  59. var nextChildren = generateComponentChildren(prevChildren,nextChildrenElements);
  60. //重新赋值_renderedChildren,使用最新的。
  61. self._renderedChildren = []
  62. $.each(nextChildren,function(key,instance) {
  63. self._renderedChildren.push(instance);
  64. })
  65.  
  66.  
  67. var nextIndex = 0; //代表到达的新的节点的index
  68. //通过对比两个集合的差异,组装差异节点添加到队列中
  69. for (name in nextChildren) {
  70. if (!nextChildren.hasOwnProperty(name)) {
  71. continue;
  72. }
  73. var prevChild = prevChildren && prevChildren[name];
  74. var nextChild = nextChildren[name];
  75. //相同的话,说明是使用的同一个component,所以我们需要做移动的操作
  76. if (prevChild === nextChild) {
  77. //添加差异对象,类型:MOVE_EXISTING
  78. diffQueue.push({
  79. parentId: self._rootNodeID,parentNode: $('[data-reactid=' + self._rootNodeID + ']'),type: UPATE_TYPES.MOVE_EXISTING,fromIndex: prevChild._mountIndex,toIndex: nextIndex
  80. })
  81. } else { //如果不相同,说明是新增加的节点
  82. //但是如果老的还存在,就是element不同,但是component一样。我们需要把它对应的老的element删除
  83. if (prevChild) {
  84. //添加差异对象,类型:REMOVE_NODE
  85. diffQueue.push({
  86. parentId: self._rootNodeID,type: UPATE_TYPES.REMOVE_NODE,toIndex: null
  87. })
  88.  
  89. //如果以前已经渲染过了,记得先去掉以前所有的事件监听,通过命名空间全部清空
  90. if (prevChild._rootNodeID) {
  91. $(document).undelegate('.' + prevChild._rootNodeID);
  92. }
  93.  
  94. }
  95. //新增加的节点,也组装差异对象放到队列里
  96. //添加差异对象,类型:INSERT_MARKUP
  97. diffQueue.push({
  98. parentId: self._rootNodeID,type: UPATE_TYPES.INSERT_MARKUP,fromIndex: null,toIndex: nextIndex,markup: nextChild.mountComponent() //新增的节点,多一个此属性,表示新节点的dom内容
  99. })
  100. }
  101. //更新mount的index
  102. nextChild._mountIndex = nextIndex;
  103. nextIndex++;
  104. }
  105.  
  106.  
  107.  
  108. //对于老的节点里有,新的节点里没有的那些,也全都删除
  109. for (name in prevChildren) {
  110. if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
  111. //添加差异对象,类型:REMOVE_NODE
  112. diffQueue.push({
  113. parentId: self._rootNodeID,toIndex: null
  114. })
  115. //如果以前已经渲染过了,记得先去掉以前所有的事件监听
  116. if (prevChildren[name]._rootNodeID) {
  117. $(document).undelegate('.' + prevChildren[name]._rootNodeID);
  118. }
  119. }
  120. }
  121. }

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

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

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

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

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

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

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

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

我们看下_patch的实现:

  1. //用于将childNode插入到指定位置
  2. function insertChildAt(parentNode,childNode,index) {
  3. var beforeChild = parentNode.children().get(index);
  4. beforeChild ? childNode.insertBefore(beforeChild) : childNode.appendTo(parentNode);
  5. }
  6.  
  7. ReactDOMComponent.prototype._patch = function(updates) {
  8. var update;
  9. var initialChildren = {};
  10. var deleteChildren = [];
  11. for (var i = 0; i < updates.length; i++) {
  12. update = updates[i];
  13. if (update.type === UPATE_TYPES.MOVE_EXISTING || update.type === UPATE_TYPES.REMOVE_NODE) {
  14. var updatedIndex = update.fromIndex;
  15. var updatedChild = $(update.parentNode.children().get(updatedIndex));
  16. var parentID = update.parentID;
  17.  
  18. //所有需要更新的节点都保存下来,方便后面使用
  19. initialChildren[parentID] = initialChildren[parentID] || [];
  20. //使用parentID作为简易命名空间
  21. initialChildren[parentID][updatedIndex] = updatedChild;
  22.  
  23.  
  24. //所有需要修改的节点先删除,对于move的,后面再重新插入到正确的位置即可
  25. deleteChildren.push(updatedChild)
  26. }
  27.  
  28. }
  29.  
  30. //删除所有需要先删除
  31. $.each(deleteChildren,child) {
  32. $(child).remove();
  33. })
  34.  
  35.  
  36. //再遍历一次,这次处理新增的节点,还有修改的节点这里也要重新插入
  37. for (var k = 0; k < updates.length; k++) {
  38. update = updates[k];
  39. switch (update.type) {
  40. case UPATE_TYPES.INSERT_MARKUP:
  41. insertChildAt(update.parentNode,$(update.markup),update.toIndex);
  42. break;
  43. case UPATE_TYPES.MOVE_EXISTING:
  44. insertChildAt(update.parentNode,initialChildren[update.parentID][update.fromIndex],update.toIndex);
  45. break;
  46. case UPATE_TYPES.REMOVE_NODE:
  47. // 什么都不需要做,因为上面已经帮忙删除掉了
  48. break;
  49. }
  50. }
  51. }

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

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

我们来改造下代码

  1. //_diff用来递归找出差别,nextChildrenElements){
  2. 。。。
  3. /**注意新增代码**/
  4. var lastIndex = 0;//代表访问的最后一次的老的集合的位置
  5. var nextIndex = 0;//代表到达的新的节点的index
  6. //通过对比两个集合的差异,组装差异节点添加到队列中
  7. for (name in nextChildren) {
  8. if (!nextChildren.hasOwnProperty(name)) {
  9. continue;
  10. }
  11. var prevChild = prevChildren && prevChildren[name];
  12. var nextChild = nextChildren[name];
  13. //相同的话,说明是使用的同一个component,所以我们需要做移动的操作
  14. if (prevChild === nextChild) {
  15. //添加差异对象,类型:MOVE_EXISTING
  16. 。。。。
  17. /**注意新增代码**/
  18. prevChild._mountIndex < lastIndex && diffQueue.push({
  19. parentId:this._rootNodeID,parentNode:$('[data-reactid='+this._rootNodeID+']'),toIndex:null
  20. })
  21. lastIndex = Math.max(prevChild._mountIndex,lastIndex);
  22. } else {
  23. //如果不相同,说明是新增加的节点,
  24. if (prevChild) {
  25. //但是如果老的还存在,就是element不同,但是component一样。我们需要把它对应的老的element删除
  26. //添加差异对象,类型:REMOVE_NODE
  27. 。。。。。
  28. /**注意新增代码**/
  29. lastIndex = Math.max(prevChild._mountIndex,lastIndex);
  30. }
  31. 。。。
  32. }
  33. //更新mount的inddex
  34. nextChild._mountIndex = nextIndex;
  35. nextIndex++;
  36. }
  37.  
  38. //对于老的节点里有,新的节点里没有的那些,也全都删除
  39. 。。。
  40. }

可以看到我们多加了个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吧。

  1. var TodoList = React.createClass({
  2. getInitialState: function() {
  3. return {items: []};
  4. },add:function(){
  5. var nextItems = this.state.items.concat([this.state.text]);
  6. this.setState({items: nextItems,text: ''});
  7. },onChange: function(e) {
  8. this.setState({text: e.target.value});
  9. },render: function() {
  10. var createItem = function(itemText) {
  11. return React.createElement("div",null,itemText);
  12. };
  13.  
  14. var lists = this.state.items.map(createItem);
  15. var input = React.createElement("input",{onkeyup: this.onChange.bind(this),value: this.state.text});
  16. var button = React.createElement("p",{onclick: this.add.bind(this)},'Add#' + (this.state.items.length + 1))
  17. var children = lists.concat([input,button])
  18.  
  19. return React.createElement("div",children);
  20. }
  21. });
  22.  
  23.  
  24. React.render(React.createElement(TodoList),document.getElementById("container"));

效果如下:

整个的流程是这样:

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

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

四、结语

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

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

猜你在找的React相关文章