当前位置: 首页 > 科技观察

手写简单前端框架:补丁更新(1.0端)

时间:2023-03-20 23:00:10 科技观察

前两篇我们实现了vdom渲染和jsx编译,实现了函数和类组件。本文实现补丁更新。它可以渲染和更新vdom,并支持组件(props、state)。这是一个比较完整的前端框架。首先我们准备测试代码:测试代码在上一节的基础上修改:添加删除按钮、输入框和添加按钮,并添加相应的事件监听器:这部分代码通常由每个人,只是但更多的解释:functionItem(props){return{props.children}X;}classListextendsComponent{constructor(props){super();this.state={list:[{text:'aaa',color:'pink'},{text:'bbb',color:'orange'},{text:'ccc',color:'yellow'}]}}handleItemRemove(index){this.setState({list:this.state.list.filter((item,i)=>i!==index)});}handleAdd(){this.setState({列表:[...this.state.list,{文本:this.ref.value}]});}render(){return

{this.state.list.map((item,index)=>{returnthis.handleItemRemove(index)}>{item.text}})}
{this.ref=ele}}/>添加
;}}render(,document.getElementById('root'));之前我们已经实现了rendering,现在要实现update,也就是setState之后更新页面的过程其实实现patch最简单的update就是setState设置的时候重新渲染一次,替换掉之前的dom:setState(nextState){this.state=Object.assign(this.state,nextState);constnewDom=render(this.render());这个.dom.replaceWith(newDom);this.dom=newDom;}测试:我们实现了更新功能!只是在开玩笑。这样前端框架不会更新,很多不必要的dom操作,性能太差。所以我们还需要实现patch,即:setState(nextState){this.state=Object.assign(this.state,nextState);如果(this.dom){补丁(this.dom,this.render());}}"patch的作用是将要渲染的vdom和已有的dom进行diff,只更新需要更新的dom,即按需更新。"是否使用patch逻辑,可以加一个shouldComponentUpdate来控制。如果props和state没有变化,则不需要打补丁。setState(nextState){this.state=Object.assign(this.state,nextState);if(this.dom&&this.shouldComponentUpdate(this.props,nextState)){patch(this.dom,this.render());}}shouldComponentUpdate(nextProps,nextState){returnnextProps!=this.props||nextState!=this.state;}如何实现补丁?渲染时,我们是递归vdom,对元素、文本和组件进行不同的处理,包括创建节点和设置属性。更新patch时的递归是一样的,只是对元素、文本、组件的处理不同:如果text判断dom节点是文本,再看vdom:如果vdom不是文本节点,直接更换;如果vdom也是一个文本节点,则比较内容,替换}else{返回dom.textContent!=vdom?替换(渲染(vdom,父级)):dom;}}这里replace的实现是使用replaceChild:constreplace=parent?el=>{parent.replaceChild(el,dom);返回el;}:(el=>el);然后更新组件:如果组件vdom是一个组件,那么对应的dom可能会也可能不会被同一个组件渲染。判断dom是否是同一个组件渲染的,如果不是,直接替换,如果是,更新子元素:怎么知道dom是从哪个组件渲染的呢?我们需要给dom添加一个属性来记录渲染的时候:更改render部分的代码,添加instance属性:instance.dom.__instance=instance;那么在更新的时候,可以比较构造函数是否相同。如果相同,则表示相同的组件,则dom相似,然后patch元素:returnpatch(dom,dom.__instance.render(),parent);}else,不一样如果是组件,则直接替换:class组件替换:if(Component.isPrototypeOf(vdom.type)){constcomponentDom=renderComponent(vdom,parent);if(parent){parent.replaceChild(componentDom,dom);返回componentDom;}else{returncomponentDom}}function组件替换:if(!Component.isPrototypeOf(vdom.type)){returnpatch(dom,vdom.type(props),parent);}所以,组件更新逻辑就是这样:elementIfdom是一个元素,它取决于它是否是同一类型:},vdom.props,{children:vdom.children});如果(dom.__instance&&dom.__instance.constructor==vdom.type){dom.__instance.componentWillReceiveProps(props);返回补丁(dom,dom.__instance.render(),父级);}elseif(Component.isPrototypeOf(vdom.type)){constcomponentDom=renderComponent(vdom,parent);if(parent){parent.replaceChild(componentDom,dom);返回componentDom;}else{returncomponentDom}}elseif(!Component.isPrototypeOf(vdom.type)){returnpatch(dom,vdom.type(props),parent);}}还有元素更新:element如果dom是一个元素,则看下面是否是同一类型:不同类型的元素,直接替换if(dom.nodeName!==vdom.type.toUpperCase()&&typeofvdom==='object'){returnreplace(render(vdom,parent));}对于相同类型的元素,更新子节点和属性更新子节点,我们希望复用它们,所以添加一个标识key渲染时的每个元素:instance.dom.__key=vdom.props.key;更新时,如果找到key,则重新使用,如果没有找到,则渲染一个新的首先,我们将所有子节点的dom放入一个对象中:constoldDoms={};[].concat(...dom.childNodes).map((child,index)=>{constkey=child.__key||`__index_${index}`;oldDoms[key]=child;});[].concat就是把数组压平,因为数组的元素也是数组。默认键设置为索引。然后循环渲染vdom的子元素,如果找到对应的key就直接复用,然后继续patch它的子元素。如果没有找到,渲染一个新的:[].concat(...vdom.children).map((child,index)=>{constkey=child.props&&child.props.key||`__index_${index}`;dom.appendChild(oldDoms[key]?patch(oldDoms[key],child):render(child,dom));deleteoldDoms[key];});从oldDoms中删除新的dom。剩下的就是不再需要的dom,直接删除即可:for(constkeyinoldDoms){oldDoms[key].remove();}删除前可以执行下一个组件的willUnmount生命周期函数:for(constkeyinoldDoms){constinstance=oldDoms[key].__instance;if(实例)instance.componentWillUnmount();oldDoms[key].remove();}处理完子节点后,进行下一个属性:这个是把旧的属性删除,设置新的props:for(constattrofdom.attributes)dom.removeAttribute(attr。姓名);for(constpropinvdom.props)setAttribute(dom,prop,vdom.props[prop]);setAttribute之前我们只处理了style,事件监听和普通属性,需要改进的是:每一个事件监听都要去掉再添加,这样多次渲染总是只有一个:functionisEventListenerAttr(key,value){returntypeofvalue=='function'&&key.startsWith('on');}if(isEventListenerAttr(key,value)){consteventType=key.slice(2).toLowerCase();dom.__handlers=dom.__handlers||{};dom.removeEventListener(eventType,dom.__handlers[eventType]);dom.__handlers[事件类型]=值;dom.addEventListener(事件类型,dom.__handlers[eventType]);}将各种事件的监听器放在dom的__handlers属性上,每次都删除之前的,换成新的然后支持ref属性:就是这样一个函数:{this.ref=ele}}/>支持key设置:if(key=='key'){dom.__key=value;}还有一些特殊属性设置,包括checked,value,className:if(key=='checked'||key=='value'||key=='className'){dom[key]=value;}其余全部设置withsetAttribute:functionisPlainAttr(key,value){returntypeofvalue!='object'&&typeofvalue!='function';}if(isPlainAttr(key,value)){dom.setAttribute(key,value);}所以当前的setAttribute是这样的:constsetAttribute=(dom,key,value)=>{if(isEventListenerAttr(key,value)){consteventType=key.slice(2).toLowerCase();dom.__handlers=dom.__handlers||{};dom.removeEventListener(eventType,dom.__handlers[eventType]);dom.__handlers[事件类型]=值;dom.addEventListener(事件类型,dom.__处理程序[事件类型]);}elseif(key=='checked'||key=='value'||key=='className'){dom[key]=value;}elseif(isRefAttr(key,value)){value(dom);}elseif(isStyleAttr(key,value)){对象。分配(dom.style,value);}elseif(key=='key'){dom.__key=value;}elseif(isPlainAttr(key,value)){dom.setAttribute(key,value);}}文本、组件、元素的更新逻辑写完了,我们测试一下:大功告成!我们实现了patch的功能,也就是细粒度的按需更新代码上传到github:https://github.com/QuarkGluonPlasma/frontend-framework-exercize总结Patch和render一样,也是递归处理元素、组件和文本。在打补丁的时候,需要比较dom和要渲染的vdom中的一些信息,然后决定是渲染一个新的dom还是复用已有的dom。所以在渲染的时候需要在dom上记录instance、key等信息。元素子元素的更新应该支持key作为标识,这样可以复用之前的元素,减少dom的创建。设置属性的时候,事件监听器每次都需要删除已有的,添加新的,这样就只有一个了。实现了vdom、组件、生命周期的渲染和更新,已经是一个完整的前端框架了。这是我们实现的第一个前端框架版本,叫做Dong1.0。然而,目前的前端框架使用递归渲染和补丁。如果vdom树太大,计算量会很大,性能也不会很好。后面在Dong2.0中,我们会将vdom转化为fiber,进而实现hooks的功能。