vue源码之数据检测
概述
vue是数据驱动页面,数据即状态的变化,页面即状态的变化导致页面的变化,也就是说可以用公式表示:UI=render(state)。UI和state都是用户设置的,都是可变的,不变的只有render(),所以vue起的就是render()的作用。
那么render有什么作用呢?即监听state什么时候读取和什么时候改变了,这个可以使用Object.defineProperty方法去实现。来个例子:
const obj = {
name: 'sifan',
age: 21
}
let name = obj.name
Object.defineProperty(obj, 'name', {
enumerable: true,
configurable:true,
set(val) {
console.log('调用set方法');
name = val
},
get() {
console.log('调用get方法');
return name
}
})
console.log(obj.name);
obj.name = '20'
console.log(obj.name);
如果只是定义对象而不设置Object.defineProperty的话,虽然也可以修改和得到数据,但是不能知道什么时候在调用或修改。那么既然可以监听到obj的一个属性了,也表示可以监听obj的其他所有属性。
为了可以把obj的所有属性都变得可观测,可以定义如下代码:
export default class Observer {
constructor(val) {
// 新建dep实例
this.dep = new Dep()
// 在这个属性上设置__ob__,避免重复遍历
def(val, '__ob__', this, false)
// 判断是不是数组,因为vue中数组方法不可用,所以需要对数组的方法进行重构
if (!Array.isArray(val))
// 如果该值不是数组,对这个值进行监听
this.walk(val)
else {
// 是数组,将重构好的arrayMethods设置到val上
Object.setPrototypeOf(val, arrayMethods)
// 进行监听
this.observeArray(val)
}
}
walk(val) {
for (const i in val) {
defineReactive(val, i)
}
}
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
export default function defineReactive(obj, str, val) {
if (arguments.length == 2) {
val = obj[str]
}
let childOb = observe(val)
Object.defineProperty(obj, str, {
configuration: true,
enumerate: true,
set(newData) {
console.log('正在修改' + str);
// 调用notify去通知依赖的watcher去修改
val = newData
// 再次进行监听,因为不知道修改的值是数组还是对象或者普通值
observe(val)
dep.notify()
},
get() {
if(childOb) {
childOb.dep.depend()
}
console.log('正在调用' + str);
return val
}
})
}
export default function observe(val) {
// 如果是普通值,直接返回
if (typeof val != 'object') return
let ob = null
// 如果为空,进行遍历,否则将值返回
if (typeof val.__obj__ !== 'undefined') {
ob = val.__obj__
} else {
ob = new Observer(val)
}
return ob
}
//处理数组的方法:在vue中创建一个数组方法拦截器,拦截在数组实例和Array.prototype之间,在拦截器中重写操作数组的方法
let arrayPototype = Array.prototype
export const arrayMethods =Object.create(arrayPototype)
let methodsNeedChange = [
'push',
'pop',
'unshift',
'shift',
'splice',
'sort',
'reverse'
]
//重写方法
methodsNeedChange.forEach(methodName => {
const original = arrayPototype[methodName]
def(arrayMethods, methodName, function () {
let arg = [...arguments]
const result = original.apply(this, arg)
const ob = this.__ob__
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = arg
break;
case 'splice':
inserted = arg.slice(2)
}
if (inserted) {
ob.arrayPototype(inserted)
}
ob.dep.notify()
return result
}, false)
})
那么简单的实现数据响应式更新已经完成了,通知数据的更新并去更改所有使用到该数据的代码如何实现呢?这个实现主要有两个类,一个是Watcher类,表示的就是依赖,大概意思是那个地方用到了某个数据,那watcher就是它的依赖;另一个是dep类,用来存储依赖。代码实现如下:
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
//获取依赖的数据,因为在获取数据中有dep.depend()的调用,
// 然后就可以将window.target中的值存入到依赖的数组
let value = this.getter.call(vm, vm)
// 释放
window.target = undefined
return value
}
update() {
const oldValue = this.value
this.value = this.get()
// 意思就是,将this.value和oldValue作为vm的参数运行
// 调用数据更新回调函数,从而更新视图
this.cb.call(this.vm, this.value, oldValue)
}
}
// 正则表达式,将点语法的东西取出来
const baiRE = /[^\w.$]/
function parsePath(path) {
if (baiRE.test(path)) {
return
}
const segments = pat.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return
}
obj = obj[segments[i]]
}
return obj
}
}
export default class Dep {
constructor() {
//存储有多少
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
this.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++) {
// 调用watcher实例中的update,去调用数据变化的更新回调函数
subs[i].update()
}
}
remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
}
如上就能实现数据的变化和侦测,但是仍然有不足之处,这个不足之处只是我看别人说的,但是我自己有点想不到,比如别人说添加一个属性或删除一个属性都无法检测到,所以vue设置了delete和set,这部分还没有看。以后看了再更。
行百里者半九十