vue原理:diff、模板编译、渲染过程等
一、虚拟DOM:
因为DOM操作非常消耗性能,在操作DOM时,会出现DOM的回流(Reflow:元素大小或者位置发生改变)与重绘(元素样式的改变)使DOM重新渲染。
现在的框架Vue和React很少直接操作DOM,因为两者都是数据驱动视图,只会对数据进行增删改的操作
因此,二者使用虚拟DOM(vdom)来解决控制DOM操作的问题:
原理:使用Js模拟DOM结构,把DOM的计算转移为Js的计算,使用diff算法计算出最小的变更,然后根据变更操作DOM
学习diff算法需要借助snabbdom这个vdom库的源码,vue也是参考它实现的
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom"; const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]); const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]); // Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode); const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
); // Second ` patch ` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
其中有两个关键函数:
- h 函数返回一个vnode,他是使用js对象表示的虚拟DOM结构。接收 sel(选择器)、data(对DOM的js描述)、children(这个虚拟DOM的子vnode元素)
- patch 函数的作用:一是将vnode渲染为真实的DOM挂载至页面;二是使用diff算法比对两个vnode的不同,然后重新渲染
二、Diff算法:
- 概述:
diff 比对两个新旧vnode的过程主要是在 patch 函数(patchVnode函数)中进行
正常情况下两棵树之间作比对,那么第一遍历tree1,第二遍历tree2,第三排序,三次遍历,时间复杂度为 O(n ^ 3)节点太多,算法就不可用
框架中diff算法的优化:
- 同级比对,不跨级
- tag不相同,则直接删除重建,不再深度比对(有可能tag不相同但是tag下面的子元素还是相同的,但是我们不管了,只要tag不相同就删掉,因为深度比较复杂度太高)
- tag和key,两者都相同,则认为是相同节点,不再深度比对,时间复杂度优化至 O(n)
- 生成vnode:
h 函数用来生成vnode,vnode函数如下:
返回一个js对象结构的虚拟DOM(vnode):
1.children和text是不能共存的,要么里面是纯text文本,要么是子元素
2.elm 就是vnode对应的那个DOM元素
3.key 就相当于 v-for 里面的 key,是我们在使用 v-for 的时候需要自己手动加上
- patch函数:
初始化:第一次执行patch,patch(container,vnode),创建空的vnode,关联传入的dom
更新:判断是否是相同的vnode,tag(sel选择器)和key相同,则认为是相同节点,执行patchVnode函数进行比对
否则删除重建,不做深度比对
- patchVnode函数(比对):
- 如果新Vnode有 children,没有 text(vnode.text === undefined)(text和children不能同时存在)
- 如果新旧vnode都有 children ,调用 updateChildren() ,再继续进行更新
- 如果新vnode有 children ,旧vnode无 children,调用 addVnodes() 添加 children 到 elm 上
- 如果新vnode无 children ,旧vnode有 children,调用 removeVnodes() 移除 children
- 如果新旧vnode都无 childre且旧vnode有 text,则把elm的 text 设置为空
- 如果新Vnode没有 children,只有 text且值也不同,就移除旧vnode的children
- updateChildren函数:
原理:
针对新旧 children
定义四个index, oldStartIdx
, oldEndIdx
, newStartIdx
, newEndIdx
,然后进行一个循环,在循环过程中
idx会一边累加或者一边累减,startIdx会累加,endIdx会累减,在这个过程中,指针会慢慢地往中间去移动,当指针重合的时候,说明遍历结束了,循环结束。
在每一轮循环过程中的具体的对比过程是:
如果出现下面情况中的一种:开始和开始节点去对比,结束和结束节点对比,结束和开始节点对比,那么就执行 patchVnode()
函数,进行递归比较,
并且指针累加或者累减,往中间移动。 进行下一轮循环的时候,指针就指到下一个了 children
key
。- 如果没有对应上,说明这个节点是新的,找个地方插入进去新的就好。
- 如果对应上了,还要判断
sel
是否相等,如果sel
不相等,那还是没对应上,说明节点是新的,那也找地方插入新的。 - 如果
sel
相等,key
相等,那么继续对这两个相同的节点执行patchVnode
方法,递归比较。
- v-for中key的作用
- 如果不使用 key ,diff 算法中 没有 key值能够对应上,会认为节点更新,之后会销毁对应的vnode,重新渲染元素
如果检测出新节点中的 key 在旧节点上有对应的 key ,在进行交换位置的操作时,就没有必要销毁,由此提升性能
- key值需要唯一值,如果使用 index,例如在一个 li 数组中 头部插入某些dom元素,index值递增了,但对应的内容却错误了
三、模板编译
零、前置知识点:JS的 with 语法
with语法:改变 {} 内自由变量的查找规则,,将 {} 内自由变量,当作 obj 的属性来查找
如果找不到匹配的 obj 属性,就会报错
with 要慎用, 它打破了作用域的规则,易读性变差
vue模板编译成什么?
模板不是html , 有指令、插值、JS 表达式,能实现判断、循环
html是标签语言,只有JS才能实现判断、循环(图灵完备的)
因此,模板一定是转换为某种JS代码,模板怎么转成js代码的过程就是模板编译
安装 vue template complier 这个库,查看编译输出值:
// 引入
const compiler = require('vue-template-compiler')
// 插值
// const template = ` <p>{{message}}</p> `
// 编译
const res = compiler.compile(template)
console.log(res.render)
打印结果:
with(this){return _c('p',[_v(_s(message))])}
其中 this 在vue中就是 vm 实例,所以 _c、_v、_s 就是vue源码中的一些函数
// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {
target._c = createElement//创建vnode
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
转化后:createElement 函数的作用是创建一个 vnode
with(this){return createElement('p',[createTextVNode(toString(message))])}
- 表达式编译:表达式会转变为js代码,然后把结果放到vnode里面去
const template = ` <p>{{flag ? message : 'no message found'}}</p> `
with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
- 动态属性:同理
const template = `
<div id="div1" class="container">
<img :src="imgUrl"/>
</div>
` with(this){return _c('div',
{staticClass:"container",attrs:{"id":"div1"}},
[_c('img',{attrs:{"src":imgUrl}})])
}
- 条件:使用三元表达式来创建不同的vnode节点
// 条件
const template = `
<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>
`
with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
- 循环:通过
_l
(renderList
)函数,传入数组或者对象,即可返回列表vnode
//循环
const template = `
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
`
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
- 事件:on属性包含所有的事件绑定
//事件
const template = `
<button @click="clickHandler">submit</button>
`
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
- v-model:原理就是
value
的attr
加input
事件监听的语法糖 最后执行render
函数,生成vnode
//v-model
const template = ` <input type="text" v-model="name"> `
//主要看 input 事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
- 总结
模板编译的过程:模板编译为render函数,执行render函数后返回vnode
之后再基于 vnode 执行 patch 和 diff 算法
注意:使用webpack,vue-loader,会在开发环境编译模板,所以最后打包出来产生的代码就没有模板代码,全部都是 render 函数形式
- render 函数:在vue组件中可以使用 render 代替 template,在某些复杂情况下,可以考虑使用render
四、初次渲染与更新过程
- 初次渲染:
- 首先解析模板为 render 函数(模板编译)
- 触发响应式,监听data属性,设置getter、setter
- 执行 render 函数,生成 vnode,patch(elm,vnode)
- 更新过程:
- 修改 data,触发 setter(此前在getter中已被监听)
- 重新执行 render 函数,生成newVnode
- patch(vnode,newVnode) (diff算法)
五、异步渲染--this$nextTick()
vue组件是异步渲染的。代码没执行完,DOM不会立即渲染。this.$nextTick 会在DOM渲染完成时回调
页面渲染时会将 data 的修改做一个整合,多次 data 的修改 最后只会渲染一个最终值
六、组件化
- MVC模式:单向绑定,即Model绑定到View,使用JS代码更新Model时,View就会自动更新
- MVVM模式:双向绑定,实现了View的变动,自动反映在VM,反之亦然。
对于双向绑定的理解,就是用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定。
再说细点,就是在单向绑定的基础上给可输入元素input、textare等添加了change(input)事件,(change事件触发,View的状态就被更新了)来动态修改model。
- MVC与MVVM的区别
MVC和MVVM的区别并不是VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller
其它视图操作业务等还是应该放在Controller中实现。也就是说MVVM实现的是业务逻辑组件的重用。
MVC中Controller演变成MVVM中的ViewModel
MVVM通过数据来显示视图层而不是节点操作
MVVM主要解决了MVC中大量的dom操作使页面渲染性能降低,加载速度变慢,影响用户体验等问题。
七、响应式
- vue2:object.defineProperty()
- vue3:proxy
引用:
https://www.shouxicto.com/article/3298.html
https://juejin.cn/post/6995232345749979172#heading-2
https://juejin.cn/post/6995204870114377741
https://blog.csdn.net/gxll499294075/article/details/123667632
vue原理:diff、模板编译、渲染过程等的更多相关文章
- [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串
本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...
- [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST
本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...
- Vue是如何渲染页面的,渲染过程以及原理代码
Vue是如何渲染页面的,渲染过程以及原理代码:https://www.cnblogs.com/ypinchina/p/7238402.html
- Vue学习笔记:编译过程
碰到是否有template选项时,会询问是否要对template进行编译: 在template编译(渲染成UI)有一个过程.模板通过编译生成AST,再由AST生成Vue的渲染函数,渲染函数结合数据生成 ...
- Vue.js双向绑定的实现原理和模板引擎实现原理(##########################################)
Vue.js双向绑定的实现原理 解析 神奇的 Object.defineProperty 这个方法了不起啊..vue.js和avalon.js 都是通过它实现双向绑定的..而且Object.obser ...
- vue模板编译
Vue 的模板编译是在 $mount 的过程中进行的,在 $mount 的时候执行了 compile 方法来将 template 里的内容转换成真正的 HTML 代码. complie 最终生成 re ...
- 详解vue的diff算法原理
我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时,vue ...
- vue 的模板编译—ast(抽象语法树) 详解与实现
首先AST是什么? 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言 ...
- 解决vue数据渲染过程中的闪动问题
关键代码 主要解决vue双大括号{{}}在数据渲染和加载过程中的闪动问题,而影响客服体验. html代码: <span class="tableTitle selftab" ...
- 二、Vue 页面渲染过程
前言 上篇博文我们依葫芦画瓢已经将hello world 展现在界面上啦,但是是不是感觉新虚虚的,总觉得这么多文件,项目怎么就启动起来了呢?怎么访问到8080 端口就能进入到我们的首页呢.整个的流程是 ...
随机推荐
- JUC学习笔记——共享模型之不可变
JUC学习笔记--共享模型之不可变 在本系列内容中我们会对JUC做一个系统的学习,本片将会介绍JUC的不可变内容 我们会分为以下几部分进行介绍: 不可变案例 不可变设计 模式之享元 原理之final ...
- vulnhub靶场之DEATHNOTE: 1
准备: 攻击机:虚拟机kali.本机win10. 靶机:DEATHNOTE: 1,网段地址我这里设置的桥接,所以与本机电脑在同一网段,下载地址:https://download.vulnhub.com ...
- 云原生之旅 - 12)使用 Kaniko 在 Kubernetes上构建 Docker 容器镜像
前言 前一篇文章[云原生之旅 - 11)基于 Kubernetes 动态伸缩 Jenkins Build Agents]有讲到在 Kubernetes Pod (Jenkins build agent ...
- 硬核!Apache Hudi Schema演变深度分析与应用
1.场景需求 在医疗场景下,涉及到的业务库有几十个,可能有上万张表要做实时入湖,其中还有某些库的表结构修改操作是通过业务人员在网页手工实现,自由度较高,导致整体上存在非常多的新增列,删除列,改列名的情 ...
- 【Shell案例】【awk、grep、sort、uniq】10、第二列是否有重复
给定一个 nowcoder.txt文件,其中有3列信息,如下实例,编写一个shell脚本来检查文件第二列是否有重复,且有几个重复,并提取出重复的行的第二列信息:实例:20201001 python 9 ...
- 详解 Redis 中 big keys 发现和解决
在使用 Redis 时,可能会出现请求响应慢.网络卡顿.数据丢失的情况.排查问题的时候,发现是 big keys 的问题. 什么是 big keys 在 Redis 中,一个字符串类型最大可以达到 5 ...
- 一图看懂Hadoop中的MapReduce与Spark的区别:从单机数据系统到分布式数据系统经历了哪些?
今日博主思考了一个问题:Hadoop中的MapReduce与Spark他们之间到底有什么关系? 直到我看到了下面这张图 废话不多说先上图 我们知道,单机数据系统,在本地主机上针对数据有单机本地存储操作 ...
- vba + ado +sql 连接数据库的常用操作方式
vba + ado +sql 连接Access.MySQL.Oracle Private Sub Connection_DBA() '********************************* ...
- 七个步骤覆盖 API 接口测试
接口测试作为最常用的集成测试方法的一部分,通过直接调用被测试的接口来确定系统在功能性.可靠性.安全性和性能方面是否能达到预期,有些情况是功能测试无法覆盖的,所以接口测试是非常必要的.首先需要对接口测试 ...
- RedisTemplate设置redis的key时出现\xac\xed\x00\x05t\x00\x0f前缀
1.问题描述 使用redisTemplate设置redis的key-value,程序运行没有问题,但是却在redis客户端查不到设置的key-value. 2.产生原因 出现这种乱码前缀的原因是没有进 ...