其他章节请看:

vue 快速入门 系列

侦测数据的变化 - [基本实现]

初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false),页面中的一行文本(现在你看到我了)就不见了。

这里涉及到 Vue 一个重要特性:响应式系统。数据模型只是普通的 JavaScript 对象,当我们修改时,视图会被更新。而变化侦测是响应式系统的核心。

Object的变化侦测

下面我们就来模拟侦测数据变化的逻辑。

强调一下我们要做的事情:数据变化,通知到外界(外界再做一些自己的逻辑处理,比如重新渲染视图)。

开始编码之前,我们首先得回答以下几个问题:

  1. 如何侦测对象的变化?

    • 使用 Object.defineProperty()。读数据的时候会触发 getter,修改数据会触发 setter。
    • 只有能侦测对象的变化,才能在数据发生变化的时候发出通知
  2. 当数据发生变化的时候,我们通知谁?
    • 通知用到数据的地方。而数据可以用在模板中,也可以用在 vm.$watch() 中,地方不同,行为也不相同,比如这里要渲染模板,那里要进行其他逻辑。所以干脆抽象出一个类。当数据变化的时候通知它,再由它去通知其他地方。
    • 这个类起名叫 Watcher。就是一个中介。
  3. 依赖谁?
    • 通知谁,就依赖谁,依赖 Watcher。
  4. 何时通知?
    • 修改数据的时候。也就是 setter 中通知
  5. 何时收集依赖?
    • 因为要通知用数据的地方。用数据就得读数据,我们就可以在读数据的时候收集,也就是在 getter 中收集
  6. 收集到哪里?
    • 可以在每个属性里面定义一个数组,与该属性有关的依赖都放里面

编码如下(可直接运行):

// 全局变量,用于存储依赖
let globalData = undefined; // 将数据转为响应式
function defineReactive (obj,key,val) {
// 依赖列表
let dependList = []
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
// 收集依赖(Watcher)
globalData && dependList.push(globalData)
return val
},
set: function reactiveSetter (newVal) {
if(val === newVal){
return
}
// 通知依赖项(Watcher)
dependList.forEach(w => {
w.update(newVal, val)
})
val = newVal
}
});
} // 依赖
class Watcher{
constructor(data, key, callback){
this.data = data;
this.key = key;
this.callback = callback;
this.val = this.get();
}
// 这段代码可以将自己添加到依赖列表中
get(){
// 将依赖保存在 globalData
globalData = this;
// 读数据的时候收集依赖
let value = this.data[this.key]
globalData = undefined
return value;
}
// 数据改变时收到通知,然后再通知到外界
update(newVal, oldVal){
this.callback(newVal, oldVal)
}
} /* 以下是测试代码 */
let data = {};
// 将 name 属性转为响应式
defineReactive(data, 'age', '88')
// 当数据 age 改变时,会通知到 Watcher,再由 Watcher 通知到外界
new Watcher(data, 'age', (newVal, oldVal) => {
console.log(`外界:newVal = ${newVal} ; oldVal = ${oldVal}`)
}) data.age -= 1 // 控制台输出: 外界:newVal = 87 ; oldVal = 88

在控制台下继续执行 data.age -= 1,则会输出 外界:newVal = 86 ; oldVal = 87

附上一张 Data、defineReactive、dependList、Watcher和外界的关系图。

首先通过 defineReactive() 方法将 data 转为响应式(defineReactive(data, 'age', '88'))。

外界通过 Watcher 读取数据(let value = this.data[this.key]),数据的 getter 则会被触发,于是通过 globalData 收集Watcher。

当数据被修改(data.age -= 1), 会触发 setter,会通知依赖(dependList),依赖则会通知 Watcher(w.update(newVal, val)),最后 Watcher 再通知给外界。

关于 Object 的问题

思考一下:上面的例子,继续执行 delete data.age 会通知到外界吗?

不会。因为不会触发 setter。请接着看:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id='app'>
<section>
{{ p1.name }}
{{ p1.age }}
</section>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
p1: {
name: 'ph',
age: 18
}
}
})
</script>
</body>
</html>

运行后,页面会显示 ph 18。我们知道更改数据,视图会重新渲染,于是在控制台执行 delete app.p1.name,发现页面没有变化。这与上面示例中执行 delete data.age 一样,都不会触发setter,也就不会通知到外界。

为了解决这个问题,Vue提供了两个 API(稍后将介绍它们):vm.$set 和 vm.$delete。

如果你继续执行 app.$delete(app.p1, 'age'),你会发现页面没有任何信息了(name 属性已经用 delete 删除了,只是当时没有重新渲染而已)。

:如果这里执行 app.p1.sex = 'man',用到数据 p1 的地方也不会被通知到,这个问题可以通过 vm.$set 解决。

Array 的变化侦测

背景

假如数据是 let data = {a:1, b:[11, 22]},通过 Object.defineProperty 将其转为响应式之后,我们修改数据 data.a = 2,会通知到外界,这个好理解;同理 data.b = [11, 22, 33] 也会通知到外界,但如果换一种方式修改数据 b,就像这样 data.b.push(33),是不会通知到外界的,因为没走 setter。请看示例:

function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`get val = ${val}`)
return val
},
set: function reactiveSetter (newVal) {
if(val === newVal){
return
}
console.log(`set val = ${newVal}; oldVal = ${val}`)
val = newVal
}
});
} // 以下是测试代码 {1}
let data = {}
defineReactive(data, 'a', [11,22])
data.a.push(33) // get val = 11,22 (没有触发 setter) {2}
data.a // get val = 11,22,33
data.a = 1 // set val = 1; oldVal = 11,22,33(触发 setter)

通过 push() 方法改变数组的值,确实没有触发 setter(行{2}),也就不能通知外界。这里好像说明了一个问题:通过 Object.definePropery() 方法,只能将对象转为响应式,不能将数组转为响应式。

其实 Object.definePropery() 可以将数组转为响应式。请看示例:

// 继续上面的例子,将测试代码(行{1})改为:
let data = []
defineReactive(data, '0', 11)
data[0] = 22 // set val = 22; oldVal = 11
data.push(33) // 不会触发 {10}

虽然 Object.definePropery() 可以将数组转为响应式,但通过 data.push(33)(行{10})这种方式修改数组,仍然不会通知到外界。

所以在 Vue 中,将数据转为响应式,用了两套方式:对象使用 Object.defineProperty();数组则使用另一套。

实现

es6 中可以用 Proxy 侦测数组的变化。请看示例:

let data = [11,22]
let p = new Proxy(data, {
set: function(target, prop, value, receiver) {
target[prop] = value;
console.log('property set: ' + prop + ' = ' + value);
return true;
}
})
console.log(p)
p.push(33)
/*
输出:
[ 11, 22 ]
property set: 2 = 33
property set: length = 3
*/

es6 以前就稍微麻烦点,可以使用拦截器。原理是:当我们执行 [].push() 时会调用数组原型(Array.prototype)中的方法。我们在 [].push()Array.prototype 之间增加一个拦截器,以后调用 [].push() 时先执行拦截器中的 push() 方法,拦截器中的 push() 在调用 Array.prototype 中的 push() 方法。请看示例:

// 数组原型
let arrayPrototype = Array.prototype // 创建拦截器
let interceptor = Object.create(arrayPrototype) // 将拦截器与原始数组的方法关联起来
;('push,pop,unshift,shift,splice,sort,reverse').split(',')
.forEach(method => {
let origin = arrayPrototype[method];
Object.defineProperty(interceptor, method, {
value: function(...args){
console.log(`拦截器: args = ${args}`)
return origin.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true
})
}); // 测试
let arr1 = ['a']
let arr2 = [10]
arr1.push('b')
// 侦测数组 arr2 的变化
Object.setPrototypeOf(arr2, interceptor) // {20}
arr2.push(11) // 拦截器: args = 11
arr2.unshift(22) // 拦截器: args = 22

这个例子将能改变数组自身内容的 7 个方法都加入到了拦截器。如果需要侦测哪个数组的变化,就将该数组的原型指向拦截器(行{20})。当我们通过 push 等 7 个方法修改该数组时,则会在拦截器中触发,从而可以通知外界。

到这里,我们只完成了侦测数组变化的任务。

数据变化,通知到外界。上文编码的实现只是针对 Object 数据,而这里需要针对 Array 数据。

我们也来思考一下同样的问题:

  1. 如何侦测数组的变化?

    • 拦截器
  2. 当数据发生变化的时候,我们通知谁?
    • Watcher
  3. 依赖谁?
    • Watcher
  4. 何时通知?
    • 修改数据的时候。拦截器中通知。
  5. 何时收集依赖?
    • 因为要通知用数据的地方。用数据就得读数据。在读数据的时候收集。这和对象收集依赖是一样的。
    • {a: [11,22]} 比如我们要使用 a 数组,肯定得访问对象的属性 a。
  6. 收集到哪里?
    • 对象是在每个属性中收集依赖,但这里得考虑数组在拦截器中能触发依赖,位置可能得调整

就到这里,不在继续展开了。接下来的文章中,我会将 vue 中与数据侦测相关的源码摘出来,配合本文,简单分析一下。

关于 Array 的问题

// 需要自己引入 vue.js。后续也尽可能只罗列核心代码
<div id='app'>
<section>
{{ p1[0] }}
{{ p1[1] }}
</section>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
p1: ['ph', '18']
}
})
</script>

运行后在页面显示 ph 18,控制台执行 app.p1[0] = 'lj' 页面没反应,因为数组只有调用指定的 7 个方法才能通过拦截器通知外界。如果执行 app.$set(app.p1, 0, 'pm') 页面内容会变成 pm 18

其他章节请看:

vue 快速入门 系列

vue 快速入门 系列 —— 侦测数据的变化 - [基本实现]的更多相关文章

  1. vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ...

  2. vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue api 原理] 前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会 ...

  3. vue 快速入门 系列 —— Vue(自身) 项目结构

    其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ...

  4. vue 快速入门 系列 —— 实例方法(或 property)和静态方法

    其他章节请看: vue 快速入门 系列 实例方法(或 property)和静态方法 在 Vue(自身) 项目结构 一文中,我们研究了 vue 项目自身构建过程,也知晓了 import Vue from ...

  5. vue 快速入门 系列 —— Vue 实例的初始化过程

    其他章节请看: vue 快速入门 系列 Vue 实例的初始化过程 书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法.倘若你看过 jQuery 的源码, ...

  6. vue 快速入门 系列 —— vue 的基础应用(上)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...

  7. vue 快速入门 系列 —— vue-cli 上

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 上 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  8. vue 快速入门 系列 —— vue-router

    其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...

  9. vue 快速入门 系列 —— 使用 vue-cli 3 搭建一个项目(下)

    其他章节请看: vue 快速入门 系列 使用 vue-cli 3 搭建一个项目(下) 上篇 我们已经成功引入 element-ui.axios.mock.iconfont.nprogress,本篇继续 ...

随机推荐

  1. JavaScript getter and setter All In One

    JavaScript getter and setter All In One getter & setter JavaScript Object Accessors JavaScript A ...

  2. js Memory Management

    js Memory Management 垃圾回收是一个术语,在计算机编程中用于描述查找和删除那些不再被其他对象引用的对象的处理过程. 换句话说,垃圾回收是删除任何其他对象未使用的对象的过程. 垃圾收 ...

  3. how to copy to clipboard using windows cmd

    how to copy to clipboard using windows cmd Windows clipboard command line https://www.labnol.org/sof ...

  4. (转)IP协议详解之子网寻址、子网掩码、构造超网

    原文网址:http://www.cnblogs.com/way_testlife/archive/2010/10/05/1844399.html 子网寻址 1. 从两级IP地址到三级IP地址 < ...

  5. 02.Fancy Indexing

    import numpy as np x = np.arange(16) index = [3,5,8] x[index] array([3, 5, 8]) X = x.reshape(4,-1) X ...

  6. 06_MySQL数据类型

    MySQL数据类型

  7. 使用RSEM进行转录组测序的差异表达分析

    仍然是两年前的笔记 1. prepare-reference 如果用RSEM对比对后的bam进行转录本定量,则在比对过程中要确保比对用到的索引是由rsem-prepare-reference产生的. ...

  8. springCloud中的服务调用feign

    springCloud中的服务调用(要在调用端写) 前提进行了服务注册 流程: 1.在服务模块中添加依赖 <!--服务调用--> <dependency> <groupI ...

  9. Socket实现简单聊天

    服务端: package main.java.com.socket_dome; import java.io.IOException; import java.io.InputStream; impo ...

  10. MarkDown简单语法回顾

    写在前面: 本文是我的第一篇博客文章,希望与大家共同交流.分享我们热爱的技术,努力成为一名优秀的软件工程师! 进入正文 使用MarkDown记笔记已经有些时候了,编辑器是使用的sublime text ...