[TOC]
<br>
<br>
# 模板转换成视图的过程
* Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
* 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。
<br>
简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。
<br>
![](https://user-gold-cdn.xitu.io/2019/6/26/16b9230a4c3cdd40?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
我们先对上图几个概念加以解释:
* **渲染函数**:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
* **VNode 虚拟节点**:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成**节点描述对象**,它描述了应该怎样去创建真实的DOM节点。
* **patch(也叫做patching算法)**:虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于[Snabbdom](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fsnabbdom%2Fsnabbdom "https://github.com/snabbdom/snabbdom")的实现,并在些基础上作了很多的调整和改进。
<br>
<br>
# Virtual DOM
## Virtual DOM 是什么?
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
<br>
简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。
<br>
对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了`模板 → 渲染函数 → 虚拟DOM树 → 真实DOM`的一个过程
![image.png](https://user-gold-cdn.xitu.io/2019/6/25/16b8a4e46453e643?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
<br>
## Virtual DOM 作用是什么?
**虚拟DOM的最终目标是将虚拟节点渲染到视图上**。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。
<br>
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。
<br>
其实虚拟DOM在Vue.js主要做了两件事:
* 提供与真实DOM节点所对应的虚拟节点vnode
* 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图
<br>
<br>
## 为何需要Virtual DOM?
* 具备跨平台的优势
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
<br>
* 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。
<br>
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)
<br>
* 提升渲染性能
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。**我们通过patch 的核心----diff 算法,找出本次DOM需要更新的节点来更新,其他的不更新**。那diff 算法的实现过程是怎样的?
<br>
## 实现 vnode
**(1)如何用 `JS` 对象模拟 `DOM` 树**
例如一个真实的 `DOM` 节点如下:
~~~
<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>
复制代码
~~~
我们用 `JavaScript` 对象来表示 `DOM` 节点,使用对象的属性记录节点的类型、属性、子节点等。
`element.js` 中表示节点对象代码如下:
~~~
/**
* Element virdual-dom 对象定义
* @param {String} tagName - dom 元素名称
* @param {Object} props - dom 属性
* @param {Array<Element|String>} - 子节点
*/
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
// dom 元素的 key 值,用作唯一标识符
if(props.key){
this.key = props.key
}
var count = 0
children.forEach(function (child, i) {
if (child instanceof Element) {
count += child.count
} else {
children[i] = '' + child
}
count++
})
// 子元素个数
this.count = count
}
function createElement(tagName, props, children){
return new Element(tagName, props, children);
}
module.exports = createElement;
~~~
根据 `element` 对象的设定,则上面的 `DOM` 结构就可以简单表示为:
~~~
var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[
el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
~~~
现在 `ul` 就是我们用 `JavaScript` 对象表示的 `DOM` 结构,我们输出查看 `ul` 对应的数据结构如下:
![](https://user-gold-cdn.xitu.io/2019/7/23/16c1e14fcff074f0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
**(2)渲染用 `JS` 表示的 `DOM` 对象**
但是页面上并没有这个结构,下一步我们介绍如何将 `ul` 渲染成页面上真实的 `DOM` 结构,相关渲染函数如下:
~~~
/**
* render 将virdual-dom 对象渲染为实际 DOM 元素
*/
Element.prototype.render = function () {
var el = document.createElement(this.tagName)
var props = this.props
// 设置节点的DOM属性
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child) // 如果字符串,只构建文本节点
el.appendChild(childEl)
})
return el
}
~~~
我们通过查看以上 `render` 方法,会根据 `tagName` 构建一个真正的 `DOM` 节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。
我们将构建好的 `DOM` 结构添加到页面 `body` 上面,如下:
~~~
ulRoot = ul.render();
document.body.appendChild(ulRoot);
复制代码
~~~
这样,页面 `body` 里面就有真正的 `DOM` 结构,效果如下图所示:
![](https://user-gold-cdn.xitu.io/2019/7/23/16c1e179a748d425?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
<br>
# diff 算法
![diff 算法](https://user-gold-cdn.xitu.io/2019/6/23/16b82599955f3694?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
Vue的diff算法是基于snabbdom改造过来的,**仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新**。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。
<br>
diff 算法包括几个步骤:
* 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
* 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
* 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了
![image.png](https://user-gold-cdn.xitu.io/2019/6/24/16b8a2b9e4725e5a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
## diff 过程
<br>
![img](https://user-gold-cdn.xitu.io/2017/9/18/ed3fe3ef6c580e16711c39159ce87cd4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx时结束循环。
<br>
索引与VNode节点的对应关系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode
<br>
在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。
<br>
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2\*2=4种比较方法。
<br>
当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。
<br>
![img](https://user-gold-cdn.xitu.io/2017/9/18/dbf1c71d42eaddc6de60301aad17c860?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
<br>
这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
<br>
![img](https://user-gold-cdn.xitu.io/2017/9/18/0b5beb1c771c3965a77c787fe55a3b57?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。
![img](https://user-gold-cdn.xitu.io/2017/9/18/dc9a1e0b27411b2585960e971559382f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。
![img](https://user-gold-cdn.xitu.io/2017/9/18/ed03e90b708939205236225c582e26fb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。
![img](https://user-gold-cdn.xitu.io/2017/9/18/73241a7ea0b6f52c0df4d835a827f3b4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。
<br>
1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。
![img](https://user-gold-cdn.xitu.io/2017/9/18/22369d39d970155963bd71a1370e9b07?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。
![img](https://user-gold-cdn.xitu.io/2017/9/18/c067fa75aa884a2c231d940de35ef7a1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
<br>
# patch
patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较viturl dom的变更,得出变化以修改视图。
<br>
那么patch如何工作的呢?
<br>
首先说一下patch的核心diff算法,diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。
<br>
![img](https://user-gold-cdn.xitu.io/2017/9/18/599392157760360fa2c45895fe3438e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
![img](https://user-gold-cdn.xitu.io/2017/9/18/ebc9bc6e6792591c8a716c9ed876f87d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
<br>
着两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。
- 第一部分 HTML
- meta
- meta标签
- HTML5
- 2.1 语义
- 2.2 通信
- 2.3 离线&存储
- 2.4 多媒体
- 2.5 3D,图像&效果
- 2.6 性能&集成
- 2.7 设备访问
- SEO
- Canvas
- 压缩图片
- 制作圆角矩形
- 全局属性
- 第二部分 CSS
- CSS原理
- 层叠上下文(stacking context)
- 外边距合并
- 块状格式化上下文(BFC)
- 盒模型
- important
- 样式继承
- 层叠
- 属性值处理流程
- 分辨率
- 视口
- CSS API
- grid(未完成)
- flex
- 选择器
- 3D
- Matrix
- AT规则
- line-height 和 vertical-align
- CSS技术
- 居中
- 响应式布局
- 兼容性
- 移动端适配方案
- CSS应用
- CSS Modules(未完成)
- 分层
- 面向对象CSS(未完成)
- 布局
- 三列布局
- 单列等宽,其他多列自适应均匀
- 多列等高
- 圣杯布局
- 双飞翼布局
- 瀑布流
- 1px问题
- 适配iPhoneX
- 横屏适配
- 图片模糊问题
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 内存空间
- 作用域
- 执行上下文栈
- 变量对象
- 作用域链
- this
- 类型转换
- 闭包(未完成)
- 原型、面向对象
- class和extend
- 继承
- new
- DOM
- Event Loop
- 垃圾回收机制
- 内存泄漏
- 数值存储
- 连等赋值
- 基本类型
- 堆栈溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍历对象属性
- 宽度、高度
- performance
- 位运算
- tostring( ) 与 valueOf( )方法
- JavaScript技术
- 错误
- 异常处理
- 存储
- Cookie与Session
- ES6(未完成)
- Babel转码
- let和const命令
- 变量的解构赋值
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 数组的扩展
- 函数的扩展
- 对象的扩展
- Symbol
- Set 和 Map 数据结构
- proxy
- Reflect
- module
- AJAX
- ES5
- 严格模式
- JSON
- 数组方法
- 对象方法
- 函数方法
- 服务端推送(未完成)
- JavaScript应用
- 复杂判断
- 3D 全景图
- 重载
- 上传(未完成)
- 上传方式
- 文件格式
- 渲染大量数据
- 图片裁剪
- 斐波那契数列
- 编码
- 数组去重
- 浅拷贝、深拷贝
- instanceof
- 模拟 new
- 防抖
- 节流
- 数组扁平化
- sleep函数
- 模拟bind
- 柯里化
- 零碎知识点
- 第四部分 进阶
- 计算机原理
- 数据结构(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 搜索算法
- 动态规划
- 二叉树
- 浏览器
- 浏览器结构
- 浏览器工作原理
- HTML解析
- CSS解析
- 渲染树构建
- 布局(Layout)
- 渲染
- 浏览器输入 URL 后发生了什么
- 跨域
- 缓存机制
- reflow(回流)和repaint(重绘)
- 渲染层合并
- 编译(未完成)
- Babel
- 设计模式(未完成)
- 函数式编程(未完成)
- 正则表达式(未完成)
- 性能
- 性能分析
- 性能指标
- 首屏加载
- 优化
- 浏览器层面
- HTTP层面
- 代码层面
- 构建层面
- 移动端首屏优化
- 服务器层面
- bigpipe
- 构建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack优化
- Webpack原理
- 实现loader
- 实现plugin
- tapable
- Webpack打包后代码
- rollup.js
- parcel
- 模块化
- ESM
- 安全
- XSS
- CSRF
- 点击劫持
- 中间人攻击
- 密码存储
- 测试(未完成)
- 单元测试
- E2E测试
- 框架测试
- 样式回归测试
- 异步测试
- 自动化测试
- PWA
- PWA官网
- web app manifest
- service worker
- app install banners
- 调试PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 饿了么整理
- 样式
- 技巧
- Vue音乐播放器
- Vue源码
- Virtual Dom
- computed原理
- 数组绑定原理
- 双向绑定
- nextTick
- keep-alive
- 导航守卫
- 组件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 动画(未完成)
- 异常监控、收集(未完成)
- 数据采集
- Sentry
- 贝塞尔曲线
- 视频
- 服务端渲染
- 服务端渲染的利与弊
- Vue SSR
- React SSR
- 客户端
- 离线包
- 第五部分 网络
- 五层协议
- TCP
- UDP
- HTTP
- 方法
- 首部
- 状态码
- 持久连接
- TLS
- content-type
- Redirect
- CSP
- 请求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服务端
- Linux
- Linux命令
- 权限
- XAMPP
- Node.js
- 安装
- Node模块化
- 设置环境变量
- Node的event loop
- 进程
- 全局对象
- 异步IO与事件驱动
- 文件系统
- Node错误处理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服务
- 负载均衡
- 获取用户IP
- 解决跨域
- 适配PC与移动环境
- 简单的访问限制
- 页面内容修改
- 图片处理
- 合并请求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自动化(未完成)
- docker
- 创建CLI
- 持续集成
- 持续交付
- 持续部署
- Jenkins
- 部署与发布
- 远程登录服务器
- 增强服务器安全等级
- 搭建 Nodejs 生产环境
- 配置 Nginx 实现反向代理
- 管理域名解析
- 配置 PM2 一键部署
- 发布上线
- 部署HTTPS
- Node 应用
- 爬虫(未完成)
- 例子
- 反爬虫
- 中间件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源码
- app.js
- config.js
- 消息队列
- RPC
- 性能优化
- 第七部分 总结
- Web服务器
- 目录结构
- 依赖
- 功能
- 代码片段
- 整理
- 知识清单、博客
- 项目、组件、库
- Node代码
- 面试必考
- 91算法
- 第八部分 工作代码总结
- 样式代码
- 框架代码
- 组件代码
- 功能代码
- 通用代码