欢迎关注个人公众号睿Talk
,获取我最新的文章:javascript
目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提升页面的渲染效率。那么,什么是Virtual DOM?它是经过什么方式去提高页面渲染效率的呢?本系列文章会详细讲解Virtual DOM的建立过程,并实现一个简单的Diff算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的Virtual DOM。敲单词太累了,下文Virtual DOM一概用VD表示。前端
这是VD系列文章的第六篇,如下是本系列其它文章的传送门:
你不知道的Virtual DOM(一):Virtual Dom介绍
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新优化
你不知道的Virtual DOM(四):key的做用
你不知道的Virtual DOM(五):自定义组件
你不知道的Virtual DOM(六):事件处理&异步更新java
今天,咱们继续在以前项目的基础上扩展功能。在上一篇文章中,介绍了自定义组件的渲染和更新的实现方法。为了验证setState是否生效,还定义了一个setTimeout方法,5秒后更新state。在现实的项目中,state的改变每每是经过事件触发的,如点击事件、键盘事件和滚动事件等。下面,咱们就将事件处理加入到项目当中。git
事件的绑定通常是定义在元素或者组件的属性当中,以前对属性的初始化和更新没有考虑支持事件,只是简单的赋值操做。github
// 属性赋值 function setProps(element, props) { // 属性赋值 element[ATTR_KEY] = props; for (let key in props) { element.setAttribute(key, props[key]); } } // 比较props的变化 function diffProps(newVDom, element) { let newProps = {...element[ATTR_KEY]}; const allProps = {...newProps, ...newVDom.props}; // 获取新旧全部属性名后,再逐一判断新旧属性值 Object.keys(allProps).forEach((key) => { const oldValue = newProps[key]; const newValue = newVDom.props[key]; // 删除属性 if (newValue == undefined) { element.removeAttribute(key); delete newProps[key]; } // 更新属性 else if (oldValue == undefined || oldValue !== newValue) { element.setAttribute(key, newValue); newProps[key] = newValue; } } ) // 属性从新赋值 element[ATTR_KEY] = newProps; }
setProps
是在建立元素的时候调用的,而diffProps
则是在diff过程当中调用的。若是须要支持事件绑定,咱们须要多作一个判断。若是属性名称是on
开头的话,好比onClick,咱们就要在当前元素上注册或删除一个事件处理。算法
// 属性赋值 function setProps(element, props) { // 属性赋值 element[ATTR_KEY] = props; for (let key in props) { // on开头的属性看成事件处理 if (key.substring(0, 2) == 'on') { const evtName = key.substring(2).toLowerCase(); element.addEventListener(evtName, evtProxy); (element._evtListeners || (element._evtListeners = {}))[evtName] = props[key]; } else { element.setAttribute(key, props[key]); } } } function evtProxy(evt) { this._evtListeners[evt.type](evt); } // 比较props的变化 function diffProps(newVDom, element) { let newProps = {...element[ATTR_KEY]}; const allProps = {...newProps, ...newVDom.props}; // 获取新旧全部属性名后,再逐一判断新旧属性值 Object.keys(allProps).forEach((key) => { const oldValue = newProps[key]; const newValue = newVDom.props[key]; // on开头的属性看成事件处理 if (key.substring(0, 2) == 'on') { const evtName = key.substring(2).toLowerCase(); if (newValue) { element.addEventListener(evtName, evtProxy); } else { element.removeEventListener(evtName, evtProxy); } (element._evtListeners || (element._evtListeners = {}))[evtName] = newValue; } else { // 删除属性 if (newValue == undefined) { element.removeAttribute(key); delete newProps[key]; } // 更新属性 else if (oldValue == undefined || oldValue !== newValue) { element.setAttribute(key, newValue); newProps[key] = newValue; } } } ) // 属性从新赋值 element[ATTR_KEY] = newProps; }
全部的事件处理函数都存到dom元素的_evtListeners当中,当事件触发的时候,将事件传给里面对应的方法处理。这样作的好处是若是之后要对浏览器传入的事件evt
作进一步的封装,就能够在evtProxy
函数里面处理。segmentfault
接下来,咱们在自定义组件里面新增一个onClick
事件,在点击的时候改变state里面的值。数组
class MyComp extends Component { constructor(props) { super(props); this.state = { name: 'Tina', count: 1 } } elmClick() { this.setState({name: `Jack${this.state.count}`, count: this.state.count + 1 }); } render() { return( <div id="myComp" onClick={this.elmClick.bind(this)}> <div>This is My Component! {this.props.count}</div> <div>name: {this.state.name}</div> </div> ) } }
项目运行的效果是每当我点一下MyComp组件的区域,里面的name就会随之立刻更新。浏览器
用过React的朋友都知道,为了减小没必要要的渲染,提升性能,React并非在咱们每次setState的时候都进行渲染,而是将一个同步操做里面的多个setState进行合并后再渲染,给人异步渲染的感受。看过源码的都应该知道,React是经过事务的方式来合并多个setState操做的,本质来讲仍是同步的。若是想对其做更深刻的学习,推荐看这篇文章。性能优化
为了达到合并操做,减小渲染的效果,最简单的方式就是异步渲染,下面咱们来看看如何实现。在上一个版本里,setState是这么定义的:
class Component { ... setState(newState) { this.state = {...this.state, ...newState}; const vdom = this.render(); diff(this.dom, vdom, this.parent); } ... };
state更新后直接就进行diff操做,进而更新页面。若是咱们onClick里面的代码改为这样:
elmClick() { this.setState({name: `Jack${this.state.count}`, count: this.state.count + 1 }); this.setState({name: `Jack${this.state.count}`, count: this.state.count + 1 }); }
页面会渲染2次。若是咱们把它改形成下面的样子:
// 等待渲染的组件数组 let pendingRenderComponents = []; class Component { ... setState(newState) { this.state = {...this.state, ...newState}; enqueueRender(this); } ... }; function enqueueRender(component) { // 若是push后数组长度为1,则将异步刷新任务加入到事件循环当中 if (pendingRenderComponents.push(component) == 1) { if (typeof Promise=='function') { Promise.resolve().then(renderComponent); } else { setTimeout(renderComponent, 0); } } } function renderComponent() { // 组件去重 const uniquePendingRenderComponents = [...new Set(pendingRenderComponents)]; // 渲染组件 uniquePendingRenderComponents.forEach(component => { const vdom = component.render(); diff(component.dom, vdom, component.parent); }); // 清空待渲染列表 pendingRenderComponents = []; }
当第一次setState
成功后,并不会立刻进行渲染,而是将组件存入待渲染组件列表当中。若是列表是空的,则存入组件后将异步刷新任务加入到事件循环当中。当运行环境支持Promise时,经过微任务运行,不然经过宏任务运行。微任务的运行时间是当前事件循环的末尾,而宏任务的运行时间是下一个事件循环。因此优先使用微任务。
紧接着进行第二次setState
操做,一样的,将组件存入待渲染组件列表当中。此时,主线程的任务执行完了,开始执行异步任务。
当异步刷新任务启动时,将待渲染列表去重后对里面的组件进行渲染。等渲染完成后再清空待渲染列表。此时,渲染出来的是2次setState
合并后的结果,而且只会进行一次diff
操做,渲染一次。
本文基于上一个版本的代码,加入了事件处理功能,同时经过异步刷新的方法提升了渲染效率。
这是VD系列的最后一篇文章。本系列从什么是Virtual Dom
这个问题出发,讲解了VD的数据结构、比较方式和更新流程,并在此基础上进行功能扩展和性能优化,支持key元素复用、自定义组件,dom事件绑定和setState异步更新。总共三百多行代码,实现了mvvm库的核心功能。
有关VD,若是还有什么想了解的,欢迎留言,有问必答。
P.S.: 想看完整代码见这里,若是有必要建一个仓库的话请留言给我:代码