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

栏目: 编程语言 · 发布时间: 5年前

内容简介:之前我的一篇文章既然深拷贝的原理有点像递归, 其实就是遇到引用类型,调用自身函数再次解析。

之前我的一篇文章 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

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 技术内幕


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数学与泛型编程

数学与泛型编程

[美]亚历山大 A. 斯捷潘诺夫(Alexander A. Stepanov)、[美]丹尼尔 E. 罗斯(Daniel E. Rose) / 爱飞翔 / 机械工业出版社 / 2017-8 / 79

这是一本内容丰富而又通俗易懂的书籍,由优秀的软件设计师 Alexander A. Stepanov 与其同事 Daniel E. Rose 所撰写。作者在书中解释泛型编程的原则及其所依据的抽象数学概念,以帮助你写出简洁而强大的代码。 只要你对编程相当熟悉,并且擅长逻辑思考,那么就可以顺利阅读本书。Stepanov 与 Rose 会清晰地讲解相关的抽象代数及数论知识。他们首先解释数学家想要解决......一起来看看 《数学与泛型编程》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器