Open buxuku opened 3 years ago
在Component
父类的forceUpdate
里面的render
也需要用wrapToVdom
来处理一下.
import {compareTwoVdoms} from '../react-dom';
+import {wrapToVdom} from '../utils';
import {Updater} from "./Updater";
/**
* React.Component父类
*/
export class Component {
static isReactComponent = {};
constructor(props) {
this.props = props;
this.state = {};
this.updater = new Updater(this)
}
setState(partialState) {
this.updater.addState(partialState)
}
forceUpdate() {
const oldVdom = this.oldVdom;
- const newVdom = this.render();
+ const newVdom = wrapToVdom(this.render());
compareTwoVdoms(oldVdom, newVdom)
this.oldVdom = newVdom; // 将更新后的虚拟DOM更新到原来的oldVdom上面
}
}
在第一节的
createElement
里面,返回的对象,如果节点是文本类型的,其children
值直接就是一个字符串或者数字.就好比下面这样.在后面的
createDom
里面,我们也通过if (typeof vdom !== 'object')
来判断一个节点是否是文本类型的节点.因为它不是一个对象,这样有一个小麻烦就是,我们没办法在最后把真实的Dom节点挂载到这个虚拟Dom上面.所以在接下来做dom-diff
时,想对真实Dom进行增删改查操作时,对于文本类型的节点就比较痛苦了.所以在这之前,我们先做一个小的HACK,把文本类型的节点也返回一个对象形式的虚拟Dom.新建
src/constant/index.js
文件,定义一个文本类型的节点,顺便把之前的forward
类型也统一进来.修改
src/react/index.js
文件里面的createElement
方法新建
src/utils/index.js
文件,实现wrapToVdom
方法同时修改
src/react-dom/index.js
文件里面的createDom
方法以及
mountClassElement
也用wrapToVdom
进行包裹一下经过这样改造之后,我们就能也能正常渲染文本类型的节点了.
设计动机
从官方的文档设计动机,它提到了两点:
key
属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;官方认为,以上假设几乎在所有的场景都成立,也就是说对于第一点,如果元素的类型变了,或者说元素的层级变了,都将直接渲染整个Dom树,而对于对个子元素,我们可以通过
key
来识别是否有变更,包括位置的移动.在我们之前的更新方法里面,我们用的全量替换,基实就是针对第1种情况才使用的.而对于其它情况,我们继续顺着来梳理.修改
src/react-dom/index.js
里面的compareTwoVdom
方法.在这个方法里面,如果节点类型变了,就直接进行全量替换,否则,走更新的逻辑.
文本节点的更新
文本节点的更新相对比较简单,因为不涉及到元素的
attribute
属性的更新,直接判断一下文本内容有没变更,有变更就进行替换操作即可.在
src/react-demo/index.js
里继续实现updateElement
方法原生HTML标签类型更新
对于原生的HTML标签,如果标签类型没有变,就直接利用原来老的节点,并更新
attribute
属性,以及遍历子元素的变更.更新
attribute
属性要考虑的主要就是属性的增删改行为,对属性的修改也是遵循只修改变动的部分.所以需要对新旧的props进行对比.元素属性的更新
在
updateElement
增加一个原生标签类型的判断对原来的
renderAttributes
方法进行改造,主要就是增加属性删除的判断,以及新老属性值是否变化的判断.子节点的更新
对于子节点的对比,这里先采用一个简单粗暴的方法,假设新旧元素都处在相同位置上,就是新旧的进行一一对比,暂不考虑元素移动的问题.
在
src/react-dom/index.js
实现一个updateChildren
方法在
updateElement
中来调用这个方法在
src/index.js
中写一个组件来模拟元素属性的增删改操作,包括布尔类型属性的变更.可以看到,现在Dom节点的更新只更新了变动的部分.
元素的挂载与卸载
对于如下一个render方法里面
当
number
不管等于1,它都将有4个子元素,比如等于1的时候.它的子元素如下:所以对于这样的子元素的挂载与卸载是可以通过前后对比判断出来的.这里注意到如果子元素如果是布尔值,
null
,undefined
类型的,不进行真实Dom的渲染的.在
src/utils/index.js
增加一个判断方法然后调整一下
src/react-dom/index.js
在
compareTwoVdom
方法里面,增加对新旧节点挂载与卸载的判断,这里需要注意的地方就是,如果老节点没有,新节点有,那么这个新节点有可能是在当前位置进行插入,也有可能是插入到最后一个元素,判断的依据就是看旧的Dom树后面还有没有子元素.同时compareTwoVdom
需要接收一个父元素的参数,在保证在oldVdom
不存在的情况下通过父元素来完成插入操作.将
src/index.js
里面的render
方法修改如下,来模拟一下组件的挂载与卸载操作.类组件的更新
前面我们已经实现了类组件
state
的变更更新组件,这里的类组件更新处理的是父组件的更新,导致了类组件类型的子元素的更新.类组件的更新,就是传入新的props,再次调用实例上面的render
方法,重新新的虚拟Dom,然后用新的虚拟Dom和老的虚拟Dom继续进行对比更新即可.在
updateElement
方法里面增加一个类组件的判断继续在
src/react-dom/index.js
里面实现updateClassComponent
这个方法在这个方法里面,我们要调用实例上面的更新器,去触发这一次更新,并把最新的props传递过去.为了能获取到组件的实例,我们在原来的
mountClassComponent
方法里面把实例挂到虚拟Dom上面在
src/react/Updater.js
里面,增加一个emitUpdate
这个方法,来授受state
和props
变更进行更新的触发操作.好,写到这里,终于出现了一个小bug,在原来的
updateTracker.isBatchingUpdate = false;
这个操作是放在batchUpdate
这个方法里面的,这就会导致一个问题,父组件一state
变更了,触发更新操作,这个isBatchingUpdate
会直到父组件更新完毕了才会重置会false
,所以就会导致父组件里面的类组件类型的子组件无法进行更新.所以需要把这一句重置放到事件执行完毕之后.修改
src/react/Updater.js
移动到
src/react/event.js
里面这里还注意到有一个问题,咱们之前的state触发类组件的更新,在
forceUpdate
里面是通过this.oldVdom
来获取之前render出来的旧的虚拟Dom,通过它是可以递归查到的真实的Dom的,而在updateChildren
里面有这一么句const nextDom = oldChildren.find((item, index) => index > i && item && findDom(item));
,这里面的findDom(item)
传入的item
如果刚好是一个类组件的虚拟Dom的话,那么咱们之前的findDom
就查不到真实的Dom了.因为它不是实例上面的oldVdom
,所以有两个方案,一个是在mountClassComponent
的时候,需要把render出来的虚拟Dom也挂载到原来的vdom
上面.这样改的话,同时也还需要在forceUpdate
的里面,在最后把最新生成的newVdom
再次更新回vdom
上面.以确保能够通过它获取到最新的真实Dom.即修改
mountClassComponent
这个方法和Componennt
这个父类里面的forceUpdate
方法但也有另外一个改法,就是让
oldVdom
还是依然只挂载的实例上面,改造一个findDom
这个方法这里我们就采用后面这种改法.
在
src/index.js
增加一个Counter
的类组件,测试一下父子组件props
更新和state
更新是否正常.函数组件的更新
函数组件的更新和类组件的更新类似,调用新的
props
执行函数返回的虚拟Dom,然后再和老的虚拟Dom进行递归对比.在
src/react-dom/index.js
增加一个updateFunctionComponent
方法并在
updateElement
中增加这一个判断在
src/index.js
里面再拆分一个函数组件出来,测试一下函数组件的更新,以及涉及到元素类型变化的情况.看看效果是否是一致呢?