vue响应式原理学习(二)— Observer的实现

之前我的一篇文章vue响应式原理学习(一)讲述了vue数据响应式原理的一些简单知识。 众所周知,Vuedata属性,是默认深度监听的,这次我们再深度分析下,Observer观察者的源码实现。

先写个深拷贝热热身

既然data属性是被深度监听,那我们就首先自己实现一个简单的深拷贝,理解下思路。

深拷贝的原理有点像递归, 其实就是遇到引用类型,调用自身函数再次解析。

function deepCopy(source) {
    // 类型校验,如果不是引用类型 或 全等于null,直接返回
    if (source === null || typeof source !== 'object') {
        return source;
    }

    let isArray = Array.isArray(source),
        result = isArray ? [] : {};
        
    // 遍历属性
    if (isArray) {
        for(let i = 0, len = source.length; i < len; i++) {
            let val = source[i];
            // typeof [] === 'object', typeof {} === 'object'
            // 考虑到 typeof null === 'object' 的情况, 所以要加个判断
            if (val && typeof val === 'object') {
                result[i] = deepCopy(val);
            } else {
                result[i] = val;
            }
        }
        // 简写 
        // result = source.map(item => {
        //     return (item && typeof item === 'object') ? deepCopy(item) : item
        // });
    } else {
        const keys = Object.keys(source);
        for(let i = 0, len = keys.length; i < len; i++) {
            let key = keys[i],
                val = source[key];
            if (val && typeof val === 'object') {
                result[key] = deepCopy(val);
            } else {
                result[key] = val;
            }
        }
        // 简写
        // keys.forEach((key) => {
        //     let val = source[key];   
        //     result[key] = (val && typeof val === 'object') ? deepCopy(val) : val;         
        // });
    }
    
    return result;
}
复制代码

为什么是简单的深拷贝,因为没考虑 RegExp, Date, 原型链,DOM/BOM对象等等。要写好一个深拷贝,不简单。

有的同学可能会问,为什么不直接一个 for in 解决。如下:

function deepCopy(source) {
    let result = Array.isArray(source) ? [] : {};
    
    // 遍历对象
    for(let key in source) {
        let val = source[key];
        result[key] = (val && typeof val === 'object') ? deepCopy(val) : val;
    }

    return result;
}
复制代码

其实 for in有一个痛点就是原型链上的非内置方法也会被遍历。例如开发者自己在对象的 prototype上扩展的方法。

又有的同学可能会说,加 hasOwnProperty 解决呀。如果是 Object 类型,确实可以解决,但如何是 Array 的话,就获取不到数组的索引啦。

说到 for in,再加个注意项,就是 for in 也是可以 continue 的,而数组的 forEach 方法不可以。因为 forEach的内部实现是在一个for循环中依次执行你传入的函数。

分析 Vue 的 Observer

这里我主要是为代码添加注释,建议看官们最好打开源码来看。

代码来源:Vue项目下的 src/core/observer/index.js

Vue 将 Observer 封装成了一个 class

扫描二维码关注公众号,回复: 4539996 查看本文章
Observer
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that has this object as root $data

    constructor(value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // 每观察一个对象,就在对象上添加 __ob__ 属性,值为当前 Observer 实例
        // 当然,前提是 value 本身是一个数组或对象,而非基础数据类型,如数字,字符串等。
        def(value, '__ob__', this)   
        
        // 如果是数组
        if (Array.isArray(value)) {
            // 这两行代码后面再讲解
            // 这里代码的作用是 为数组的操作函数赋能
            // 也就是,当我们使用 push pop splice 等数组的api时,也可以触发数据响应,更新视图。
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)
            
            // 遍历数组并观察
            this.observeArray(value)
        } else {
            // 遍历对象并观察
            // 这里会有存在 value 不是 Object 的情况,
            // 不过没事,Object.keys的参数为数字,字符串时 会 返回一个空数组。
            this.walk(value)
        }
    }

    // 遍历对象并观察
    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            // 观察对象,defineReactive 函数内部调用了 observe 方法, 
            // observe 内部 调用了 Observer 构造函数
            defineReactive(obj, keys[i])
        }
    }

    // 遍历数组并观察
    observeArray(items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            // 观察对象,observe 内部 调用了 Observer 构造函数
            observe(items[i])
        }
    }
}

function protoAugment(target, src: Object, keys: any) {
    target.__proto__ = src
}

function copyAugment(target: Object, src: Object, keys: Array<string>) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}
复制代码

上面的代码中,细心的同学可能对observedefdefineReactive这些函数不明所以,接下来说说这几个函数

observe 函数

用来调用 Observer构造函数

export function observe(value: any, asRootData: ?boolean): Observer | void {
    // 如果不是对象,或者是VNode实例,直接返回。
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    // 定义一个 变量,用来存储 Observer 实例
    let ob: Observer | void
    // 如果对象已经被观察过,Vue会自动给对象加上一个 __ob__ 属性,避免重复观察
    // 如果对象上已经有 __ob__属性,表示已经被观察过,就直接返回 __ob__
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else if (
        shouldObserve &&       // 是否应该观察
        !isServerRendering() &&  // 非服务端渲染
        (Array.isArray(value) || isPlainObject(value)) &&     // 是数组或者Object对象
        Object.isExtensible(value) &&     // 对象是否可扩展,也就是是否可向对象添加新属性
        !value._isVue // 非 Vue 实例
    ) {
        ob = new Observer(value) 
    }
    if (asRootData && ob) {  // 暂时还不清楚,不过我们可以先忽略它
        ob.vmCount++
    }  
    return ob  // 返回 Observer 实例
}
复制代码

可以发现 observe 函数,只是 返回 一个 Observer 实例,只是多了些许判断。为了方便理解,我们完全可以把代码缩减:

// 这就清晰多了
function observe(value) {
    let ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.___ob___
    } else {
        ob = new Observer(value) 
    }
    return ob;
}
复制代码
def 函数

其实就是 Object.defineProperty 的封装

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
    Object.defineProperty(obj, key, {
        value: val,
        // 默认不可枚举,也就意味着正常情况,Vue帮我们在对象上添加的 __ob__属性,是遍历不到的
        enumerable: !!enumerable,  
        writable: true,
        configurable: true
    })
}
复制代码
defineReactive 函数

defineReactive函数的功能较多,主要是用来 初始化时收集依赖改变属性时触发依赖

export function defineReactive(
    obj: Object,     // 被观察对象
    key: string,     // 对象的属性
    val: any,        // 用户给属性赋值
    customSetter?: ?Function,   // 用户额外自定义的 set
    shallow?: boolean           // 是否深度观察
) {
    // 用于收集依赖
    const dep = new Dep()

    // 如果不可修改,直接返回
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }
    
    
    // 如果用户自己 未在对象上定义get 或 已在对象上定义set,且用户没有传入 val 参数
    // 则先计算对象的初始值,赋值给 val 参数
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }

    // !shallow 表示 深度观察,shallow 不为 true 的情况下,表示默认深度观察
    // 如果是深度观察,执行 observe 方法观察对象
    let childOb = !shallow && observe(val)
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            // 获取对象的原有值
            const value = getter ? getter.call(obj) : val
            
            // 收集依赖。收集依赖和触发依赖是个比较大的流程,日后再说
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            
            // 返回对象的原有值
            return value
        },
        set: function reactiveSetter(newVal) {
            // 获取对象的原有值
            const value = getter ? getter.call(obj) : val

            // 判断值是否改变
            // (newVal !== newVal && value !== value) 用来判断 NaN !== NaN 的情况
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            
            // 非生产环境,触发用户额外自定义的 setter
            if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter()
            }
            
            // 触发对象原有的 setter,如果没有的话,用新值(newVal)覆盖旧值(val)
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }

            // 如果是深度观察,属性被更改后,重新观察
            childOb = !shallow && observe(newVal)
            
            // 触发依赖。收集依赖和触发依赖是个比较大的流程,日后再说
            dep.notify()
        }
    })
}
复制代码
入口在哪

说了这么多,那Vue观察对象的初始化入口在哪里呢,当然是在初始化Vue实例的地方了,也就是 new Vue 的时候。

代码来源:Vue项目下src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)    // 这个方法 定义在 initMixin 函数内
}

// 就是这里,initMixin 函数会在 Vue 的 prototype 上扩展一个 _init 方法
// 我们 new Vue 的时候就是执行的 this._init(options) 方法
initMixin(Vue)  

stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
复制代码

initMixin 函数在 Vue.prototype 上扩展一个 _init 方法,_init方法会有一个initState函数进行数据初始化

initState(vm)   // vm 为当前 Vue 实例,Vue 会将我们传入的 data 属性赋值给 vm._data 
复制代码

initState 函数会在内部执行一段代码,观察 vm实例上的data属性

代码来源:Vue项目下 src/core/instance/state.js。无用的代码我先注释掉了,只保留初始化 data 的代码。

export function initState(vm: Component) {
    // vm._watchers = []
    // const opts = vm.$options
    // if (opts.props) initProps(vm, opts.props)
    // if (opts.methods) initMethods(vm, opts.methods)
    
    // 如果传入了 data 属性
    // 这里的 data 就是我们 new Vue 时传入的 data 属性
    if (opts.data) {    
        // initData 内部会将 我们传入的 data属性 规范化。
        // 如果传入的 data 不是函数,则直接 observe(data)
        // 如果传入的 data 是函数,会先执行函数,将 返回值 赋值给 data,覆盖原有的值,再observe(data)。
        // 这也就是为什么我们写组件时 data 可以传入一个函数
        initData(vm)    
    } else {
        // 如果没传入 data 属性,观察一个空对象
        observe(vm._data = {}, true /* asRootData */)
    }
    
    // if (opts.computed) initComputed(vm, opts.computed)
    // if (opts.watch && opts.watch !== nativeWatch) {
    //     initWatch(vm, opts.watch)
    // }
}
复制代码
总结

我们 new Vue 的时候 Vue 对我们传入的 data 属性到底做了什么操作?

  1. 如果我们传入的 data 是一个函数,会先执行函数得到返回值。并赋值覆盖 data。如果传入的是对象,则不做操作。
  2. 执行 observe(data)
    • observe 内部会执行 new Observer(data)
    • new Observer(data) 会在 data对象 上扩展一个不可枚举的属性 __ob__,这个属性有大作用。
    • 如果 data 是个数组
      • 执行 observeArray(data)。这个方法会遍历data对象,并对每一个数组项执行observe之后的流程参考第2步
    • 如果 data 是对象
      • 执行 walk(data)。这个方法会遍历data对象,并对每一个属性执行 defineReactive
      • defineReactive 内部会对传入的对象属性执行 observe之后的流程参考第2步

篇幅和精力有限,关于 protoAugmentcopyAugment的作用,defineReactive 内如何收集依赖与触发依赖的实现,日后再说。

文章内容如果有错误之处,还请指出。

参考:

JavaScript 如何完整实现深度Clone对象

Vue 技术内幕

猜你喜欢

转载自juejin.im/post/5c15de1be51d4514a87dc672