阅完此文,Vue响应式不再话下

作者简介
小北
9年前端开发工作经验,前端leade,主要分享:前端方面技术文章
csdn:https://me.csdn.net/xllily_11
公众号:前端你别闹

vue的双向数据绑定,众所周知是基于Object.defineProperty这个在浏览器的特性api来实现的。但是怎么从视图到数据,数据到视图,这个整个大过程,对于很多盆友来说,还有点不是很清楚。

这篇文章,将会特别轻松的换个角度让你明白整个过程。just do it !!! 💃💃💃

Vue的响应式系统

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

<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//数量      },    computed:{       totlePriceWithTax(){             return this.price*this.quantity*1.03            }    }  })</script>

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

  • 更新页面显示的price的值

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

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

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

JavaScript正常的运行方式

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

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

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

>> 变化之后的总价:10

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

>> 变化之后的总价:40

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

问题

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

尝试一下

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

let total=price*quantity;

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

image

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

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

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

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

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

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

这儿,我们遍历了所有记录的内容,并且每一个都执行。

这个时候,我们的代码就可以更改一下:

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类,代码如下:

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,这个时候要运行,就只需要:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

现在我们假设每一个属性(price和quantity)都有自己内部的Dep类。

image

当我们运行watcher这个函数的时候:

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

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

image

如果这个时候还有其他的地方也在使用data.price,我们也希望可以把对应的匿名函数放到Dep类中记录下来。

image

那么,什么时候会调用price对应的Dep中的notify呢?在price赋值,值发生改变的时候。我们最后希望发生的效果是:

>> total10>> price=20>> total40

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

解决方案

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

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的值。

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

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

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

这样所有的数据都变了可监听的了。

把他们结合起来

total=data.price*data.quantity

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

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

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

切换到Dep class的模式:

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

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

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

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];//初始值  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;})

这就结合了这一块的东西,price和quantity两个属性变成了响应式的情况,可以下来试一下。

直接上架构图:

image

最后,Vue2中还有很多东西,Vue3也出来了,我们这块出了对应的课程。年轻人不要犹犹豫豫。机会和成长总在犹豫的时候就溜走了。

在这样一个信息爆炸、知识唾手可得的时代,年轻人一定要做个明白人,懂得筛选和判断优质内容。

作者简介
小北
9年前端开发工作经验,前端leade,主要分享:前端方面技术文章
csdn:https://me.csdn.net/xllily_11
公众号:前端你别闹

本文已经获得小北老师授权转发,其他人若有兴趣转载,请直接联系作者授权。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345