我是靠谱客的博主 强健小蝴蝶,最近开发中收集的这篇文章主要介绍数据响应式,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

MVVM

model <= view-model => view
数据变化,视图会自动变化。
vue的数据变化是非侵入式的,不需要调用其他的api去改变数据。

从Object.defineProperty()说数据响应式

方式一

 Object.defineProperty(obj,key,{
value:value,
enumerable:true,
writable:true,
configurable:true
})

方式二

 Object.defineProperty(obj,key,{
enumerable:true,
writable:true,
configurable:true,
get(){
return 该数据的值
},
set(newVal) {
//改变数据回会触发
}
})

Object.defineProperty的一些隐藏属性可以帮我们对数据加以控制,其中get、和set 在数据获取的时候和改变的时候会触发。由于直接使用 Object.definePropertyget的时候只能返回一个特定的值,所以我们需要借助一个变量,用于存储数据。写法如下:

let temp;
Object.defineProperty(obj,key,{
enumerable:true,
writable:true,
configurable:true,
get(){
return temp
},
set(newVal) {
//改变数据回会触发
temp = newVal
}
})

在我们后面使用时,我们的temp变成了传进来的val,同时也满足我们的初始值,此时我们使用闭包。

function defineReactive( data , key ,val){
Object.defineProperty(data , key ,{
get(){
console.log('触发了get',key);
return val
},
set(newValue){
console.log('触发了set',key);
val = newValue
},
enumerable:true,
configurable:true
})
}

--------------------------------------------------分割线:下面开始实现数据响应式------------------------------------

observe函数

使用observe函数将数据变成响应式数据,在我们的observe函数中我们会给我们的数据绑定一个不可枚举的属性__ob__,对数据监控添加的属性。并且将该数据返回。函数内部实现,我们首先得判断该数据是否是对象,如果是对象我们才进行检测,在vue中我们的data,props就是一个对象。然后判断数据是否已经存在__ob__属性。如果存在我们就直接将此属性返回。如果不存在我们就通过Observer类对数据进行实例化,然后返回实例化之后的数据。

import Observer from "./Observer";
// 创建observe函数
export default function observe(value){
// 如果value不是对象,什么也不做
if(typeof value != 'object') return ;
let ob;
if(typeof value.__ob__ !== 'undefined'){
ob = value.__ob__;
}else{
ob = new Observer(value)
}
return ob;
}

Observer类

在observe中,我们创建了observe类。
注意: dep,是挂载在observe实例上的收集依赖的,此处不做讲解,在后续的Dep中进行讲解。
当我们拿到的数据,首先是给数据本身创建一个__ob__属性,监控数据的本身。此时由于我们有明确的值,我们可以直接使用Object.defineProperty创建响应式数据挂载在当前数据的__ob__属性上。代码中使用了def,def函数在下面进行讲解,其本身就是实现Object.defineProperty的包装,如果我们的数据是一个数组,我们则需要将这个数组的原型指向arrayMethodsarrayMethods会在后续讲解,你如果读到了此处,可以直接理解为将数组变为响应式。 Object.setPrototypeOf则是设置原型。此处可以理解为将value的原型指向arrayMethodsobserveArray则是遍历数组的每一项,将数组的每一项都变成响应式数据,此时我们只需要将每一项的值传入observe。你可以理解为循环递归。对于对象来说,我们也需要将每一项变成响应式数据,此时我们使用walk方法,对对象的每个属性遍历并变为响应式数据,将对象变成响应式数据将在defineReactive中说明。

import defineReactive from './defineReactive'
import { def } from './utils'
import { arrayMethods } from './array'
import observe from './observe';
import Dep from './Dep';
export default class Observer {
constructor(value){
// 每个Observer实例身上都有一个dep的实例用来收集依赖
this.dep = new Dep()
def(value,'__ob__',this,false)
// 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
if(Array.isArray(value)){
// 如果是数组则要非常蛮干:将这个数组的原型指向arrayMethods
Object.setPrototypeOf(value , arrayMethods);
this.observeArray(value)
}else{
this.walk(value)
}
}
// 遍历
walk(value){
for(let k in value){
defineReactive(value, k)
}
}
// 数组的特殊遍历
observeArray(arr){
for(let i = 0 , l = arr.length ; i <l ; i++){
observe(arr[i])
}
}
}

将数据变成响应式def

在 文章上方我们讲解了Object.defineProperty,此处进行了包装。

export const def = function(obj,key,value,enumerable){
Object.defineProperty(obj,key,{
value,
enumerable,
writable:true,
configurable:true
})
}

----------------------------------------- 分割线:结合watcher Dep------------------------------------------------------------

defineReactive对象的响应式

dep是用于收集依赖的,此处我们创建的dep是属于当前这个需要处理为响应式数据的,数据响应式我们在get中收集依赖,在set中更新依赖。所以我们对数据使用 Object.defineProperty的时候在get中进行依赖的收集。Dep.target是Dep类的静态属性全局唯一,指向当前的渲染watcher。所以如果存在Dep.target,我们就要调用dep.depend()将依赖收集起来,请结合Watcher一起阅读。Dep是全局唯一的类,我们在Watcher中将Dep.target指向了当前的Watcher。同时我们需要收集childOb的依赖,在使用创建childOb的时候我们使用observe创建,此时会形成循环,使得每一个子代都能被收集起来。在set中,从前面的讲解我们知道被监测的数据发生改变时会触发set,所以我们可以在set中进行依赖的更新同时需要对新的数据进行observe把新数据变成响应式。同时我们需要使用 dep.notify()进行依赖的更新。

import observe from "./observe"
import Dep from './Dep'
export default function defineReactive( data , key ,val){
const dep = new Dep()
//只传了两个参数,代码只有data和key,则对val的初始化为data[key]
if(arguments.length == 2){
val = data[key]
}
let childOb = observe(val)
Object.defineProperty(data , key ,{
get(){
console.log('触发了get',key);
// 依赖收集阶段
if(Dep.target){
dep.depend()
if(childOb){
childOb.dep.depend()
}
}
return val
},
set(newValue){
console.log('触发了set',key);
if(val == newValue){
return ;
}
val = newValue
// 当设置了新之,新值也要被observe
childOb = observe(newValue)
// 发布订阅模式,通知dep
dep.notify()
},
enumerable:true,
configurable:true
})
}

数组响应式

修改数组的方法将数组变成响应式,['push','pop','shift','unshift','splice','sort','reverse']数组的这些方法会使得数组的数据发生改变且不是响应式的,所以我们需要改写数组的这些方法。在改写之前我们首先得先将原有的这些方法保存起来,此时我们只需要将Array.prototype进行保存,在之后我们将我们改写的方法的原型指向我们数组的原型,实现原有其他方法的继承,不影响数组使用其余的方法。Object.create指定原型,相当于__porto__。之后我们将重写方法,首先我们需要先备份原有的方法,我们在调用def来将数组变为响应式,我们在第第三个参数,value使用函数进行返回,所以我们需要调用原来的函数,将值进行返回。我们还需要判断数组是否有新元素的加入,如果存在新元素的加入则需要对新元素实现响应式。在那7个方法中,我们只有push、unshift、splice会往数组插入新元素,此时我们只需要拿到这些新元素在他的__ob__上调用observeArray()方法,将其变成响应式,当然也需要通知dep进行更新,则需要使用ob.dep.notify()更新依赖。

import { def } from './utils'
// 得到Array的原型
const arrayPrototype = Array.prototype
// 已Array.prototype为原型创建arrayMethods对象
export const arrayMethods = Object.create(arrayPrototype)
// 要被改写的7个方法
const methodsNeedChange = ['push','pop','shift','unshift','splice','sort','reverse']
methodsNeedChange.forEach(methodName => {
// 备份原来的方法
const original = arrayPrototype[methodName]
def(arrayMethods , methodName , function(){
// 恢复原来的调用
const result =
original.apply(this,arguments)
// 将类数组变为数组
const args = [...arguments]
console.log(args);
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组。
const ob = this.__ob__ ;
let inserted = [];
switch(methodName){
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice方法从第2个参数开始往后都是需要添加进数组的值
inserted = args.slice(2);
break;
}
// 判断有没有要插入的新项,让新项也年初响应的
if(inserted){
ob.observeArray(inserted)
}
ob.dep.notify()
// 恢复原有的返回值
return result
},false)
})

Dep依赖收集

依赖收集器,subs用于存储依赖,addSub添加新的订阅,depend添加依赖,notify更新依赖。

var uid = 0
export default class Dep {
constructor(){
this.id = uid++
// 存储自己的订阅者,这个数组里面放的是Watcher的实例
this.subs = []
}
// 添加订阅
addSub(sub){
this.subs.push(sub)
}
// 添加依赖
depend(){
// Dep.target就是我们自己指定的全局的位置,你用window.target也行,只要是全局唯一,没有歧义就行
if(Dep.target){
this.addSub(Dep.target)
}
}
notify(){
console.log('我是notify');
// 浅克隆
const subs = this.subs.slice();
// 遍历
for(let i = 0 , l = subs.length ; i < l ; i++){
subs[i].update();
}
}
}

Watcher依赖

只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。刚刚创建时我们就会调用get方法,从而进行将watcher的实例绑定在Dep的target上,并将值返回给实例的value,如果更新了我们在Dep的notify会触发watcher的更新,此时会调用到getAndInvoke,在getAndInvoke函数中我们将新数据旧数据拿到,并调用使用watcher的回调函数,在回调函数中利用新值和旧值做其他事。

import Dep from "./Dep";
var uid = 0
export default class Watcher{
constructor(target , expression ,callback){
this.id = uid++;
this.target = target;
this.getter = parsePath(expression)
this.callback = callback;
this.value = this.get()
}
update(){
this.run()
}
get(){
// 进入依赖收集阶段,让全局的Dep.target设置为Watcher本身,那么就进入依赖收集阶段
Dep.target = this;
const obj = this.target;
let value;
// 只要能找,就一直找
try {
value = this.getter(obj)
} catch (error) {
Dep.target = null;
}
return value
}
run(){
this.getAndInvoke(this.callback)
}
getAndInvoke(cb){
const value = this.get()
if(value !== this.value || typeof value == 'object'){
const oldValue = this.value
this.value = value;
cb.call(this.target , value ,oldValue)
}
}
}
function parsePath(str){
let segments = str.split('.');
return (obj) => {
for(let i = 0 ; i < segments.length ; i++){
if(!obj) return ;
obj = obj[segments[i]]
}
return obj
}
}

结尾

在我们的watcher的回调函数时,我们可以将新数据给到data,将新的data和模板进行patch,进行diff,从而达到最小量更新。

最后

以上就是强健小蝴蝶为你收集整理的数据响应式的全部内容,希望文章能够帮你解决数据响应式所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(37)

评论列表共有 0 条评论

立即
投稿
返回
顶部