vue会自动通过状态生成dom,然后将其输入到页面上显示出来,这个过程叫做渲染。vue的渲染过程是声明式的,我们通过模板来描述状态和dom之间的映射关系。
通常在运行时应用的内部状态是不断改变的,在改变的过程中要不断地进行重新渲染。那么要知道他们内部状态什么时候发生改变,就要进行变化侦测!变化侦测要对每个对象的每个属性进行侦测。其中的每个属性都会设置一个数组来收集订阅该属性的,收集到的叫做依赖,对于这个数组来说可能订阅这个有很多,一开始每个使用这个数据的dom节点都会被当成依赖收集进去,但是每个组件可能有多个地方使用这个数据,如果有大量的dom节点都使用了这个数据,那么在存储的数组的内存上消耗是很大的。而且如果要通知更新,要去遍历,内存消耗也是很大的。所以在vue2.0之后,依赖收集的是组件为单位的了,这样就明显可以减少依赖的数量。
关于追踪变化,大多数了解vue的都会知道Object.defineProperty和es6的Proxy。由于使用Object.defineProperty存在很多缺陷,所以尤雨溪说以后也要对其进行重写。但是接下来我们还要根据书上的学,使用object.defineProperty,因为即使使用Proxy重写,但是原理也是相似。
直奔主题,定义响应式
将一个对象、他的属性和属性所对应的值传进defineReactive(定义响应式的意思),当去获取这个对象的时候,就会触发getter,如果去设置这个属性的值得时候就会触发setter。那么就是说在获取的时候,在getter里面可以进行操作,来记录是谁来获取这个属性的值,并且把它记录起来保存在一个数组中,这样就会记录下来所有尝试获取这个属性值的‘东西’。另外每个获取这个属性的都会被记录下来,那么当这个属性值改变的时候,我们就去那个记录的数组中通知这些所有的‘东西’去把之前获取的这个值更新成新的值。
/**
*
* @param {*} data 数据对象
* @param {*} key 对象的key值
* @param {*} value 对象key所对应的值
* 这里只是以一个属性为例,因为要完成所有属性的响应式要进行遍历的
*
*
*/
function defineReactive (data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,//是否可枚举,也就是是否可以遍历
configurable: true,//是否可以删除或者修改
get: function () {//获取的时候,可以获取
return value
},
set: function (newValue) {//设置的时候如果新值不等于旧值重新赋值
if (value !== newValue) {
value = newValue
}
}
})
}
(下面我们将一个属性的值改变称为状态的改变,因为书中介绍的就是这样写的) 刚刚说到尝试获取这个属性值的这个‘东西’,这个称为依赖,就是每一个去获取的都是一个依赖,都会被推入一个数组中,每一个具体的依赖是一个dom节点。当状态发生改变的时候,会通知每一个依赖,然后进行dom更新操作。但是,如果每个dom都是一个依赖的话,我们想一想,一个组件里面可以有好多节点去获取这个属性的值,那么这个存储依赖的数组就会变得很多,每个状态绑定的依赖越多,依赖追踪在内存上的开销就越大,vue2.0增加了虚拟dom,之后依赖并不是一个dom节点了,而是一个组件。这样状态改变后,会通知到组件,组件内部再使用虚拟dom进行对比。这样大大降低了依赖数,降低了追踪依赖所消耗的内存。
如何进行收集依赖?
之所以观察数据,就是当数据改变的时候,要通知那些曾经使用了这个数据的地方更新状态
例如
<template>
<div>{{name}}</div>
</template>
当name发生该改变的时候,要通知使用他的地方(vue2.0中模板使用数据等同于组件使用数据,当数据发生便化的时候会通知组件,组件再根据虚拟dom重新进行渲染)
另外每一key都对应一个数组来存储当前订阅key的依赖。假设依赖是一个函数,保存再window.target上,就可以将之前的函数改造一下
function defineReactive (data, key, value) {
let dep = [];//新增收集依赖
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
if (window.target) {//新增收集依赖
dep.push(window.target)
}
return value;
},
set: function (newValue) {
if (newValue !== value) {
dep.forEach(item => { //新增,通知每个依赖函数更新值
item(newValue, value)
})
value = newValue
}
}
})
}
为了增强dep的独立性,给dep封装一个类,专门来管理依赖,包含添加依赖,通知更新,另外还有删除依赖的
其实关于这是发布订阅模式,订阅是依赖订阅这个属性状态,发布是这个存储的数组里所有的依赖进行更新
class Dep {
constructor() {
this.subs = []
}
addSub (sub) {//添加依赖的时候
this.subs.push(sub)
}
depend () {
if (window.target) {
this.addSub(window.target)
}
}
removeSub () {//删除某个依赖的时候
remove(this.subs, sub)
}
//发布更新的时候,上面的时候我们说把依赖是挂在到全局的window.target上的,我们要通知window.target让他取调用自己updata方法去更window.target新
notify () {
const subs = this.subs.slice()
for (let i = 0; i < subs.length; i++) {
subs[i].updata()
}
}
}
// 删除依赖的操作
function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
arr.splice(index, 1)
}
}
}
// 以上这个集订阅,发布和删除的于一身的Dep类封装完毕,接下来就是defineReactive的修改
function defineReactive (data, key, value) {
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
return value
},
set (newValue) {
if (value !== newValue) {
dep.notify()
value = newValue
}
}
})
}
上面所提到,依赖是windo.target,而且当时我们假设他是个函数,而且当我们发布的时候还调用了他的updata方法,那么依赖window.target到底是什么
我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既可以是模板,也有可能是用户写的一个watch , 这时需要抽象一个能够集中处理这些情况的类.然后我们再收集依赖的阶段只收集封装好的这个类的实例进来,通知也通知他一个,接着他在负责通知其他用到数据的地方,而这个就是watch
* watch的用法:vm.$watch(path , function(value , oldValue){}) , vm代表的是一个组件 , a.b.c代表属性的字符串 , 后面的function是data.a.b属性发生变化的时候的回调
/**
*
* @param {*} path
* 这里面path是一个字符串'a.b.c'
* 要将他转化成取值的操作,这只是个解析简单的路径的
*/
let reg = /^\w.$/
function parsePath (path) {
if (reg.test(path)) return
const sefment = path.split('.')
console.log(sefment);
// sefment=['a' , 'b' , 'c']所以应该是先是a , 然后是a.b, a.b.c
return function (obj) {
for (let i = sefment; i < sefment.length; i++) {
if (!obj) return
obj = obj[obj[i]]
}
return obj
}
}
class Watcher {
constructor(vm, path, cb) {
this.vm = vm;
// this.getter返回的是一个函数,这个函数执行的时候会获取obj.a.b.c里面的数据
this.getter = parsePath(path);
this.cb = cb
this.value = this.get()//初始化获取到的数据,如果没有更新状态,这里是新的,更新了状态之后这里存的是旧值,要用get到的新值覆盖
}
get () {
window.target = this
// 在下面的一步中this.vm执行继承了getter函数(其实this.vm指的就是这个组件),并且执行了该函数,将this.vm作为参数穿进去,另外这一步是获取值的过程,在获取的时候会触发我们定义的响应式里的收集依赖的操作,这个时候window.target是this,也就是watcher这个实例!!将window.target收集完了之后,将其置为空。第二个参数this.vm,把这个组件上所有的挂载的数据事件等传过去
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return
}
// 上面说到了,状态dep只是通知到watcher中,而watcher应该来通知所有的来更新,另外存储在this.value中的数据是旧的,get到的才是新的
updata () {
const oldValue = this.value;
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
另外,我们先打印一下一个组件的this
我们可以看到,那个红色的是我定义methods里的方法和data里面的数据,都是挂载到这里的,this.vm就是这个,将一整个组件传入 ,这样let value = this.getter.call(this.vm, this.vm)就很好理解了。
parsePath返回的一个函数,函数里面有个obj对象,this.getter.call里面的第二个参数就是obj,所以obj能拿到数据就很正常了,一开始我也比较困惑。
最后我们要对每个属性做到响应式,意思就是遍历所有的属性值,如果遇到对象的话,再对其进行劫持(所谓的劫持就是new Observe)
//以上是对一个属性的封装 , 接下来要递归所有的key
class Observe {
constructor(value) {
this.value = value
//因为关于定义响应式有两种情况,这是第一种对象的情况,数据的情况下次再讲
if (!Array.isArray()) {
this.walk(value)
}
}
walk (value) {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
this.defineReactive(value, keys[i], value[keys[i]]);
}
}
defineReactive (data, key, value) {
if (typeof value === 'object') {
new Observe(valued)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
dep.depend();
return value
},
set (newValue) {
if (value !== newValue) {
value = newValue
dep.notify();
}
}
})
}
}
现在终于可以对这个图进行解读了,以前都还是搞不懂的。
首先,在beforeCreate阶段会对data进行数据劫持,数据劫持(observer)就是将data里面所有的对象属性转化为setter/getter,在这里是没有进行模板编译环节的,是不会产生watcher的。
到了模板编译环节的时候,会根据模板上的有v-的值,到data中去获取,这样就触发了getter,getter触发之后会将这个watcher实例加入到订阅的对应的属性的数组中。当组件状态进行改变的时候,那么就会到这个属性保存的数组中去通知watcher , watcher再去集中更新数据,然后在页面上改变数据。大概就是这个样子。
以上来源于对深入浅出vue.js,有些内容是自己的理解,如果有错误,请指正。