vue 快速入门 系列 —— 侦测数据的变化 - [基本实现]
其他章节请看:
侦测数据的变化 - [基本实现]
在 初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false
),页面中的一行文本(现在你看到我了
)就不见了。
这里涉及到 Vue 一个重要特性:响应式系统。数据模型只是普通的 JavaScript 对象,当我们修改时,视图会被更新。而变化侦测是响应式系统的核心。
Object的变化侦测
下面我们就来模拟侦测数据变化的逻辑。
强调一下我们要做的事情:数据变化,通知到外界(外界再做一些自己的逻辑处理,比如重新渲染视图)。
开始编码之前,我们首先得回答以下几个问题:
- 如何侦测对象的变化?
- 使用 Object.defineProperty()。读数据的时候会触发 getter,修改数据会触发 setter。
- 只有能侦测对象的变化,才能在数据发生变化的时候发出通知
- 当数据发生变化的时候,我们通知谁?
- 通知用到数据的地方。而数据可以用在模板中,也可以用在 vm.$watch() 中,地方不同,行为也不相同,比如这里要渲染模板,那里要进行其他逻辑。所以干脆抽象出一个类。当数据变化的时候通知它,再由它去通知其他地方。
- 这个类起名叫 Watcher。就是一个中介。
- 依赖谁?
- 通知谁,就依赖谁,依赖 Watcher。
- 何时通知?
- 修改数据的时候。也就是 setter 中通知
- 何时收集依赖?
- 因为要通知用数据的地方。用数据就得读数据,我们就可以在读数据的时候收集,也就是在 getter 中收集
- 收集到哪里?
- 可以在每个属性里面定义一个数组,与该属性有关的依赖都放里面
编码如下(可直接运行):
// 全局变量,用于存储依赖
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 数据。
我们也来思考一下同样的问题:
- 如何侦测数组的变化?
- 拦截器
- 当数据发生变化的时候,我们通知谁?
- Watcher
- 依赖谁?
- Watcher
- 何时通知?
- 修改数据的时候。拦截器中通知。
- 何时收集依赖?
- 因为要通知用数据的地方。用数据就得读数据。在读数据的时候收集。这和对象收集依赖是一样的。
{a: [11,22]}
比如我们要使用 a 数组,肯定得访问对象的属性 a。
- 收集到哪里?
- 对象是在每个属性中收集依赖,但这里得考虑数组在拦截器中能触发依赖,位置可能得调整
就到这里,不在继续展开了。接下来的文章中,我会将 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 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]
其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ...
- vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]
其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue api 原理] 前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会 ...
- vue 快速入门 系列 —— Vue(自身) 项目结构
其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ...
- vue 快速入门 系列 —— 实例方法(或 property)和静态方法
其他章节请看: vue 快速入门 系列 实例方法(或 property)和静态方法 在 Vue(自身) 项目结构 一文中,我们研究了 vue 项目自身构建过程,也知晓了 import Vue from ...
- vue 快速入门 系列 —— Vue 实例的初始化过程
其他章节请看: vue 快速入门 系列 Vue 实例的初始化过程 书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法.倘若你看过 jQuery 的源码, ...
- vue 快速入门 系列 —— vue 的基础应用(上)
其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...
- vue 快速入门 系列 —— vue-cli 上
其他章节请看: vue 快速入门 系列 Vue CLI 4.x 上 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...
- vue 快速入门 系列 —— vue-router
其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...
- vue 快速入门 系列 —— 使用 vue-cli 3 搭建一个项目(下)
其他章节请看: vue 快速入门 系列 使用 vue-cli 3 搭建一个项目(下) 上篇 我们已经成功引入 element-ui.axios.mock.iconfont.nprogress,本篇继续 ...
随机推荐
- RESTful 架构 && RESTful API
RESTful 架构 && RESTful API REpresentational State Transfer (REST) 具象状态传输https://en.wikipedia. ...
- 十三香 & 香料
十三香 & 香料 十三香原料组成不完全一致, 但有一些香料却是大家都会采用的: 草蔻.砂仁.肉豆蔻.肉桂.丁香. 花椒.大料.小茴香.木香.白芷. 山萘.良姜和姜 王守义十三香 http:// ...
- trao 模拟点击 & js auto click
trao 模拟点击 & js auto click 日历上选择某一天,在 scrollview 自动定位到选择的那一天 click 后获取 item 的 e.target.offsetLeft ...
- wxPython 创建基本窗口
$ pip install wxPython import wx class MyFrame(wx.Frame): def __init__(self, parent, title): super(M ...
- js 触发长按事件
为网站添加触摸功能 <button id="btn1">长按触发</button> <button id="btn2">长按 ...
- Flutter 将TextField平滑过渡到Text
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends State ...
- Flutter Android Toast Message(flutter访问Android Toast Message)
原文 Android Toast通知可用于向用户发送快速消息,并在几秒钟后消失. 但是当涉及Flutter时,没有直接的方式来显示这些Toast消息.因此,我们需要找到一种替代方法来实现它.在这种情况 ...
- alpakka-kafka(1)-producer
alpakka项目是一个基于akka-streams流处理编程工具的scala/java开源项目,通过提供connector连接各种数据源并在akka-streams里进行数据处理.alpakka-k ...
- Ping 的工作原理你懂了,那 ICMP 你懂不懂?
计算机网络我也连载了很多篇了,大家可以在我的公众号「程序员cxuan」 或者我的 github 系统学习. 计算机网络第一篇,聊一聊网络基础 :计算机网络基础知识总结 计算机网络第二篇,聊一聊 TCP ...
- SpringBoot2.x中的AOP机制总结(附带demo)
寄语:刚开始学aop的时候是大三吧,老师讲的不好,我也没学好,导致现在才有个较为清晰的认知,那个时候只知道有aop, 根部不明白aop的作用,时至今日,任然觉得aop难以咀嚼,奈何平时不用面试要用,特 ...