我们都知道在vue2中,响应式是发布订阅模式结合Object.defineProperty实现的。但是面试中往往回答到这里是远远不够的。最近准备面试,所以就梳理一下vue2中的响应式的实现原理,一步一步剖析,并以代码的形式记录下来,希望对自己对大家有所帮助。由于是第一次分析源码,可能有些地方不深入,甚至会有错误,多多包涵,发现有错的地方希望大家能指出来,共同进步。
这里简单实现一个响应式
首先创建一个Obeserve类,用来实例化一个响应式对象。
class Observer{
constructor(value){
this.value = value
// 判断是数组先暂时不用管待会儿补充
if(Array.isArray(value)){
return
}
this.walk(this.value)
}
walk(obj){
const keys = Object.keys(obj);
console.log('当前监听对象的所有属性名',keys);
for(let i = 0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
get(){
console.log('属性被读取')
return val
},
set(newval){
if(newval!=val){
console.log('属性值被设置了')
val = newval
}
}
})
}
let obj = new Observer({
name:'DGT',
age:'26',
number:'18229282791'
})
到这里,一个简单的对象数据的响应式监听就完成了。 接下来要做的就是依赖收集。
我们都知道数据变更就会通知视图进行更新,但是视图很大,它是怎么知道哪部分视图应该被更新呢?肯定会想到,谁用到了这个数据就更新谁,我们可以为每一个响应式数据创建一个管理器,用来存放当前数据被哪些视图所使用(依赖),这个过程就可以被称为依赖收集。
总结:在响应式数据的getter中进行依赖收集,在setter中进行通知依赖更新。
下面我们来写一个依赖收集器用来管理收集的依赖。
class Dep{
constructor(){
this.subs = []; // 实例化的dep都有一个subs用来存放当前响应式对象所有收集到的依赖
}
addSub(sub){
this.subs.push(sub)
}
// 删除依赖
removeSub(){
remove(this.subs, sub)
}
// 添加一个依赖
depend(){
// 全局唯一target用来指定当前的依赖
if (window.target) {
this.addSub(window.target)
}
}
// 通知所有依赖更新
notify(){
const subs = this.subs.slice(); // 浅拷贝一份数据
for(let i = 0,i<subs.length;i++){
subs[i].update(); // //调用对应依赖的视图更新方法
}
}
}
/**
* Remove an item from an array
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
至此我们完成了一个简易的依赖管理器,现在改写一下我们的Observer类:
class Observer{
constructor(value){
this.value = value
// 判断是数组先暂时不用管待会儿补充
if(Array.isArray(value)){
return
}
this.walk(this.value)
}
walk(obj){
const keys = Object.keys(obj);
console.log('当前监听对象的所有属性名',keys);
for(let i = 0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}
function defineReactive(obj,key,val){
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj,key,{
get(){
console.log('属性被读取')
dep.depend(); // 进行依赖收集
return val
},
set(newval){
if(newval!=val){
console.log('属性值被设置了')
val = newval
dep.notify(); // 进行统治依赖收集更新
}
}
})
}
let obj = new Observer({
name:'DGT',
age:'26',
number:'18229282791'
})
现在大体上明白了,Observer类跟Dep和dep之间的关系。 我们刚刚说了dep中存放的是收集到的依赖,但是这些依赖到底长什么样子呢?
在vue中创建了一个Watcher类,Watcher类的实例就是当前依赖数据的那个依赖。所以dep中存放的就是当前响应式对象属性的watcher。
总结一下:当读取数据的时候,调用dep.depend()方法,将当前watcher进行收集,存放在dep中。当数据变化的时候,调用dep.notify()方法通知收集到的watcher去执行视图的更新。
Watcher类的实现如下:
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
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
当我们实例化一个watcher的时候,会调用get方法,将当前的实例挂载在全局属性target中,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,getter中当判断有window.target的时候会进行依赖收集。收集完后执行window.target = undefined;以免二次收集。 以上就是vue2中响应式的原理,以及依赖收集等方面的原理。