Vue的响应式系统

我们第一次使用Vue的时候,会感觉有些神奇,举个例子:

  1. <div id="app"> <div>价格:¥{{price}}</div> <div>总价:¥{{price*quantity}}</div> <div>折扣后:¥{{totlePriceWithTax}}</div></div><script> var vm=new Vue({ el:'#app', data:(){ price:5.00,//单价 quantity:2//数量 },
  2. computed:{ totlePriceWithTax(){ return this.price*this.quantity*1.03 } } })</script>

我们使用vue的时候,不知道它内部做了什么。它都能知道price这个字段的值是否发生过变化,如果发生了变化,他会做如下几件事:

  • 更新页面显示的price的值

  • 重新计算总价的乘法表达式并且更新显示结果

  • 重新调用totlePriceWithTax函数,并且更新显示

这儿,咱们就有一个疑问,vue怎么就知道price变化了之后,都要更新哪些值呢?为什么,每次一变化,就要更新呢?如何跟踪的呢?

JavaScript正常的运行方式

我们把这个例子整理成我们正常的JavaScript程序来看看:

  1. let price=5;let quantity=2;let total=price*quantity;//计算总价pice=20;//price字段发生变更之后console.log(`变化之后的总价:${total}`);

这个会输出打印多少呢?因为我这儿没有使用Vue,很明显,这儿会输出10:

  1. >> 变化之后的总价:10

在咱们经常使用的Vue中,我们想要在price或者quantity这两个字段更新时,和它有关的表达式也会更新,和它有关的函数也会执行。

  1. >> 变化之后的总价:40

但是,javascript是过程性的,不是响应式的,所以这个代码在实际运行的时候是不行的。为了让total在price更新的时候,它也跟着更新,我们必须让JavaScript语言具备不同的运行方式。

问题

那么我们现在就遇到了一个问题,怎么样,才能在price字段或者quantity更新的时候,total也重新更新显示呢?

尝试一下

首先,我们需要明白price和totle的关联是:

  1. let total=price*quantity;

那么,在price更新之后,需要重新得到新的total,就需要重新执行这个方法。那么就需要有一个地方把这个方法储存起来,在price变更的时候,重新运行储存起来的方法,这样total值就更新了。

那我们就来尝试一下,把函数记录下来,后面变更的时候,再次运行。

  1. let price=5;let quantity=2;let total=0;let target=null;//记录函数target=()=>{ total=price*quantity;}record();//后面讲解,记住这个我们后面想要运行的函数target();//同时,我们执行一遍这个方法

record记录函数的实现就很简单了:

  1. let storage=[];//这是要记录函数的地方,就是上面图中椭圆的那个东西//记录方法的实现,这个时候的target就是我们要记录的方法function record(){ storage.push(target)}

这一步,将target储存了起来,这样我们后面就可以运行它。这个时候,我们就需要一个运行所有记录的内容的函数。那我们就来搞一哈:

  1. function replay(){ storage.forEach((run)=>{ run(); })}

这儿,我们遍历了所有记录的内容,并且每一个都执行。这个时候,我们的代码就可以更改一下:

  1. let price=5;let quantity=2;let total=0;let target=null;
    function record(){ storage.push(target)}
    function replay(){ storage.forEach((run)=>{ run(); })}
    target=()=>{ total=price*quantity;}record();target();
    console.log(total)// 10price=20;replay();console.log(total)//40

这样我们就实现了,一个记录的过程,但是这样没有一个很好地管理,我们能不能把记录这块的内容,维护成一个类,让这个类维护一个tagert列表,每次需要重新运行的时候,这个类都会得到通知。年轻人,火力旺,说干就干。维护一个单独的Dep类,

代码如下:

  1. class Dep{ constructor(){ this.subscribers=[];//维护所有target的列表,在得到通知的时候,全部都会运行 } depend(){ if(target&&!this.subscribers.includes(target)){ //只有target有方法,并且没有被记录过 this.subscribers.push(target); } }
    notify(){
    this.subscribers.forEach((sub)=>{ sub(); }) }}

在这个类中,我们不再使用storage,使用subscribers这个字符来记录target函数的内容,也不再使用record,使用depend,也用了notify替代了replay,这个时候要运行,就只需要:

  1. const dep=new Dep();
    let price=5;let quantity=2;let total=0;let target=null;
    target=()=>{ total=price*quantity;}dep.depend();//记录到subscribers中target();
    console.log(total)// 10price=20;dep.notify();//遍历执行所有target,分发内容console.log(total)//40

这样,整体的过程就会好一点,但是还是会显得很冗余,如果能过把匿名函数创建,观察,更新的这些行为封装起来,那就更好了。

年轻人,总是冲动,咱们说干就干。把原来的创建和记录:

  1. target=()=>{ total=price*quantity;}dep.depend();//记录到subscribers中target();

这块内容封装起来,咱们给封装起来的函数起名叫做watcher,封装起来之后,我们就只需要这样调用:

  1. watcher(()=>{ total=price*quantity})

那我们在实现watcher的时候,这么做就好:

  1. function watcher(myFunc){ target=myFunc;//传入的函数赋值 dep.depend();//收集 target();//执行一下 target=null;//重置}

这儿,咱们看到watcher函数接受了一个变量myFunc,这个myFunc后面接收的是匿名函数,然后赋值给target属性,调用dep.depend(),将以订阅者的形式添加target到记录的地方,然后调用target,并且重置。

现在结合上面的代码咱们尝试一下这个代码:

  1. price=20;console.log(total);dep.notify();console.log(total);

这里面有一个问题,就是target为什么要设置成全局变量,而不是将其传递给需要的函数。咱们后面会细聊。

现在我们有一个Dep类了,但是我们整整想要实现的情况是,每一个变量都有响应的地方记录它关联的变更,每个变量都有自己的Dep。这个可咋整?

年轻人,不怕事,说干就干。咱们首先把所有的变量放到一起:

  1. let data={ price:5, quantity:2}

现在我们假设每一个属性(price和quantity)都有自己内部的Dep类。当我们运行watcher这个函数的时候:

  1. wacther(()=>{ total=data.price*data.quantity})

因为我们是使用到了data.price的值,那么我们希望price属性的Dep类可以将使用它的匿名函数(储存在target上)放在订阅数组中,记录下来(通过调用dep.depend())。同时data.quantity这个变量也被访问了,所以也希望能够被记录下来,放在对应的订阅数组中:

如果这个时候还有其他的地方也在使用data.price,我们也希望可以把对应的匿名函数放到Dep类中记录下来。那么,什么时候会调用price对应的Dep中的notify呢?在price赋值,值发生改变的时候。我们最后希望发生的效果是:

  1. >> total10>> price=20>> total40

我们希望,当数据被访问的时候,能够把对应的target匿名函数储存到订阅数组中,当属性变更的时候,能够运行对应的储存在订阅数组中的匿名函数。

解决方案

这个一眼看过去,访问时,改变时。脑海中直接就出来了Object.defineProperty,这个允许我们为属性定义getter和setter函数。在展示如何和Dep结合的之前,先看下用法:

  1. let data={price:5,quantity:2};Object.defineProperty(data,'price',{ get(){ console.log('被访问') }, set(newVal){ console.log('被修改') }});data.price;//输出:被访问data.price=20;//输出:被修改

这里,我们并没有实际的修改get和set的值,因为功能被覆盖了。现在,我们希望get的时候能够返回一个值,set的时候能够更新值。所以我们先添加一个变量internalValue来储存当前的price的值。

  1. let data={price:5,quantity:2};
    let internalValue=data.price;//初始值
    Object.defineProperty(data,'price',{ get(){ console.log('被访问'); return internalValue }, set(newVal){ console.log('被修改'); internalValue=newVal }});total=data.price*data.quantity;//调用getdata.price=20;//调用set

这样我们就可以把所有我们想要的监听的数据,全部给处理一下:

  1. let data={price:5,quantity:2};
    Object.keys(data).forEach((key)=>{ let internalValue=data[key];//初始值
    Object.defineProperty(data,key,{ get(){ console.log('被访问'); return internalValue }, set(newVal){ console.log('被修改'); internalValue=newVal } });})
    total=data.price*data.quantity;//调用getdata.price=20;//调用set

这样所有的数据都变了可监听的了。把他们结合起来

  1. total=data.price*data.quantity

当这个代码运行的时候,会触发price属性对应的get方法,我们希望price的Dep可以记住这个对应的匿名函数(target)。通过这个方式,如果发生改变,触发了set,那么就能够调用这个属性对应的储存起来的匿名函数。

  • Get—记住匿名函数,当值发生变化的时候重新运行。

  • Set—运行保存的匿名函数,对应匿名函数绑定的值就会发生变化

切换到Dep class的模式:

  • price被访问时—调用dep.depend保存当前target

  • price被改变时—调用price的dep.notify,重新运行所有的target

最后,我们就把这个结合起来,年轻人,不要磨磨蹭蹭,突突两下就可以了:

  1. let data={price:5,quantity:2};let target=null;class Dep{ constructor(){ this.subscribers=[];//维护所有target的列表,在得到通知的时候,全部都会运行 } depend(){ if(target&&!this.subscribers.includes(target)){
    //只有target有方法,并且没有被记录过
    this.subscribers.push(target); } } notify(){ this.subscribers.forEach((sub)=>{ sub(); }) }}Object.keys(data).forEach((key)=>{  let internalValue=data[key];
  2. //初始值 Object.defineProperty(data,key,{ get(){ console.log('被访问'); dep.depend();//添加对应的匿名函数target return internalValue }, set(newVal){ console.log('被修改'); internalValue=newVal;
    dep.notify();//触发对应的储存的函数
    } });})function watcher(myFunc){ target=myFunc;//传入的函数赋值 target();//执行一下 target=null;//重置}watcher(()=>{ data.total=data.price*data.quantity;})

Vue的响应式系统的更多相关文章

  1. 你是如何理解Vue的响应式系统的

    1.响应式系统简述: 任何一个 Vue Component 都有一个与之对应的 Watcher 实例. Vue 的 data 上的属性会被添加 getter 和 setter 属性. 当 Vue Co ...

  2. 前端必读:Vue响应式系统大PK

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://www.sitepoint.com/vue-3-reactivity-system ...

  3. vue原理探索--响应式系统

    Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」. 首先看一下 Object.defi ...

  4. Vue 及框架响应式系统原理

    个人bolg地址 全局概览 Vue运行内部运行机制 总览图: 初始化及挂载 在 new Vue()之后. Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期. ...

  5. 【js】vue 2.5.1 源码学习 (七) 初始化之 initState 响应式系统基本思路

    大体思路(六) 本节内容: 一.生命周期的钩子函数的实现 ==> callHook(vm , 'beforeCreate') beforeCreate 实例创建之后 事件数据还未创建 二.初始化 ...

  6. 前端必读:Vue响应式系统大PK(下)

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://www.sitepoint.com/vue-3-reactivity-system ...

  7. vue深入响应式原理

    vue深入响应式原理 深入响应式原理 — Vue.jshttps://cn.vuejs.org/v2/guide/reactivity.html 注意:这里说的响应式不是bootsharp那种前端UI ...

  8. Vue 数据响应式原理

    Vue 数据响应式原理 Vue.js 的核心包括一套“响应式系统”.“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码.例如,视图渲染中使用了数据,数据改变后,视图也会自动更新. 举个简单 ...

  9. Vuejs - 深入浅出响应式系统

    Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 Javascript 对象.而当你修改它们时,视图会进行更新.这使得状态管理非常简单直接,不过理解其工作原理同样非常重要,这样 ...

随机推荐

  1. DSP未来五大发展趋势

    在4G无线通信领域,数据吞吐量已经达到了3G时代的100到1000倍;在多媒体处理器领域,每天都有新的标准涌现出来;在更多其它DSP应用领域,密集的数据处理器需求不断给处理器设计团队带来新的挑战.随着 ...

  2. 【Linux】一步一步学Linux——初识Linux命令解析器(10)

    目录 00. 目录 01. Shell简介 02. Shell分类 03. 交互式shell和非交互式shell 04. 登录shell和非登录shell 05. Shell类型 06. 参考 00. ...

  3. 《Docker Deep Dive》Note - Docker 引擎

    <Docker Deep Dive>Note Docker 引擎 1. 概览 graph TB A(Docker client) --- B(daemon) subgraph Docker ...

  4. IP核——RAM

    一.Quartus 1.打开Quartus ii,点击Tools---MegaWizard Plug-In Manager 2.弹出创建页面,选择Creat a new custom megafunc ...

  5. Linux守护进程编写指南

    Linux守护进程编写指南 守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.守护进程是一种很有用的进 程.Linux的大多数服务器 ...

  6. Python2与Python3兼容

    Python2与Python3兼容 python3写的代码如何也能在pyhon2上跑?请无论如何加上这一句,python3没有啥影响 from __future__ import absolute_i ...

  7. easyui-datagrid清空表中原有数据

    $('#dg').datagrid('loadData', { total: 0, rows: [] });

  8. springboot笔记06——使用Thymeleaf模板引擎

    前言 Springboot 推荐使用Thymeleaf做视图层.Thymeleaf支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式.浏览器解释 html 时会忽略 ...

  9. AJAX 初识

    AJAX全称为 Asynchronous Javasript And XML,是在浏览器端进行网络编程(发送请求,接收响应)的技术方案.AJAX 也就是浏览器提供的一套API,可以供 Javascri ...

  10. Js保存图片到本地

    注:此方法是使用hbuilderx云打包之后才能用,否则在浏览器中会报 plus is not defined 官方文档 http://www.html5plus.org/doc/zh_cn/gall ...