通过defineProperty 实现对数据的访问修改进行拦截
先来看看defineProperty的用法:
const o = {}
Object.defineProperty(o, 'property1', {
value: 42,
writable: false,
});
console.log(o)
结果如下:
其中,defineProperty是Obeject原型对象上的一个函数,入参包含三个,Object.defineProperty(obj, prop, descriptor)
obj 是要定义属性的对象,prop是一个字符串或者symbol,指定key值。descriptor是一个的对象,用于实现拦截的核心就在这里;这个对象包含一下可选值:value
,writable
,get
,set
,如果描述符没有 value、writable、get 和 set 键中的任何一个,它将被视为数据描述符。如果描述符同时具有 [value 或 writable] 和 [get 或 set] 键,则会抛出异常。
明白了基本的用法,接下来,就是通过在get和set当中添加我们的拦截动作去实现监听数据变化,参考Vue高级进阶课程,实现一个简单的对象属性拦截
let data = {
name: '柴柴老师',
age: 18,
height:180
}
// 遍历每一个属性
Object.keys(data).forEach((key)=>{
// key 属性名
// data[key] 属性值
// data 原对象
defineReactive(data,key,data[key])
})
// 响应式转化方法
function defineReactive(data,key,value){
Object.defineProperty(data,key,{
get(){
return value
},
set(newVal){
value = newVal
}
})
}
解析:初始化时,data对象中有一些初始值;然后遍历data中的属性,把每个key和value丢进defineReeactive函数中;在这个函数中,调用 Object.defineProperty
,为每一个key添加get和set函数,巧妙的是,这里每次调用defineReeactive
时都实现了一个闭包,把初始值当作一个变量传进去,get和set两个函数对其有访问,内存中会一直存在着这个变量,后续对属性的访问以及修改都要经过get和set函数,修改和访问的其实都是这个保存在内存中的变量。也可以直接修改成这样,闭包存在于get和set两个函数对value变量的访问过程,而不是defineReactive函数的调用
let data = {
name: '柴柴老师',
age: 18,
height:180
}
// 遍历每一个属性
Object.keys(data).forEach((key)=>{
// key 属性名
// data[key] 属性值
// data 原对象
let value = data[key]
Object.defineProperty(data,key,{
get(){
return value
},
set(newVal){
value = newVal
}
})
})
至此,我们大致理解了如何修改对象的属性以达到对对象属性访问和修改时,能够进行一些我们的操作
那么vue在整个应用构建的过程中,是如何实现一步步构建整个双向数据绑定,自动化实现数据响应式的呢?obersever,watcher,dep又是什么?带着这些疑问,接着往下学习
发布订阅模式
在进一步学习前,先来学习一个设计模式
发布订阅模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。
前面说到,vue在初始化数据时,会遍历数据中每个属性,通过defineProperty给属性添加拦截,以便实现自己的某些操作,这些操作实际是和响应式息息相关的,当数据第一次初始化到dom中时,会计算数据的值触发get函数,此时我们可以拿到dom的信息以添加依赖到相应的结构中;当数据变化时,会触发set函数,此时把先前的依赖依次拿出来更新数据;这样,就实现了数据层到视图层的数据绑定,初始化时构建依赖,数据变化时根据依赖去更新dom;但是,这样还不够,因为视图层到数据层的数据绑定还没有实现,比如一个输入框,用户修改数据之后,如何通知数据层去修改数据呢?其实这个是小问题,只要检测一下用户的操作数据是什么,然后调用一下set函数即可。v-model就是对这个的封装;
从故事的最开始说起
先来了解一下生命周期便于后续的学习:vue
生命周期
一个vue页面从开始构建到展示在我们眼前,从一个new Vue()
开始。构造函数会执行 this._init,在 _init 中会进行合并配置、初始化生命周期、事件、渲染等,最后执行 vm.$mount 进行挂载。
(引入observer)
其中,created完成之后,vue实例已经创建,data以及method方法都可以访问到了,也就是说,给数据添加响应式的拦截已经初始化好了。我们具体来看看怎么实现的。这里引入Observer
这个概念,意为数据的观察者。在beforeCreated进行初始化数据的时候,在vue实例内部创建了一个observer实例,这个实例内部,其实做的事情也不复杂,就是把我们前面大篇幅说的给属性添加get,set进行拦截封装起来。具体来说,就是递归地把 data 对象和子对象添加 ob 属性,这个属性通过我们熟知的 defindReactive 为属性定义 getter/setter。
(引入dep)
与之前说的一样,我们可以在get和set中进行一些操作,这些操作就涉及到接下来要介绍的dep
,可以理解为数据和依赖的中间信使,记录着数据和依赖的关联;先来看一下dep类的简单源码
// src/core/observer/dep.js
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
depend () {}
notify () {}
}
在深入看里面是什么前,我们先来看看dep在什么地方创建
function defineReactive(data,key,value){
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
dep.depend()
//...
},
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
}
在defineReactive
中,创建了dep的实例,在get和set中分别利用闭包特性访问了其depend和notify函数,回到前面dep类的源码,这两两个函数其实是在往subs数组里面push和update元素,元素是什么呢?是下面将要介绍的watcher
。到这里,只需要知道,oberserve封装着把数据实现响应式所需要的数据拦截的实现,每一个数据属性内部都有一个dep负责管理依赖。距离数据响应式还剩下最后一步!
(引入watcher)
响应式数据和依赖管理都准备好了,接下来就需要 Wacther
来订阅了。watcher意为订阅者。当数据初始化和变更,伴随着依赖的生成和调用,dep负责管理这些依赖,这些依赖具体而言就是watcher,每一个watcher,就是一个回调函数,用于同步数据层和视图层的数据,内部通过闭包来绑定node和数据。初始化时候,在beforeMounted时 vm.$mount 挂载,会执行 mountComponent 方法,Watcher 就是在这里实例化的。具体怎么实例化的呢?和编译
有关,后续会聊到编译,现在只需要了解,在挂载的过程中,会对每一个dom扫描,包括但不限于指令,属性等等,然后对于每一个需要绑定数据的地方,都会生成一个watcher。watcher的创建伴随着计算数据的值调用数据的get,然后把watcher丢进dep的subs数组中;watcher的触发伴随着数据的set,然后dep的subs数组全部watcher触发一遍,把所有依赖此数据的视图都更新
(引入编译)
编译就是一个函数,在初始化dom结构的时候调用,把dom结构和数据联系起来,对dom树进行扫描,凡是涉及到数据,指令,属性等等的地方,都创建watcher(就是一个函数,操作dom的值和数据同步),并把watcher填入对应数据的dep的subs中;看看大佬写的简单的编译函数
// 编译函数
function compile() {
let app = document.getElementById('app')
// 1.拿到app下所有的子元素
const nodes = app.childNodes // [text, input, text]
//2.遍历所有的子元素
nodes.forEach(node => {
// nodeType为1为元素节点
if (node.nodeType === 1) {
const attrs = node.attributes
// 遍历所有的attrubites找到 v-model
Array.from(attrs).forEach(attr => {
const dirName = attr.nodeName
const dataProp = attr.nodeValue
console.log(dirName, dataProp)
if (dirName === 'v-text') {
console.log(`更新了${dirName}指令,需要更新的属性为${dataProp}`)
node.innerText = data[dataProp]
// 收集更新函数
dep.collect(dataProp, () => {
node.innerText = data[dataProp]
})
}
})
}
})
}
至此,数据的响应式已经完成,回顾整个流程,首先是利用defineProperty对所有访问操作数据进行拦截,把这个部分封装成observer;在dom树挂在的时候进行编译,绑定dom操作的node实例对象和数据,并把这个函数分装成watcher;在初始化访问数据时候调用get,自动把watcher填入dep,在修改数据时自动执行dep的notify触发所有依赖于此数据的watcher,实现数据更新
响应式构建过程:
联系vue的MVVM模型
vue的MVVM模型:模型(Model)、视图(View)和视图模型(ViewModel),model可以理解为数据层,view则为视图层,viewModel在中间负责两边沟通的桥梁