大致了解Vue2响应式原理(二)

207 阅读6分钟

前言

在上一篇中,我们讲到了Vue在创建一个实例,需要完成的任务主要是初始化实例,其中对数据的响应式在初始化函数initData中的observe实现的,observe函数中,该对象首先判断是否已经是响应式,如果不是响应式,则添加一个一个值为Observer实例的ob属性。Observer类有三个属性,分别是value,dep,vmCount,在其构造函数中,我们会先判断value是数组还是对象,如果是对象,则通过walk方法遍历,并执行defineReactive函数;如果是数组,则先改变数组的原型链,指向arrayMethods,再遍历数组,对每个元素再调用observe方法(数组和对象分别处理原因参考大致了解Vue2响应式原理(一))。

接下来就defineReactive函数和Dep类展开描述

defineReactive函数

defineReactive主要通过defineProperty对属性进行getter和setter的重写,在get中收集依赖,在set中提醒依赖进行更新,以下是defineReactive实现代码。

export function defineReactive(
    obj:object,
    key:string,
    val?:any,
    customSetter?: Function | null, //自定义函数,在set中进行回调
    shallow?:boolean,//是否浅遍历
){
    const dep = new Dep()
    
    //此方法返回给定对象的所有属性的信息,如果不可配置,直接return
    const property = Object.getOwnPropertyDescriptor(obj,key)
    if(property && property.configurable === false){
        return
    }
    
    //用户是否有自定义get、set属性
    const getter = property && property.get
    const setter = property && property.set
    
    if(
        (!getter || setter) &&
        //如果只传递了obj和key,给val赋值
        (val === NO_INITIAL_VALUE || arguments.length === 2)
    ){
        val = obj[key]
    }
    
    //对val再通过observe递归
    let childOb = !shallow && observe(val,false)
    
    Object.defineProperty(obj,key,{
        enumerable:true, //可以被枚举
        configurable:true,//可以配置
        get:function reactiveGetter(){
            const value = getter ? getter.call(obj) : val
            if(Dep.target){
                if(__DEV__){
                    dep.depend({
                        target:obj,
                        type:TrackOpTypes.GET,
                        key
                    })
                }else{
                    dep.depend()
                }
                if(childOb){
                    childOb.dep.depend()
                    if(isArray(value)){
                        dependArray(value)
                    }
                }
            }
            return isRef(value) && !shallow ? value.value : value
        },
        set:function reactiveSetter(newVal){
            const value = getter ? getter.call(obj) : val
            if(!hasChanged(value,newVal)){
                return
            }
            if(__DEV__ && customSetter){
                customSetter()
            }
            if(setter){
                setter.call(obj,newVal)
            }else if(getter){
                return 
            }else if(!shallow && isRef(value) && !isRef(newVal)){
                value.value = newVal
                return
            }else{
                val = newVal
            }
            childOb = !shallow && observe(newVal,false)
            if(__DEV__){
                dep.notify({
                    type:TriggerOpTypes.SET,
                    target:obj,
                    key,
                    newValue:newVal,
                    oldValue:value
                })
            }else{
                dep.notify()
            }
        }
    })
    return dep
}

在defineReactive函数中,我们主要完成了以下任务:

  • 通过getOwnPropertyDescriptor获取该对象属性的所有属性,并判断是否可以对该属性进行配置
  • 定义getter、setter,通过&&,如果有property,则继续执行property.get/set ,如果没有property,则返回undefined,这种用法被称为“短路求值”
  • 如果用户没有传递val,则自定义val,通过obj[key]对val赋值
  • 重写getter、setter如下
  • getter:判断目前是否有watcher(关于watcher在后续中会提到),如果不存在则直接返回value,如果存在,则将当前的watcher加入到该value的依赖中。
  • setter:判断是否需要新旧值是否相等,相等的话不用触发更新,判断用户是否有自定义的setter,有的话就调用自定义setter,如果旧值是一个ref对象,而新值不是,则把newVal赋值给旧值的value(因为是响应式数据,取值需要.value),否则直接赋值,如果newVals是对象或者数组,则通过observe递归,最后对相关的dep对象进行更新。

Dep类

在看代码之前,我们先弄清楚Dep类的作用是什么,在Vue2中,Watcher对象依赖于各个数据,一个Watcher对象可能会依赖多个数据,而一个数据也可能被多个Watcher依赖,于是提取出这种依赖,就是Dep对象。每一个数据都有一个对应的Dep对象,当一个数据被读取的时候,会调用getter函数,于是将依赖该数据的Watcher存储至该Dep对象中便于管理。Dep和数据一一对应,Dep和Wather的关系是多对多,下面对Dep类的代码简单分析一下。

let uid = 0
const pendingCleanupDeps: Dep[] = []

export interface DepTarget extends DebuggerOptions {
    id: number
    addDep(dep: Dep): void
    update(): void
}

export default class Dep{
    static target?: DepTarget | null //当前正在执行的Watcher对象
    id: number //dep的唯一标识
    subs:Array<DepTarge | null> //依赖当前dep的所有Watcher对象
    _pending = false // 标识当前Dep对象中是否有Watcher对象被设置为null,subs数组等待被cleanupDeps
    
    constructor(){
        this.id = uid++
        this.subs = []
    }
    
    //向依赖数组中添加一个Watcher对象
    addSub(sub:DepTarget){
        this.subs.push(sub)
    }

    removeSub(sub){
        this.subs[this.subs.indexOf(sub)] = null
        //当前有Watcher对象被清除了
        if(!this._pending){
            this._pending = true
            //将Dep实例加入待清除的pendingCleanupDep数组中,这个一个Watcher对象用来记录依赖的数组
            pendingCleanupDep.push(this)
        }
    }
    
    //如果当前存在正在计算的Watcher对象进行关联,将该Dep对象,通过addDep方法添加到Watcher对象的deps属性中
    depend(info){
        if(Dep.target){
            Dep.target.addDep(this)
            if(__DEV__ && info && Dep.target.onTarck){
                Dep.target.onTrack({
                    effect:Dep.target,
                    ...info
                })
            }
        }
    }
    
    notify(info){
        const subs = this.subs.filter(s => s) as DepTarget[]
        if(__DEV__ && !config.async){
            subs.sort((a,b)=>a.id - b.id)
        }
        for(let i = 0; i < subs.length; i++){
            const sub = subs[i]
            if(__DEV__ && info){
                sub.onTrigger && sub.onTrigger({
                    effect:subs[i]
                    ...info
                })
            }
            sub.update()
        }
    }
}

Dep类中的属性

首先我们对Dep类的属性方法有一个大致的了解:Dep类有target,id,subs,pending四个属性

  • target是静态变量,是定义在Dep类上的,而不是dep实例上,这意味着所有的Dep实例都共享这一个属性target,该属性代表的是目前正在计算的Watcher对象(Watcher对象一时间只能有一个,可以理解为不管什么时候,都只有一个正在运行/计算/活跃的Watcher对象)
  • id是唯一标识
  • subs是依赖该Dep对象的所有Watcher对象
  • pending是一个清除标识符,在removeSub方法中会具体说到

Dep中的方法

接下来聊一下Dep中的方法:addSub,removeSub,depend,notify

  • addSub方法相对较为简单,传入参数Watcher对象,通过对subs数组push,将这个Watcher对象加入到依赖数组中即可
  • removeSub方法,目的是将当前Watcher对象移出依赖数组,由于直接修改subs数组会造成性能上的问题,所有在修改subs数组时,并不是直接删除,而是通过索引找到该Watcher对象在subs数组中的位置并赋值为null,然后将pending标识符置为true,然后将当前Dep对象加入到pendingCleanupDeps全局数组中,在后续统一对这个数组遍历,将数组中dep元素的sub为null的值删除,然后把pending置为false。
  • depend方法,将当前的Dep对象添加到正在计算的Watcher对象的deps属性中。如果是开发模式下,通知开发者当前正在进行的追踪操作。
  • notify方法,首先对当前的subs数组过滤值不为null的Watcher对象,遍历过滤后的数组,然后用过update方法,通知依赖该dep的Watcher对象进行更新。

结语

在本章中,我们大致了解了defineReactive以及dep类,这部分是Vue2响应式的核心原理,至于其中还提到了很多细节的函数,并未在这里一一说明,后续会慢慢补充,下一章将会对Watcher对象进行了解,再将Dep对象与Watcher对象做出更深刻的理解,并了解render函数。