1. 变化侦测是啥
当组件中的状态发生了变化,Vue会天然的知道状态变化了,并根据状态生成DOM进行渲染.
通常,在应用运行时,内部的状态在不停的变化,那该如何知道是哪些状态发生了变化呢?
变化侦测(也有叫数据绑定的?)就是来解决这个问题的.
不同于React和Angular的'拉'的方式,Vue属于'推'.当状态发生变化时,Vue立马就知道了,而且知道是哪些状态发生了变化.
假如说有一个状态name绑定着好多个依赖,每个依赖就是一个DOM节点,当name发生变化时,Vue会向依赖name的所有依赖发送通知,让DOM节点进行更新操作.
但是这样做会有一定的代价,如果每一个状态都绑定着很多的依赖,每次更新操作时的依赖追踪的开销会很大.因此,Vue从2.0开始引入了虚拟DOM,每个状态所绑定的依赖不再是某个DOM节点,而是一个组件.当状态发生变化时,通知到组件即可,组件内部再使用虚拟DOM进行比对.
2. 咋就知道数据变了呢
如何侦测到数据变化了呢?
好吧,其实你们已经都知道了,那就是使用强大的Object.defineProperty和ES6的Proxy.只是目前ES6在浏览器的兼容性并不理想,到目前为止Vue仍使用Object.defineProperty来实现(尤大大说以后会用Proxy进行重写的,不过无妨,原理都是一样的)
那么,尝试封装一个函数吧:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if (val === newVal) return
val = newVal
}
})
}
defineReactive翻译一下叫做定义响应式,已经很直白了,好伐?每当从obj中读取key属性时,都会触发get函数;每当给obj的key属性设置数据时,都会出发set函数
3. 依赖是啥
如果只是定义了defineReactive,并没有什么卵用(反手就给我两巴掌).
让我们用劫持的思想考虑一下,是不是可以在get和set函数里面搞一些事儿呢?
答案是:必~须~的~
举个糖炒例子:
<template>
<span>{{name}}</span>
</template>
模板中用到了name数据,那必然是先在初始化时获取name,那必然就会触发get函数;当给name赋值时,必然会触发set函数.
捋顺了,在get函数中收集依赖,在set函数中通知依赖,那就尝试定义一个Dep类吧:
let id = 0
function Dep () {
this.id = id ++
this.subs = []
}
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(sub)
}
Dep.prototype.depend = function depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
Dep.prototype.notify = function notify () {
const subs = this.subs.slice()
for (let i = 0; i < subs.length; i ++ ) {
subs[i].update()
}
}
Dep.prototype.removeSub = function removeSub (sub) {
//从数组中删除
remove(this.subs, sub);
};
Dep.target = null
}
然后再修改一下defineReactive:
function defineReactive(data, key, val) {
const dep = new Dep() //新增
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
//新增
if (Dep.target) {
dep.depend()
}
return val
},
set: function (newVal) {
if (val === newVal) return
val = newVal
dep.notify() //新增
}
})
}
Dep.target是个啥?Dep.prototype.depend和Dep.prototype.notify都是干嘛用的?
简单来说,Dep.target是个全局唯一,完全可以用window.target来表示.Dep.prototype.depend嘛,就是收集依赖,依赖就保存在Dep.target里,再调用依赖的addDep来建立关系.Dep.prototype.notify就是当数据发生变化了,通知依赖.
这时候你就会想,是不是感觉缺点什么?这个依赖貌似很有用的样子,是不是要封装一下?
function Watcher(vm, expOrFn, cb) {
this.vm = vm
this.cb = cb
this.value = this.get()
this.depIds = {}
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parseGetter(expOrFn.trim())
}
}
Watcher.prototype.addDep = function (dep) {
if(!this.depIds.hasOwnProperty(dep.id)){
dep.addSub(this)
this.depIds[dep.id] = dep
}
}
Watcher.prototype.get = function () {
Dep.target = this
const value = this.getter.call(this.vm, this.vm)
Dep.target = null
return value
}
Watcher.prototype.update = function () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
function parseGetter (exp) {
const bailRE = /[^\w.$]/
if (bailRE.test(exp)) return
const exps = exp.split('.')
return function (obj) {
for (let i = 0; i < exps.length; i++) {
if (!obj) return
obj = obj[exps[i]]
}
return obj
}
}
在get方法中先把Dep.target设置为this,紧接着读取表达式的值,肯定会触发表达式的get方法
触发了get,就会进入if(Dep.target),将watcher和dep建立关系,dep里用subs保存所有依赖于这个表达式的watcher,watcher里用depId保存着所观察的dep,并且不会重复添加
这样Vue就知道当前表达式变化的时候,要通知哪一个watcher
接着修改表达式的值,触发set方法,dep.notify()会通知dep中的每一个watcher去执行update方法
update方法会把旧值和新值传递给callback
是不是有小伙伴对parseGetter有点好奇,其实并不复杂,内部就是一个循环读取,举个例子:
当前表达式为'a.b.c',经过循环之后,最后的obj为c的值
4.递归侦测
经过以上的折腾,我们终于可以侦测data中某一个数据的某一个属性了,对,只是某一个不是全部.
我们希望定一个类,在对data数据进行劫持的时候,对每一个数据都进行侦测:
function Observer(value) {
this.value = value
if(!Array.isArray(value)){
this.walk(value)
}
}
Observer.prototype.walk = function (obj) {
const keys = Object.keys(obj)
for (let i = 0; i< keys.length; i ++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
修改一下defineReactive:
function defineReactive(data, key, val) {
if(typeof val === 'object'){
new Observer(val)
}
...省略代码
Observe目前只考虑Object的情况,Array下一篇章再讲.
要想侦测一个object中的每一个属性,只需要使用递归就好了,在劫持之前,先判断当前的val是否是对象,如果是,就去递归.如果不是,那就放心的劫持吧~
经过这样的折腾,只要将任何一个object传到Observe中,那么这个object就是一个响应式的了.欧耶!
5. 有啥问题
由于是使用Object.defineProperty的方式来变化侦测,那么就会有些问题:
比如,在data中定义了一个obj,它天然就是响应式的
obj: {
name: 'ntyang'
}
但是,当我们这样操作时,Vue无法侦测到这种变化
methods: {
handler(){
this.obj.address = 'bj'
delete this.obj.name
}
}
Object.defineProperty只能设置getter/setter来追踪修改和获取,并不能追踪到新增或删除(除非来一个deleter/adder?).不过也别怕,Vue提供了vm.$set
和vm.$delete
两个API来帮助我们实现,以后会讲的哦
参考:
2.<深入浅出Vue.js> 刘博文著 人民邮电出版社
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。