部分转自:https://www.vue-js.com/topic/6129d7d661c8f900316ae37a

1 简介

  Vue采用MVVM(数据驱动视图)的模式,去充当MVVM中的VM层,在数据层与视图层间做双向响应,使前端开发可以通过只改变数据,进而改变视图,其中用到的原理便是JS新提供原生的API——Object.defineProperty去实现双向数据绑定

2 Object.defineProperty(双向数据绑定)

  https://www.cnblogs.com/jthr/p/16397673.html

  我们正式封装一个这样的函数去劫持数据,这个函数可以对对象的属性进行劫持,当使用这个这个属性值的时候,进入defineProperty的get方法里并得到return回来的返回值,改变这个属性值的时候,进入到了set方法里

 /**
* 封装一个简单的object.definePropoty实现数据的劫持
* data:对象
* key:属性
* value:值
*/
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
get() {
console.log("进入到了get方法里");
return value
},
set(newVal) {
console.log("进入到了set方法里");
if (value === newVal) return
value = newVal
}
})
} // 定义一个普通的对象
const obj = {
val: "1"
}
//进行劫持数据
defineReactive(obj,"val",obj.val)
//使用了属性值,进入get方法里,并且输出了return回来的返回值
console.log(obj.val)
//修改了属性值,进入set方法里
obj.val=2

  在vue中,我们知道,一个数据的更新会触发组件的更新,那么是怎么做的呢,简单,既然已经知道了有object.defineProporty这个新颖的API,并且数据的改变会进入到set方法里,那么我们可以大胆的猜想一下,在set方法里,触发了组件更新的方法,接下来,让我们继续改造这个函数

3 Vue中的依赖收集

  了解完vue的响应式原理,就让我们来接触一下Vue是怎么利用它来工作的。

  首先,我们假设一下这样的场景,我们知道数据的更新会触发set方法中的组件更新,那么是所有的data数据都会触发组件更新嘛?显然不是的,因为这样会很损失性能,我们希望只有改变被使用了并且渲染在页面上的data才会触发组件的更新,这就是依赖收集的由来。

  Vue进行依赖收集的原理参照了发布订阅模式

  在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者),而是通过消息通道(消息中间间)广播出去,让订阅改消息主题的订阅者消费到

  参照这个模式,我们来改写我们封装的代码,

    1)首先设置一个消息中间件数组Dep

    2)再将所有被使用过的数据(get的时候)收集进入了数组里

    3)在数据被更改(set的时候)时,触发组件的updata方法,这里用log数据代替。

/**
* 利用封装的劫持函数,进行最简单的依赖收集
* 依赖收集的实质是收集组件,这里用最简单的函数进行代替
* 依赖收集靠的是 发布订阅模式
* dep实际上就是一个发布订阅的中介
*/
let dep = [] //发布订阅数组——消息中间件
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
get() {
// 收集依赖
dep.push("收集依赖")
console.log(1);
return value
},
set(newVal) {
console.log(" 进入到了set方法里");
if (value === newVal) return
value = newVal
// 触发依赖,实际上就是遍历所有的dep数组
for (let i = 0; i < dep.length; i++) {
console.log(dep[i]); //输出整个dep数组,实际中是触发dep[i].updata()
}
}
})
}
// 定义一个普通的对象
const obj = {
val1: "1",
val2: "2"
}
//模拟遍历,对obj属性进行数据劫持
defineReactive(obj, "val1", obj.val1)
defineReactive(obj, "val2", obj.val2) // 收集依赖
console.log(obj);
console.log("收集依赖:"+obj.val1);
console.log("收集依赖:"+obj.val2);
obj.val2=3//触发依赖

  上面只是对依赖收集的一个模拟,那么问题来了,在实际中,vue依赖收集,到底收集了什么呢?收集的是watcher,watcher其实就是组件,那么watcher到底是如何被收集进dep数组中的呢,接下来我们继续修改我们的代码

4 封装watcher和Dep

  在实际开发中,我们dep需要收集的类型可能更为复杂,我们需要定义一个dep类来专门管理这个数据

 /**
* dep类,专门用来管理依赖
* 假设依赖是一个函数,存在window.target上
* window.target其实全局的内存空间,在这里代表watcher
*/
class Dep {
//构造函数
constructor() {
this.subs = [] //为原型对象上的属性
}
// 添加数组
addSub(sub) {
this.subs.push(sub)
}
//移除数组
removeSub(sub) {
remove(this.subs, sub)
}
//收集依赖
depend() {
//判断是否为一个watcher类型,如果是就收集watcher
if (window.target) {
console.log(window.target);
this.addSub(window.target)
}
}
//通知组件
notify() {
const subs = this.subs.slice()
console.log(subs);
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].updata()
}
}
}
//工具函数
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) return arr.splice(index, 1)
}
}

  接下来,我们继续封装,发布订阅者模式中,最终的观察者——watcher,我们希望watcher在组件被挂载的时候被创建,并且能自动被进入dep数组中,当数据被修改时,触发组件的更新,watcher就是组件!!!,其实在实际中,只有使用watcher,才会被收集进中介里,那么我们继续封装一个watcher类,来实现上面的功能:

/**
* watcher类
* watcher的本质就是一个实例对象
* 会触发get方法主动将自己添加到dep中去
* 关键问题:watcher在哪里被创造的呢?????????
*/
class watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
//判断expOrFn,也就是属性的合理性,返回一个能读取自己值得函数
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
console.log('进入watcher的get方法里了');
window.target = this //把watcher对象存进内存中
let value = this.getter.call(this.vm, this.vm) //在读一下自己的值,使其加入依赖
window.target = undefined //清空内存
return value
}
//updata方法也就是组件更新的方法
updata() {
console.log('进入watcher的到updata方法里了');
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
// 返回一个能解析对象最终值得函数
const bailRE = /[^\w.$]/
function parsePath(path) {
if (bailRE.test(path)) return
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
console.log(obj);
return obj
}
}

  这样我们就实现好了watcher类,当被watcher实例被new出来的时候,就会通过this.getter方法读取一下自己的值,这样就会触发defineProporty中的get方法,从而将自己添加到Dep中去

改装一下我们的劫持函数,使其通过封装好的dep工具类去收集和管理依赖

 /**
* 改装劫持函数
*/
function defineReactive(data, key, value) {
let dep = new Dep() //创造一个Dep实例
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
get() {
console.log(" 进入到了get方法里");
dep.depend() //收集依赖
return value
},
set(newVal) {
console.log(" 进入到了set方法里");
if (value === newVal) return
value = newVal
// 触发依赖
dep.notify()
}
})
}

  注意:Dep.depend()中做了一层判断,只有是watcher实例,才能被加入到数组中去!!!!

  通过这三个例子的对比,我们不难发现,在实际中,只有Watcher才会被当做依赖收集进入Dep中去,那么问题来了 ,watcher是什么被创建的呢,也就是什么时候,watcher才会被收集进入数组呢

    组件挂载,watch选项,computed选项中的时候,都有new Watcher的环节

  接下来我们来封装一个Observer类,用来将对象的所有的属性推入劫持函数中被劫持,这样我们就不用手动的一个个加了

 /**
* 采用Observer类来实现所有属性的检测
*/
class Observer{
constructor(value){
this.value=value
if(!Array.isArray(value)){
this.walk(value)
}
}
//将每一个属性都去劫持
walk(obj){
const keys = Object.keys(obj)
for(let i = 0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}

  这样,我们就实现了一个Vue从0到1,完整的一个变化侦测,注意:以上方法只适用于对象的情况,数组的变化侦测,我们在下一节内容中再聊

5 完整代码示例

 /**
* 模拟一下
* Vue的监听是怎么实现的
*/
//工具函数
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) return arr.splice(index, 1)
}
}
// 解析简单路径
const bailRE = /[^\w.$]/
function parsePath(path) {
if (bailRE.test(path)) return
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
console.log(obj);
return obj
}
}
/**
* 改装劫持函数
*/
function defineReactive(data, key, value) {
let dep = new Dep() //
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: true, //可配置
get() {
console.log(" 进入到了get方法里");
dep.depend() //收集依赖
return value
},
set(newVal) {
console.log(" 进入到了set方法里");
if (value === newVal) return
value = newVal
// 触发依赖
dep.notify()
}
})
}
/**
* dep类,专门用来管理依赖
* 假设依赖是一个函数,存在window.target上
* window.target其实就是相当于一个watcher对象
*/
class Dep {
//构造函数
constructor() {
this.subs = [] //为原型对象上的属性
}
// 添加数组
addSub(sub) {
this.subs.push(sub)
}
//移除数组
removeSub(sub) {
remove(this.subs, sub)
}
//收集依赖
depend() {
//判断是否为一个watcher类型,如果是就收集watcher
if (window.target) {
console.log(window.target);
this.addSub(window.target)
}
}
//通知组件
notify() {
const subs = this.subs.slice()
console.log(subs);
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].updata()
}
}
}
/**
* watcher类
* watcher的本质就是一个实例对象
* 会触发get方法主动将自己添加到dep中去
* 关键问题:watcher在哪里被创造的呢?????????
*/
class watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
//判断expOrFn,也就是属性的合理性,返回一个能读取自己值得函数
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
console.log('进入watcher的到get方法里了');
window.target = this //把watcher对象存进内存中
let value = this.getter.call(this.vm, this.vm) //在读一下自己的值,使其加入依赖
window.target = undefined //清空内存
return value
}
updata() {
console.log('进入watcher的到updata方法里了');
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* 采用Observer类来实现所有属性的检测
*/
class Observer{
constructor(value){
this.value=value
if(!Array.isArray(value)){
this.walk(value)
}
}
//将每一个属性都去劫持
walk(obj){
const keys = Object.keys(obj)
for(let i = 0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
} // 定义一个普通的对象
const vue = {
val1: "1",
val2: "2"
}
new Observer(vue)//实现全数据的劫持 console.log(vue.val1);
vue.val1=3//触发依赖
// obj.val2=3//触发依赖

  到现在,我们就完成了Vue2对对象的一个变化的侦测,在这里,我们接触到了一些开发中听不到的名词,以及对vue数据的一个变化过程有了更深的理解,Vue不是变魔术,它的一切都是有迹可循的,这样就可以在日常的开发中,更精准的确认到一些隐晦难明的BUG

6 vue数据监测的一些注意细节

6.1 Vue会监视data中所有层次的数据

  通过setter实现监视,且要在new Vue时就传入要监测的数据。

  (1)对象中后追加的属性,Vue默认不做响应式处理;

  (2)若需给后添加的属性做响应式,如下两个API都可以实现:

    Vue.set(target,propertyName/index,value)

    vm.$set(target,propertyName/index,value)

    注意,这两个函数不能给vm和vm的根数据对象添加属性

6.2 如何监测数组中的数据

  在Vue中修改数组中的某个元素一定要使用如下方法:

    1)使用这些API:push(),pop(),shift(),unshift(),splice(),sort(),reverse();

    2)Vue.set()或vm.$set();
 

  VUE通过封装包裹数组更新元素的方法实现,我们在调用上面这些方法的时候,不是调用的原生的,而是vue给我们的同名方法,vue在同名方法中做了两件事:

    1)调用原生对应的方法对数组进行更新;

    2)重新解析模板。进行页面更新;

6.3 用一些例子来证明下上面的这些注意细节

1)示例 对象的监测

<body>

    <script type="text/javascript" src="../js/vue.js"></script>

    <div id="root">
<h1 >{{person}}</h1> </div> <script type="text/javascript">
Vue.config.productionTip = false const vm = new Vue({
el: '#root',
data: {
person:{
name:'张三'
}
}
}) </script> </body>

初始化时页面如下

执行命令 vm.serson.sex='女',为person对象添加属性sex,发现页面没有变化,而person对象虽然youlesex属性,但是sex属性没有getter和setter函数

执行命令 Vue.set(vm.person,'age',13) 为person添加age属性,发现页面变化,person对象有age属性,且age属性有setter和getter函数

执行命令 Vue.set(vm,'address',13) ,想给vm对象添加address属性,发现出错,说明不能给vm添加属性

执行命令 Vue.set(vm._data,'address',13) ,想给vm的根数据对象_data添加address属性,发现出错,说明不能给vm的根数据对象添加属性

2)示例 数组中的数据的监测

<body>

    <title>更新时的一个问题</title>
<script type="text/javascript" src="../js/vue.js"></script> <div id="root">
<h2>人员列表</h2>
<button @click="updateMei">更新马冬梅的信息</button>
<ul>
<li v-for="(p,index) of persons" :key="p.id">
{{p.name}}-{{p.age}}-{{p.sex}}
</li>
</ul>
</div> <script type="text/javascript">
Vue.config.productionTip = false const vm = new Vue({
el: '#root',
data: {
persons: [
{ id: '001', name: '马冬梅', age: 30, sex: '女' },
{ id: '002', name: '周冬雨', age: 31, sex: '女' },
{ id: '003', name: '周杰伦', age: 18, sex: '男' },
{ id: '004', name: '温兆伦', age: 19, sex: '男' }
]
},
methods: {
updateMei() {
this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'} //不奏效
// this.persons[0].name = '马老师' //奏效
//this.persons.splice(0, 1, { id: '001', name: '马老师', age: 50, sex: '男' })
}
}
})
</script> </body>

上面代码中,我们在updateMei方法尝试3种修改数组中数据的方法

  1、this.persons[0] = {id:'001',name:'马老师',age:50,sex:'男'}

      这样修改,发现数据变了,但是页面没有同步

  2、this.persons[0].name = '马老师'

      这样修改,发现数据变了,页面也同步改变

  3、this.persons.splice(0, 1, { id: '001', name: '马老师', age: 50, sex: '男' })

      这样修改,发现数据变了,页面也同步改变

  我们看一下初始时,vm._data,发现vue为person添加了setter和getter函数,但是没有为数组里面的元素添加setter和getter函数,但是呢,数组元素为对象的话,vue又为数组元素对象的属性添加了setter和getter函数

  

  上面的三种写法中所达到的不同的效果,就可以解释1和2了

    1直接更换数组的元素,不会触发setter和getter函数,所以页面没有响应

    2修改数组对象的name属性,会触发name的setter函数

  第3种情况该如何解释呢,上面说了,vue对数组的7个方法进行了包裹,在调用这7个方法的时候,实际上调用的是vue给的同名方法,在方法中,它去调用原生方法更新数据,再重新解析模板,进行页面更新。

  如下图,证明了我们调用的是vue的同名方法而不是原生方法。

VUE16 检测数据变化的原理的更多相关文章

  1. Vue学习之--------列表排序(ffilter、sort、indexOf方法的使用)、Vue检测数据变化的原理(2022/7/15)

    文章目录 1.列表排序 1.1 .代码实例 1.2 .测试效果 1.3.需要掌握的前提知识 2.Vue监测数据变化的原理 2.1.代码实例 2.2 .测试效果 3.Vue检测数据的原理 3.1 基本知 ...

  2. Vue中Object和Array数据变化侦测原理

    在学完Vue.js框架,完成了一个SPA项目后,一直想抽时间找本讲解Vue.js内部实现原理的书来看看,经过多方打听之后,我最后选择了<深入浅出Vue.js>这本书.然而惭愧的是,这本书已 ...

  3. Adapter数据变化改变现有View的实现原理及案例

    首先说说Adapter详细的类的继承关系.例如以下图 Adapte为接口它的实现类的对象作为AdapterView和View的桥梁,Adapter是装载了View(比方ListView和girdVie ...

  4. Angular(06)- 为什么数据变化,绑定的视图就会自动更新了?

    这里提一点,前端三大框架(Angular,React,Vue)的数据驱动来更新视图的原理,即 MVVM 的实现. 为什么数据发生变化,绑定的视图就会刷新了呢? 以下是我的个人理解,仅供参考: 在还是 ...

  5. Atitit 图像清晰度 模糊度 检测 识别 评价算法 原理

    Atitit 图像清晰度 模糊度 检测 识别 评价算法 原理 1.1. 图像边缘一般都是通过对图像进行梯度运算来实现的1 1.2. Remark: 1 1.3.  1.失焦检测. 衡量画面模糊的主要方 ...

  6. Linux数据包路由原理、Iptables/netfilter入门学习

    相关学习资料 https://www.frozentux.net/iptables-tutorial/cn/iptables-tutorial-cn-1.1.19.html http://zh.wik ...

  7. Vue数据双向绑定原理及简单实现

    嘿,Goodgirl and GoodBoy,点进来了就看完点个赞再go. Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. ...

  8. Android ContenObserver 监听联系人数据变化

    一.知识介绍 1.ContentProvider是内容提供者 ContentResolver是内容解决者(对内容提供的数据进行操作) ContentObserver是内容观察者(观察内容提供者提供的数 ...

  9. Vue 数据响应式原理

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

  10. [转帖]Git数据存储的原理浅析

    Git数据存储的原理浅析 https://segmentfault.com/a/1190000016320008   写作背景 进来在闲暇的时间里在看一些关系P2P网络的拓扑发现的内容,重点关注了Ma ...

随机推荐

  1. springboot中使用mybatis_plus逆向工程

    创建springboot项目,选择图片中所示依赖 mybatis-plus生成的依赖 <!-- mybatis_plus --> <dependency> <groupI ...

  2. 2022春每日一题:Day 30

    题目:[JSOI2009]电子字典 读完题后,暴力?确实,计算一下时间复杂度最坏情况下,20263*10000=1.5e8,卡一下常可以直接卡到7e7,最严格来说应该卡的过去,但是此题数据可以直接卡过 ...

  3. C温故补缺(十六):未定义行为

    未定义行为 在计算机程序设计中,未定义行为是指执行某种计算机代码 所产生的结果,这种代码在当前程序状态下的行为在其所使用的语言标准中没有规定. 以C语言为例,未定义行为指C语言标准未作规定的行为,同时 ...

  4. 《回炉重造》——Lambda表达式

    前言 Lambda 表达式(Lambda Expression),相信大家对 Lambda 肯定是很熟悉的,毕竟我们数学上经常用到它,即 λ .不过,感觉数学中的 Lambda 和编程语言中的 Lam ...

  5. jsp 页面返回、本页面刷新

    返回上一页面: window.history.go(-1);  //返回上一页window.history.back();  //返回上一页 返回上一页面并对上一页面刷新: history.go(-1 ...

  6. 模拟Promise的功能

    模拟Promise的功能,  按照下面的步骤,一步一步 1. 新建是个构造函数 2. 传入一个可执行函数 函数的入参第一个为 fullFill函数 第二个为 reject函数: 函数立即执行, 参数函 ...

  7. MySQL视图-触发器

    目录 一:视图 1.什么是视图? 2.为什么要用视图? 3.如何使用视图 4.反复拼接的繁琐(引入视图的作用) 5.解决方法 二:视图的应用 1.创建视图的格式: 2.查询视图层 3.查询Navica ...

  8. 去哪儿是如何做到大规模故障演练的?|TakinTalks

    # 一分钟精华速览 # 混沌工程作为一种提高技术架构弹性能力和容错能力的复杂技术手段,近年来讨论声音不断,相比在分布式系统上进行随机的故障注入实验,基于混沌工程的大规模自动化故障演练,不仅能将&quo ...

  9. 编译安装nmon

    nmon 是什么? nmon(Nigel's performance Monitor for Linux)是一种Linux性能监视工具,当前它支持 Power/x86/x86_64/Mainframe ...

  10. LeetCode HOT 100:子集(简单易懂的回溯)

    题目:78. 子集 题目描述: 给你一个整数数组,数组中元素互不相同.返回数组中所有可能的子集,且子集不能重复! 什么是子集?举个例子:原数组[1, 2, 3],[].[1].[1, 2].[1, 3 ...