vue响应式编程,vue的响应式原理是怎样的
最近接触到vue.js,一度非常好奇vue.js是如何监控数据更新,重新渲染页面的。本文主要介绍如何实现Vue源代码的响应式学习的相关信息。有需要的可以参考一下。
目录
前言1。响应系统的关键要素1。如何监控数据更改2。如何收集依赖项3354实现Dep类3?如何更新3354实现Watcher class 2?虚拟DOM和diff1。什么是虚拟DOM?2.diff算法——新旧节点比较III。nextTick IV。摘要
前言
作为前端开发,我们的日常工作就是将数据渲染到页面上处理用户交互。在Vue中,当数据发生变化时,页面会重新呈现。例如,我们在页面上显示一个数字,旁边有一个点击按钮。每点击一次按钮,页面上显示的数字就会加一。如何实现这一点?
按照原生JS的逻辑想一想。我们要做三件事:监听click事件,修改事件处理程序中的数据,然后手动修改DOM进行重新渲染。这个和我们用Vue最大的区别就是多了一个步骤【手动修改DOM重新渲染】。这一步看似简单,但我们必须考虑几个问题:
需要修改哪个DOM?
每次数据变化都需要修改DOM吗?
如何保证修改DOM的性能?
因此,实现响应系统并不容易。下面我们结合Vue的源代码来学习一下Vue中的优秀思想。
一、一个响应式系统的关键要素
1、如何监听数据变化
显然,通过监控所有用户交互事件来获取数据变化是非常繁琐的,有些数据变化不一定是用户触发的。Vue如何监控数据变化?—— Object.defineProperty属性
为什么Object.defineProperty方法可以监视数据更改?这个方法可以直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性并返回这个对象。让我们先来看看它的语法:
Object.defineProperty(对象,属性,描述符)
//obj是传入的对象,prop是要定义或修改的属性,descriptor是属性描述符。
这里的核心是descriptor,它有许多可选的键值。这里我们最关心的是get和set,其中get是为一个属性提供的getter方法,当我们访问该属性时会触发;Set是为属性提供的setter方法,当我们修改属性时会触发。
简而言之,一旦一个数据对象有了一个getter和一个setter,我们就可以很容易地监控它的变化,并把它称为一个响应式对象。具体怎么做?
功能观察(数据){
if (isObject(data)) {
Object.keys(数据)。forEach(key={
defineReactive(数据,键)
})
}
}
函数defineReactive(obj,prop) {
let val=obj[prop]
let=new dep()//用于收集依赖项
Object.defineProperty(obj,prop,{
get() {
//访问对象属性,表示依赖当前对象属性,收集依赖关系。
dep.depend赖()
返回值
}
set(newVal) {
if (newVal===val)返回
//数据已被修改,该通知相关人员更新相应视图了。
val=newVal
dep.notify()
}
})
//深度监控
if (isObject(val)) {
观察(值)
}
返回对象
}
这里我们需要一个Dep类(dependency)来进行依赖收集。
Ps: PS:Object.defineProperty只能监听已有的属性,对新增加的属性无能为力。同时不能监听数组的变化(这个问题在Vue2中通过在数组原型上重写方法解决了),所以在Vue3中换成了更强大的代理。
2、如何进行依赖收集——实现 Dep 类
基于构造函数的实现:
函数Dep() {
//使用deps数组存储各种依赖关系
this.deps=[]
}
//Dep.target用于记录正在运行的watcher实例,它是一个全局唯一的Watcher。
//这是一个非常巧妙的设计,因为JS是单线程的,同时只能计算一个全局观察器
Dep.target=null
//在原型上定义依赖方法,每个实例都可以访问该方法
dep . prototype . depend=function(){
if (Dep.target) {
本部门推(部门目标)
}
}
//在原型上定义notify方法,用于通知watcher更新。
dep . prototype . notify=function(){
this.deps.forEach(watcher={
观察器.更新()
})
}
Vue中会有嵌套的逻辑,比如组件嵌套,所以用堆栈来记录嵌套的watcher。
//堆栈,先入后出
const targetStack=[]
函数pushTarget(_target) {
if(dep . target)target stack . push(dep . target)
部门目标=_目标
}
函数popTarget() {
Dep.target=targetStack.pop()
}
这里我们主要了解原型上的两个方法:depend和notify,一个用于添加依赖关系,另一个用于通知更新。当我们谈到收集“依赖项”时,this.deps数组中究竟存储了什么?e将观察器的概念设置为依赖表示,即观察器被收集在this.deps中.
3、数据变化时如何更新——实现 Watcher 类
Watcher,Vue中有三种类型,分别用于页面渲染和两个API,computed和Watch。为了区分,不同用途的观察器分别称为renderWatcher、computedWatcher和watchWatcher。
用类实现它:
类监视器{
构造函数(expOrFn) {
//这里传入的参数不是函数时,需要解析。省略了parsePath。
this . getter=type of expor fn=== function ?expOrFn : parsePath(expOrFn)
this.get()
}
//类中定义的函数不需要写成function。
get() {
//在执行时,这是watcher和Dep.target的当前实例
推送目标(this)
this.value=this.getter()
popTarget()
}
update() {
this.get()
}
}
至此,一个简单的响应式系统已经成型。综上所述:Object.defineProperty使我们能够知道谁访问了数据,什么时候更改的。Dep可以记录哪个DOM与某个数据相关,Watcher可以在数据发生变化时通知DOM进行更新。
Watcher和Dep是一个非常经典的观察者设计模式的实现。
二、虚拟 DOM 和 diff
1、虚拟 DOM 是什么?
虚拟DOM是用JS中的对象来表示真实的DOM。如果有数据变化,先改虚拟DOM,再改真实DOM。好主意!
关于虚拟DOM的优势,我还是听大的:
在我看来,虚拟DOM的真正价值从来不是性能,而是它1)打开了函数式UI编程的大门;2)它可以被呈现到除DOM之外的后端。
例如:
模板
div id=app class=container
h1HELLO WORLD!/h1
/div
/模板
//对应的vnode
{
标签:“div”,
props: { id: app ,class: container },
孩子们:{标签: h1 ,孩子们:你好,世界!}
}
我们可以这样定义它:
函数VNode(标签、数据、子节点、文本、elm) {
this.tag=标签
this.data=数据
this.childern=childern
this.text=text
This.elm=elm //对真实节点的引用
}
2、diff 算法——新旧节点对比
当数据发生变化时,会触发渲染观察器的回调来更新视图。在Vue源代码中,更新视图时使用patch方法比较新旧节点的异同。
(1)判断新旧节点是否为同一节点
函数sameVNode()
函数sameVnode(a,b) {
return a.key===b.key
)a .标签===b .标签
a.isComment===b.isComment
isDef(a.data)===isDef(b.data)
sameInputType(a,b)
)
}
(2)如果新旧节点不同
替换旧节点:创建新节点-删除旧节点。
(3)如果新旧节点相同
没有子节点,好说。
一个有子节点,一个没有,很好说,要么删除子节点,要么添加新的。
有子节点,有点复杂。执行更新子项:
函数updateChildren (parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly) {
设oldStartIdx=0
设newStartIdx=0
设oldEndIdx=oldCh.length - 1
设oldStartVnode=oldCh[0]
设oldEndVnode=oldCh[oldEndIdx]
设newEndIdx=newCh.length - 1
设newStartVnode=newCh[0]
设newendvnode=newch[newenddx]
让oldkeytoidx、idxInOld、vnodeToMove、refElm
///
while(old start idx=oldenddx news startidx=newenddx)>
///
if(isun ndef(旧开始vnode))>
旧开始vnode=旧ch[旧开始idx]/vnode已移动到左侧
} else if(isundaf(oldendvnode))>
oldendvnode=old ch[ - oldenddx]
} else if(相同vnode(旧开始vnode,新开始vnode))& gt
///
补丁程序节点(oldStartVnode、newstartvnode、insertedVnodeQueue)
旧开始vnode=旧ch[旧开始idx]
news startvnode=newch[news startidx]
} else if(samvnode(oldendvnode,newendvnode))& gt
///
补丁程序节点(oldEndVnode、newEndVnode、insertedVnodeQueue)
oldendvnode=old ch[ - oldenddx]
newendvnode=newch[ - newenddx]
} else if(samvnode(old start vnode,newendvnode)){//vnode右移
///
补丁程序节点(oldStartVnode、newEndVnode、insertedVnodeQueue)
可以移动节点操作。在(parent elm、oldStartVnode.elm、nodeops)之前插入。nextsibling(oldendvnode。榆树))
旧开始vnode=旧ch[旧开始idx]
newendvnode=newch[ - newenddx]
} else if(samvnode(oldendvnode,newstartvnode)){//vnode左移
///
补丁程序节点(oldEndVnode、newStartVnode、insertedVnodeQueue)
可以移动节点操作。在(父elm、oldEndVnode.elm、oldStartVnode.elm)之前插入
oldendvnode=old ch[ - oldenddx]
news startvnode=newch[news startidx]
}否则
///
///
if(old keytodx))old keytodx=createkeytool dix(old ch、oldStartIdx、oldendidx)
idxinold=isdef(news tartvnode。关键)
?旧keytodx[新闻s tartvnode。关键]
:findIdxInOld(newStartVnode、oldCh、oldStartIdx、oldendidx)
///
///
if(idxinold)){//新项目
createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newstartidx)
}否则
vnodeToMove=oldCh[idxInOld]
if(samvnode(vnodetomove,news tartvnode))]
补丁程序节点(vnodetomove、newStartVnode、insertedVnodeQueue)
奥德奇(idxinold)=未定义
可以移动节点操作。插入之前(父elm,vnodeToMove.elm,oldStartVnode.elm)
}否则
//相同的键但不同的元素。视为新元素
createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newstartidx)
}
}
news startvnode=newch[news startidx]
}
}
if(old startidx oldenddx)>
ref elm=ISU ndef(newenddx 1)?null:newch[newenddx 1]。榆树
addvnnodes(父elm、refElm、newCh、newstartidx、newendidx、insertedVnodeQueue)
} else if(news startidx new enddx)>
removevnnodes(父elm、oldCh、oldStartIdx、oldendidx)
}
}
这里的主要逻辑是:将新节点的头尾与旧节点的头尾进行比较,看是否是同一个节点,如果是,patchVnode直接;否则,使用一个Map来存储旧节点的键,然后遍历新节点的键,看它们是否存在于旧节点中,重用相同的键;这里时间复杂度是O(n),空间复杂度也是O(n)。用空间换时间~
Diff算法主要是为了减少更新量,寻找差异最小的部分DOM,只更新差异部分。
三、nextTick
所谓nextTick,也就是下一个Tick,tick是什么?
我们知道JS执行是单线程的,它对异步逻辑的处理是基于事件循环的,主要分为以下几个步骤:
所有同步任务都在主线程上执行,形成执行上下文栈;
主线外还有一个‘任务队列’。只要异步任务有运行结果,就在‘任务队列’中放一个事件;
一旦“执行堆栈”中的所有同步任务都已执行,系统将读取“任务队列”以查看其中有哪些事件。那些对应的异步任务,然后结束等待状态,进入执行栈,开始执行;
主线程不断重复上面的第三步。
主线程的执行过程是一个tick,所有异步结果都通过“任务队列”进行调度。在消息队列中,存储一个任务。根据规范,任务分为两类,即宏任务和微任务,每个宏任务完成后,所有微任务都要被清除。
for(宏任务队列的宏任务){
//1.处理当前宏任务
handleMacroTask()
//2.处理所有微任务
for(微任务队列的微任务){
微任务处理
}
}
在浏览器环境中,常见的宏任务包括setTimeout、MessageChannel、postMessage、setImmediate、setInterval;常见的微任务包括变异观察和承诺然后
我们知道数据的改变到DOM的重新渲染是一个异步的过程,这将在下一个tick中发生。比如在开发过程中,当我们从服务器接口获取数据时,数据被修改。如果我们的一些方法依赖于数据修改后的DOM变化,我们必须在nextTick后执行它。例如,下面的伪代码:
getData(res)。然后(()={
this.xxx=res.data
这个。$nextTick(()={//这里我们可以获取更改后的DOM})
})
四、总结
关于如何实现Vue源代码的响应式学习的这篇文章到此为止。有关更多相关的Vue响应式实施,请搜索我们以前的文章或继续浏览下面的相关文章。希望大家以后能多多支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。