内容简介:当我们操作Dom其实是一件非常耗性能的事,每个元素都涵盖了许多的属性,因为浏览器的标准就把 DOM 设计的非常复杂。而Virtual Dom就是用一个原生的JS对象去描述一个DOM节点,即VNode,所以它比创建一个真实的Dom元素所产生代价要小得多。而我们主流的框架React和Vue正是采用了这种做法,那我们来看下如何实现一个简单的Virtual Dom。完整代码首先我们需要构建vDom, 用js对象来描述真正的dom tree,构建好了vDom之后就需要将其render到我们的页面上了下面这个就是构建的
当我们操作Dom其实是一件非常耗性能的事,每个元素都涵盖了许多的属性,因为浏览器的标准就把 DOM 设计的非常复杂。而Virtual Dom就是用一个原生的JS对象去描述一个DOM节点,即VNode,所以它比创建一个真实的Dom元素所产生代价要小得多。而我们主流的框架React和Vue正是采用了这种做法,那我们来看下如何实现一个简单的Virtual Dom。完整代码 GitHub 。喜欢的话希望点个小星星哦 ^_^~~~
核心
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
构建vDOM
首先我们需要构建vDom, 用js对象来描述真正的dom tree,构建好了vDom之后就需要将其render到我们的页面上了
// createElement.js // give some default value. export default (tagName, {attrs = {}, children = []} = {}) => { return { tagName, attrs, children } } // main.js import createElement from './vdom/createElement' const createVApp = (count) => createElement('div', { attrs: { id: 'app', dataCount: count }, children: [ createElement('input'), // dom重绘使得Input失焦 String(count), // 文本节点 createElement('img', { attrs: { src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1555610261877&di=6619e67b4f45768a359a296c55ec1cc3&imgtype=0&src=http%3A%2F%2Fimg.bimg.126.net%2Fphoto%2Fmr7DezX-Q4GLNBM_VPVaWA%3D%3D%2F333829322379622331.jpg' } }) ] }) let count = 0; let vApp = createVApp(count); 复制代码
下面这个就是构建的 vDom 啦!
然后我我们看看render 方法,这个方法就是将我们的 vDom 转化成真是的 element.
// render.js const renderElem = ({ tagName, attrs, children}) => { // create root element let $el = document.createElement(tagName); // set attributeds for (const [k, v] of Object.entries(attrs)) { $el.setAttribute(k, v); } // set children (Array) for (const child of children) { const $child = render(child); $el.appendChild($child); } return $el; } const render = (vNode) => { // if element node is text, and createTextNode if (typeof vNode === 'string') { return document.createTextNode(vNode); } // otherwise return renderElem return renderElem(vNode); } export default render 复制代码
然后我们回到main.js中
// 引入 render.js 模块 const $app = render(vApp); // 开始构建真实的dom let $rootEl = mount($app, document.getElementById('app')); // 创建 mount.js export default ($node, $target) => { // use $node element replace $target element! $target.replaceWith($node); return $node; } 复制代码
最后你就可以看到效果了. 是不是很帅 ? O(∩_∩)O哈哈 ~~~~
现在我们来做一些好玩的事儿。回到 main.js 中,我们加入如下这段代码:
setInterval(() => { count++; $rootEl = mount(render(createVApp(count)), $rootEl); // $rootEl 就是整颗real dom }, 1000) 复制代码
然后回到我们的页面,发现什么了吗? 你可以尝试在 input 里面输入一些东西,然后发现了什么异常了吗 ?
查看源代码,原来,每隔一秒我们就刷新了一次页面。可是我们只改变了 count ,就重绘一次页面,未免也夸张了吧,假如我们填写一个表单,填的手都要断了,结果刷新了页面,你猜会怎么着? 会不会想砸电脑呢 ? 别急,diff 算法能帮我们解决这给令人头疼的问题 !
diff
diff 算法的概念我就在这儿就不介绍了,大家可以在网上搜到很多答案。直接上代码 !
// diff.js import render from './render' const zip = (xs, ys) => { const zipped = []; for (let i = 0; i < Math.min(xs.length, ys.length); i++) { zipped.push([xs[i], ys[i]]); } return zipped; }; const diffAttributes = (oldAttrs, newAttrs) => { const patches = []; // set new attributes // oldAttrs = {dataCount: 0, id: 'app'} // newAttrs = {dataCount: 1, id: 'app'} // Object.entries(newAttrs) => [['dataCount', 1], ['id', 'app']] for(const [k, v] of Object.entries(newAttrs)) { patches.push($node => { $node.setAttribute(k, v); return $node; }) } // remove old attribute for(const k in oldAttrs) { if (!(k in newAttrs)) { // $node 是整颗真实的 dom tree patches.push($node => { $node.removeAttribute(k); return $node; }) } } return $node => { for (const patch of patches) { patch($node); } } } const diffChildren = (oldVChildren, newVChildren) => { const childPatches = []; for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) { childPatches.push(diff(oldVChild, newVChild)); } const additionalPatches = []; for (const additionalVChild of additionalPatches.slice(oldVChildren.length)) { additionalPatches.push($node => { $node.appendChild(render(additionalVChild)); return $node; }) } return $parent => { for (const [patch, child] of zip(childPatches, $parent.childNodes)) { patch(child); } for (const patch of additionalPatches) { patch($parent); } return $parent; } } const diff = (vOldNode, vNewNode) => { // remove all if (vNewNode === 'undefined') { return $node => { // Node.remove() 方法,把对象从它所属的DOM树中删除。 $node.remove(); return undefined; }; } // when element is textnode (like count) if (typeof vOldNode === 'string' || typeof vNewNode === 'string') { if (vOldNode !== vNewNode) { return $node => { const $newNode = render(vNewNode); $node.replaceWith($newNode); return $newNode; }; } else { return $node => undefined; } } if (vOldNode.tagName !== vNewNode.tagName) { return $node => { const $newNode = render(vNewNode); $node.replaceWith($newNode); return $newNode; }; } const patchAttrs = diffAttributes(vOldNode.attrs, vNewNode.attrs); const patchChildren = diffChildren(vOldNode.children, vNewNode.children); return $node => { patchAttrs($node); patchChildren($node); return $node; }; }; export default diff; // main.js setInterval(() => { count++; // 每隔一秒,重绘一次页面,input失焦(缺点) // $rootEl = mount(render(createVApp(count)), $rootEl) // 衍生出 diff 算法 const vNewApp = createVApp(count); // 新的 vDom const patch = diff(vApp, vNewApp); // 对比差异 $rootEl = patch($rootEl); vApp = vNewApp; // 每一秒之后都有更新,保存起来以供下次比对。 }, 1000) 复制代码
废话少说,先看效果 (: ~~
可以发现,input 没有情况,也就是说页面没有刷新,setInterval每次将count++, 页面上也只更新了变化了的属性以及文本,这就是diff算法的威力。
分析一波
- diff
diff 函数接收两个参数,vOldNode 和 vNewNode.
- 判断 vNewNode 是不是 undefined,假如整颗树都给删了呢 ? 那就 $node.remove() 移出就好了
- 如果只是改了标签名,那好办,直接 render ,然后 replaceWith 就好了。
- 如果新老节点是 'string' 类型,那还得判断新老节点是否相等 !
- 所有得到的差异结果都扔进 patches 中, 注意,是个函数哦 , 接收的参数就是 $rootEl
- diffAttributes
比对属性好办,就是拿到新的 vDom 的属性,然后遍历老的 vDom 的属性,判断老的 vDom 的属性是否存在于新的 vDom 中。关键点我将它描述出来
- Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环也枚举原型链中的属性)
- 通过for of 遍历oldAttrs,拿到所有老的 vDom 中的key
- 通过 in 操作符 来判断 2 中的 key 是否存在于 newAttrs 中.
- 最后返回一个函数,接收 $rootEl,遍历属性对比出来的 patches.每一项是一个函数.
- diffChildren
最后就是要对比 children 了。
- 接收俩参数,oldVChildren 和 newVChildren
- 这里最主要的还是 zip 函数了。得到新老节点的 child, 将每个节点的老的节点和新的节点存放到一个数组中,如图:
- 然后遍历这个 zipped 数组.继续diff, 并且保存 diff 后的结果
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) { childPatches.push(diff(oldVChild, newVChild)); } 复制代码
结语
Virtual DOM 最核心的部分就是 diff 算法了,这里还是比较复杂的,需要多加练习反复琢磨,好了,今天的介绍就到这了,如果喜欢你就点点赞哦 !
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
Markdown 在线编辑器
Markdown 在线编辑器