vue响应式编程,vue的响应式原理是怎样的

  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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: