携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情
前言
Vue最大的特点之一就是数据驱动视图,数据一旦发生改变,页面也随之发生变化。根据这个概念得出一个公式
UI = render(state)状态
state是输入,页面UI输出,这两个都是用户定的,而不变就是render(),所以Vue就扮演了render()这个角色,那么Vue是怎么知道state变化了呢?Vue是通过变化侦测知道
state发生了变化。变化侦测就是监听数据的变化,一旦发生了变化,就要去更新视图。
Object的变化侦测
数据驱动视图的关键点在于如何知道数据发生了变化,数据发生变化,通知视图更新即可。JS 提供了 Object.defineProperty 方法。
使Object数据变得“可观测”
知道数据什么时候被读取了或数据被修改了,称为数据变得“可观测”。
看一个示例:
let person = {
name: "jiaji",
age: 20
}
person.name = "jack";
console.log(person.age);
定义了一个 person 对象,当这个对象的属性被读取或者修改时,我们并不知青,使用
Object.defineProperty()让这个对象主动告诉我们它的属性被读取或者修改了。
使用 Object.defineProprety()改写上面的额例子:
let person = {name:"jiaji"};
let age = 20;
Object.defineProperty(person, "age", {
enumerable: true,
configurable: true,
get(){
console.log("age属性被读取了");
return age;
},
set(val){
console.log("age属性被修改了");
age = val;
}
})
console.log(person.age);
person.age = 30;
console.log(person.age);
使用
Object.defineProperty()方法给 person 对象定义了一个 age 属性,当 person 对象进行读或写的操作时,就会触发get()和set()方法,现在这个对象就变成“可观测”啦
把 person 对象的所有属性都变成“可观测”:
// 源码位置:src/core/observer/index.js
/**
* Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
*/
export class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
// 当value为数组时的逻辑
// ...
} else {
this.walk(value);
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive(obj, key, val) {
// 如果只传了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === 'object') {
new Observer(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
判断数据类型,当类型为
Object时调用walk将每一个属性都调用defineReactive方法,如果value的数据类型还是Object,就递归子属性,把所有的属性都转换成get()/set()的形式来侦测变化。
现在,可以直接这样定义 person:
let person = new Observer({
name:"jiaji",
age:20
})
console.log(person)
依赖收集
现在数据已经变得可侦测了,只要数据发生变化,去更新视图就好了,但是一个视图这么多节点,应该更新哪些呢?
数据发生变化,使用了这个数据的节点就都要更新,也就是说:谁(节点)依赖了这个数据,谁就要更新,一个数据可能被多个节点同时使用,所以给每个数据都创建一个依赖数组,总的来说就是:谁(节点)依赖就这个数据,就把谁放进这个数据的依赖数组中,当这个数据放生变化时,就去这个数组的依赖数组中,告诉这些依赖,该更新视图了。
当可观测的数据被获取时会触发 get() 方法,数据发生变化时会触发 set() 方法,总结起来就是:在 get() 中收集依赖,在 set() 中通知依赖更新。
依赖的保存是通过数组的形式,但是只用一个数组来保存依赖显然是不合理的,更好的做法是为每一个数据都创建一个依赖数组,各自管理自己的依赖,所以依赖管理器 Dep 类应运而生:
// 源码位置:src/core/observer/dep.js
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub) {
remove(this.subs, sub)
}
// 添加一个依赖
depend () {
if (window.target) {
this.addSub(window.target)
}
}
// 通知所有依赖更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
使用
subs数组来存放依赖,定义了一些实例方法对依赖进行添加、删除、通知更新等操作。接下来在get()中收集依赖,在set()中通知依赖更新。
function defineReactive(obj, key, val) {
if (arguments.length === 2) {
val = obj[key];
}
if (typeof val === "object") {
new Observer(val);
}
const dep = new Dep(); //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend(); // 在getter中收集依赖
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
dep.notify(); // 在setter中通知依赖更新
},
});
}
一直在说依赖,也知道依赖就是使用了这个数据的节点,但是在收集、更新的时候怎么把这个依赖描述出来?
Vue中还有一个 Watcher 类,前面说到谁用了数据,谁就是依赖,Vue就会给这个依赖创建一个 Watcher实例,在数据发生变化后,去通知依赖对应的 Watcher 实例,由这个实例去通知真正的视图。
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export 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]]
}
return obj
}
}
逻辑分析:
- 实例化
watcher类时,会执行构造函数 - 在构造函数中调用
this.get()实例方法,通过window。target = this把自身实例赋给了全局的唯一对象window.target上,然后通过let value = this.getter.call(vm,vm)获取被依赖额数据,这时会触发这个数据的get()方法,通过调用dep.depend()收集依赖,将挂载到window。target上的值存入依赖数组中。 - 数据发生变化时,会触发数据的
set()方法,通过调用dep.notify(),遍历所有依赖( watcher 实例 ),执行依赖的update()方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。
简单总结:依赖( watcher 实例 )创建后挂载到 window.target 属性上,然后读取数据,触发 get() 将这个依赖收集到依赖数组中,数据发生变化后,触发 set(),遍历所有依赖,执行 update()实例方法,更新视图。
不足之处
通过 Object.defineproperty方法实现了对象数据的”可观测“,但是只能观测到属性的读取和修改,如果直接给对象新增一个属性或者删除一个属性,它是无法观测到的,导致无法通知依赖更新视图。
Vue增加了两个全局 API 解决了这个问题
- vue.set
- vue.delete
小结
Observer类,实现数据的”可观测“Dep类,将所有依赖保存到依赖数组中watcher类,视图通过watcher读取数据时,会将watcher实例添加到依赖中,发生变化时,会触发set(),遍历依赖,告诉每个依赖更新视图。