前言

当我们操作Dom其实是一件非常耗性能的事,每个元素都涵盖了许多的属性,因为浏览器的标准就把 DOM 设计的非常复杂。而Virtual Dom就是用一个原生的JS对象去描述一个DOM节点,即VNode,所以它比创建一个真实的Dom元素所产生代价要小得多。而我们主流的框架React和Vue正是采用了这种做法,那我们来看下如何实现一个简单的Virtual Dom。完整代码GitHub。喜欢的话希望点个小星星哦 ^_^~~~

核心

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把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.jsimport 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.jsconst renderElem = ({ tagName, attrs, children}) => {// create root elementlet $el = document.createElement(tagName);// set attributedsfor (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 createTextNodeif (typeof vNode === 'string') {return document.createTextNode(vNode);}// otherwise return renderElemreturn renderElem(vNode);
}export default render复制代码

然后我们回到main.js中

// 引入 render.js 模块const $app  = render(vApp); // 开始构建真实的domlet $rootEl = mount($app, document.getElementById('app'));// 创建 mount.jsexport 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.jsimport 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 attributefor(const k in oldAttrs) {if (!(k in newAttrs)) {// $node 是整颗真实的 dom treepatches.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 allif (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); // 新的 vDomconst patch = diff(vApp, vNewApp); // 对比差异$rootEl = patch($rootEl);vApp = vNewApp; // 每一秒之后都有更新,保存起来以供下次比对。
}, 1000)
复制代码

废话少说,先看效果 (: ~~

可以发现,input 没有情况,也就是说页面没有刷新,setInterval每次将count++, 页面上也只更新了变化了的属性以及文本,这就是diff算法的威力。

分析一波

  • diff

diff 函数接收两个参数,vOldNode 和 vNewNode.

  1. 判断 vNewNode 是不是 undefined,假如整颗树都给删了呢 ? 那就 $node.remove() 移出就好了
  2. 如果只是改了标签名,那好办,直接 render ,然后 replaceWith 就好了。
  3. 如果新老节点是 'string' 类型,那还得判断新老节点是否相等 !
  4. 所有得到的差异结果都扔进 patches 中, 注意,是个函数哦 , 接收的参数就是 $rootEl
  • diffAttributes

比对属性好办,就是拿到新的 vDom 的属性,然后遍历老的 vDom 的属性,判断老的 vDom 的属性是否存在于新的 vDom 中。关键点我将它描述出来

  1. Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环也枚举原型链中的属性)
  2. 通过for of 遍历oldAttrs,拿到所有老的 vDom 中的key
  3. 通过 in 操作符 来判断 2 中的 key 是否存在于 newAttrs 中.
  4. 最后返回一个函数,接收 $rootEl,遍历属性对比出来的 patches.每一项是一个函数.
  • diffChildren

最后就是要对比 children 了。

  1. 接收俩参数,oldVChildren 和 newVChildren
  2. 这里最主要的还是 zip 函数了。得到新老节点的 child, 将每个节点的老的节点和新的节点存放到一个数组中,如图:
  1. 然后遍历这个 zipped 数组.继续diff, 并且保存 diff 后的结果
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {childPatches.push(diff(oldVChild, newVChild));
}复制代码

结语

Virtual DOM 最核心的部分就是 diff 算法了,这里还是比较复杂的,需要多加练习反复琢磨,好了,今天的介绍就到这了,如果喜欢你就点点赞哦 !

Vue源码分析系列四:Virtual DOM相关推荐

  1. vue源码分析系列二:$mount()和new Watcher()的执行过程

    续vue源码分析系列一:new Vue的初始化过程 在initMixin()里面调用了$mount() if (vm.$options.el) {vm.$mount(vm.$options.el);/ ...

  2. vue源码分析系列三:render的执行过程和Virtual DOM的产生

    render 手写 render 函数,仔细观察下面这段代码,试想一下这里的 createElement 参数是什么 . new Vue({el: '#application',render(crea ...

  3. vue源码分析系列一:new Vue的初始化过程

    import Vue from 'vue'(作者用的vue-cli一键生成) node环境下import Vue from 'vue'的作用是什么意思? 在 NPM 包的 dist/ 目录你将会找到很 ...

  4. Vue源码解析系列——数据驱动篇:patch的执行过程

    准备 vue版本号2.6.12,为方便分析,选择了runtime+compiler版本. 回顾 如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:<Vue源码分析系列:目录> ...

  5. [Vue源码分析] 模板的编译

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: Vue有两个版本:Runtime + Compiler . Runtime only ,前者是包含编译代码的版本,后者不包含编译代码,编 ...

  6. live555源码分析(四)RTSPServer分析

    live555源码分析系列 live555源码分析(一)live555初体验 live555源码分析(二)基本组件上 live555源码分析(三)基本组件下 live555源码分析(四)RTSPSer ...

  7. webpack 源码分析系列 ——loader

    想要更好的格式阅读体验,请查看原文:webpack 源码分析系列 --loader 为什么需要 loader webpack是一个用于现代 JavaScript 应用程序的静态模块打包工具.内部通过构 ...

  8. jQuery源码分析系列

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...

  9. [Vue源码分析]自定义事件原理及事件总线的实现

    最近小组有个关于vue源码分析的分享会,提前准备一下- 前言: 我们都知道Vue中父组件可以通过 props 向下传数据给子组件:子组件可以通过向$emit触发一个事件,在父组件中执行回调函数,从而实 ...

最新文章

  1. wxpython中的所有文本框都是TextCtrl,不同的形式的文本框通过样式来实现,下面创建了一个密码输入框...
  2. numpy中tile函数
  3. c++ vector 初始化_什么?还不懂c++vector的用法,你凭什么勇气来的!
  4. 前端—每天5道面试题(3)
  5. 重新认识一个强大的 Gson
  6. python情感分析词典_基于情感词典的文本情感分析
  7. iphone pop服务器没有响应,iPhone 6 Plus跳屏或者触摸失灵的解决办法
  8. minidump详细介绍
  9. linux如何查询内存型号,查看linux 查看内存型号
  10. 怎样手工清除autorun病毒
  11. [转]我在上海的五年奋斗岁月
  12. Protocol buffer配置-生成jar包和java文件
  13. JAVA经典面试题(来源于互联网)
  14. 这款安全好用的手机浏览器,真是一股清流,值得点赞
  15. 【web前端】小人行走
  16. 【Comet OJ - 2019国庆欢乐赛 F】 高速公路
  17. 方形图片转为圆形图片
  18. mysql pga_PGA概述
  19. AL32UTF8/UTF8(Unicode)数据库字符集含义 (文档 ID 1946289.1)
  20. 好玩的农场游戏排行榜,好玩的农场游戏有哪些

热门文章

  1. 冲刺阶段——Day2
  2. ACM-ICPC 2018 徐州赛区网络预赛 Morgana Net
  3. Ubuntu设置环境变量并立即生效
  4. hdu 2275 Kiki Little Kiki 1
  5. JAVA的静态变量、静态方法、静态类
  6. OSI七层与TCP/IP四/五层网络架构
  7. 用python画图所需要的插件_用Python画图
  8. for of 和 for in 在数组和对象中的区别
  9. AI:IPPR的数学表示-CNN结构/参数分析
  10. 酷派android是什么系统版本,酷派大神X7的手机系统是什么?能升级安卓5.0吗?