当前位置: 首页 > Web前端 > HTML5

BuildyourownReact-(3)Instances,reconciliationandvirtualDOM

时间:2023-04-05 19:03:29 HTML5

翻译自:https://engineering.hexacta.c...至此我们就可以使用JSX来创建和渲染页面DOM了。在本节中,我们将重点介绍如何更新DOM。在引入setState之前,更新DOM只能通过更改输入参数并再次调用render方法来完成。如果我们想实现一个时钟,代码如下所示:constclockElement=

{时间}

;渲染(clockElement,rootDom);}tick();setInterval(tick,1000);事实上,上面的代码运行后并没有达到预期的效果,多次调用当前版本的render方法只会不断地向页面添加新的元素,而不是像我们预期的那样更新已有的元素。让我们想办法实现更新操作。在render方法的最后,我们可以检查父元素是否包含子元素,如果是,我们用新生成的元素替换旧元素。functionrender(element,parentDom){//...//从元素创建dom//...if(!parentDom.lastChild){parentDom.appendChild(dom);}else{parentDom.replaceChild(dom,parentDom.lastChild);}}对于一开始的时钟例子,上面render的实现是没有问题的。但是对于更复杂的情况,比如有多个子元素的时候,上面的代码就不能满足要求了。正确的做法是,我们需要比较前后两次调用render方法时生成的元素树,比较不同后才更新变化的部分。VirtualDOM和ReconciliationReact将一致性验证的过程称为“diffing”,我们也得像React一样做。首先,我们需要保存当前的元素树,以便后面和新的元素树进行对比,也就是说,我们需要保存当前页面内容对应的虚拟DOM。需要讨论这个虚拟DOM树的节点。一种选择是使用DidactElements,它已经有一个props.children属性,我们可以从中构建一个虚拟DOM树。现在有两个问题摆在我们面前:首先,为了方便对比,我们需要保存每个虚拟DOM指向的真实DOM的引用(我们会在验证过程中根据需要更新真实DOM的属性),并且元素必须不可用第二,当前元素不支持具有内部状态(state)的组件。Instances我们需要引入一个新的概念-----instances-----来解决上面的问题。一个实例表示一个已经渲染到DOM的元素,它是一个具有element、dom和childInstances属性的JS对象。childInstances是子元素对应的实例数组。请注意,这里提到的实例与DanAbramov在ReactComponents,Elements,andInstances中提到的实例不同。Dan说的是publicinstance,也就是调用继承自React.Component的组件的构造函数后返回的东西。我们将在后面的章节中添加公共实例。每个DOM节点都会有一个对应的实例。一致性检查的目的之一是尽可能避免创建或删除实例。创建和删除实例意味着我们必须修改DOM树,因此我们重用实例越多,我们修改DOM树的次数就越少。重构接下来我们重写render方法,添加一致性检查算法,添加instantiate方法为元素创建实例。让rootInstance=null;//用于保存上次调用render函数生成的实例render(element,container){constprevInstance=rootInstance;constnextInstance=reconcile(container,prevInstance,element);rootInstance=nextInstace;}//目前只对根元素进行校验,不处理子元素functionreconcile(parentDom,instance,element){if(instance===null){constnewInstance=instantiate(元素);parentDom.appendChild(newInstance.dom);返回新实例;}else{constnewInstance=实例化(元素);parentDom.replaceChild(newInstance.dom,instance.dom);返回新实例;}}//生成元素对应实例的方法functioninstantiate(element){const{type,props}=element;constisTextElement=type==='TEXT_ELEMENT';constdom=isTextElement?document.createTextNode(''):document.createElement(type);//添加事件constisListener=name=>name.startsWith("on");Object.keys(props).filter(isListener).forEach(na我=>{consteventType=name.toLowerCase().substring(2);dom.addEventListener(eventType,props[name]);});//设置属性constisAttribute=name=>!isListener(name)&&name!="children";Object.keys(props).filter(isAttribute).forEach(name=>{dom[name]=props[name];});常量childElements=props.children||[];constchildInstances=childElements.map(实例化);constchildDoms=childInstances.map(childInstance=>childInstace.dom);childDoms.forEach(childDom=>dom.appendChild(childDOm));constinstance={dom,element,childInstances};returninstance;}上面的render方法和前面的类似,不同的是保存了上次调用render方法生成的实例。我们还将一致性检查功能与创建实例的代码分开。为了复用dom节点,我们需要一个可以更新dom属性的方法,这样我们就不用每次都创建新的dom节点了。让我们修改现有代码中设置属性的部分。函数实例化(元素){常量{类型,道具}=元素;//创建DOM元素constisTextElement=type==='TEXT_ELEMENT';constdom=isTextElement?文档.createTextNode(""):文档.createElement(类型);updateDomProperties(dom,[],props);//实例化一个新的元素//实例化并添加子元素constchildElements=props.children||[];constchildInstances=childElements.map(实例化);constchildDoms=childInstances.map(childInstance=>childInstance.dom);childDoms.forEach(childDom=>dom.appendChild(childDom));constinstance={dom,element,childInstances};返回实例;}functionupdateDomProperties(dom,prevProps,nextProps){constisEvent=name=>name.startsWith('on');constisAttribute=name=>!isEvent(name)&&name!='children';Object.keys(prevProps).filter(isEvent).forEach(name=>{consteventType=name.toLowerCase().substring(2);dom.removeEventListener(eventType,prevProps[名称]);});Object.keys(preProps).filter(isAttribute).forEach(name=>{dom[name]=nextProps[name];});//设置属性Object.keys(nextProps).filter(isAttribute).forEach(name=>{dom[name]=nextProps[name];});//添加事件监听器Object.keys(nextProps).filter(isEvent).forEach(name=>{consteventType=name.toLowerCase().substring(2);dom.addEventListener(eventType,nextProps[name]);});}updateDomProperties方法会移除所有旧的属性,如果属性没有变化,再添加新的属性仍然会进行移除和添加操作,有点浪费,不过暂时先这么放着,处理一下之后。复用DOM节点前面提到,一致性验证算法需要复用尽可能多的节点。因为当前元素的类型在HTML中是一个表示标签名的字符串,如果同一位置前后两次渲染的元素类型相同,则说明两者是同一类型的元素,对应的已经渲染到页面上的dom节点可以被复用。接下来我们在reconcile中加入判断前后两次渲染的元素类型是否相同的功能。如果相同,则执行更新操作,否则创建或替换。functionreconcile(parentDom,instance,element){if(instance==null){//创建实例constnewInstance=instantiate(element);parentDom.appendChild(newInstance.dom);返回新实例;}elseif(instance.element.type===element.type){//将类型与旧实例进行比较//更新updateDomProperties(instance.dom,instance.element.props,element.props);instance.element=元素;返回实例;}else{//如果不相等,直接替换constnewInstance=instantiate(element);parentDom.replaceChild(newInstance.dom,instance.dom);返回新实例;}}ChildrenReconciliation子元素尚未在验证过程中处理。子元素的校验是React的关键部分。这个过程需要元素的一个额外的属性键来完成。如果新旧虚拟DOM上某个元素的键值相同,则表示该元素没有发生变化。直接复用即可。在当前版本的代码中,我们会遍历instance.childInstances和element.props.children,将instance和元素在同一位置进行比较,通过这种方式完成子元素的一致性检查。这种方法的缺点是,如果只是交换子元素,相应的DOM节点将不会被重用。我们递归比较同一个实例的最后一个instance.childInstances和本次对应元素的element.props.children,保存每次reconcile返回的结果来更新childInstances。functionreconcile(parentDom,instance,element){if(instance==null){constnewInstance=instantiate(element);parentDom.appendChild(newInstance.dom);返回新实例;}elseif(instance.element.type===element.type){updateDomProperties(instance.dom,instance.element.props,element.props);instance.childInstances=reconcileChildren(实例,元素);instance.element=元素;返回实例;}else{constnewInstance=实例化(元素);parentDom.replaceChild(newInstance.dom,instance.dom);返回新实例;}}functionreconcileChildren(instance,element){constdom=instance.dom;constchildInstances=instance.childInstances;常量nextChildElements=element.props.children||[];constnewChildInstances=[];constcount=Math.max(childInstances.length,nextChildElements.length);for(让i=0;iinstance!=null)}总结在这一节中,我们为Didact添加了更新DOM的功能。通过复用节点,我们避免了频繁创建和移除DOM节点,提高了Didact的工作效率。重用节点也有一定的好处,比如保存一些内部状态信息,比如DOM的位置或焦点。目前我们是在根元素上调用render方法,每次有变化也是对整个元素树的一致性检查。我们将在下一节介绍组件。对于组件,我们只能对子树发生变化的部分进行一致性检查。

最新推荐
猜你喜欢