• 【前端源码解析】数据响应式原理


    参考:Vue 源码解析系列课程

    源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/Data_Reactive_Study

    课程目的:彻底弄懂 Vue2 的数据更新原理

    数据响应式

    MVVM 模式:

    侵入式 和 非侵入式:

    Object.defineProperty()

    参考文档:Object.defineProperty() - JavaScript | MDN (mozilla.org)

    Object.defineProperty() 用于数据劫持 / 数据代理,利用 JavaScript 引擎赋予的功能,检测对象属性变化。

    该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

    var obj = {}
    
    Object.defineProperty(obj, 'a', {
        value: 3, // 值
        writable: false, // 只读
        enumerable: true, // 可枚举
    })
    
    console.log(obj.a) // 3
    obj.a++ // 只读的,不能修改
    console.log(obj.a); // 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    该方法的 getter / setter 需要变量周转才能工作:

    var obj = {}
    var temp // 临时变量
    
    Object.defineProperty(obj, 'a', {
        get() {
            console.log('get a');
            return temp
        }, 
        set(newVal) {
            console.log('set a',  newVal);
            temp = newVal
        }
    })
    
    obj.a = 1
    obj.a++ 
    console.log(obj.a) // 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    自定义一个 defineReactive 函数,使用闭包,就不需要设置临时变量:

    export default function defineReactive(obj, key, val) {
        if (arguments.length == 2) {
            val = obj[key]
        }
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚举
            configurable: true, // 可配置
            get() {
                console.log('get', key);
                return val
            },
            set(newVal) {
                console.log('set', key, newVal);
                if (val === newVal) return
                val = newVal
            }
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    let obj = {}
    
    // 实现了数据响应式
    defineReactive(obj, 'a', 1)
    console.log(obj.a); // 1
    obj.a = 10
    console.log(obj.a); // 10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    递归检测对象全部属性

    对于如下对象,使用上面的 defineReactive 是无法监听 obj.a.m.n 的属性的(只能监听一层,无法监听多层)

    let obj = {
        a: {
            m: {
                n: 1
            }
        },
        b: 1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    想要达到的效果:通过 observe 使 obj 所有属性都变成响应式的

    observe(obj)
    obj.b++ // 响应式
    obj.a.m.n++ // 响应式
    
    • 1
    • 2
    • 3

    程序流程图:(通过各级函数之间的调用实现了递归的效果)

    observe.js

    /**
     * 将 obj 所有属性变为响应式
     */
    export default function observe(obj) {
        if (!obj || typeof obj !== 'object') return
        var ob;
    	// 判断是否已经是响应式
        if (typeof obj.__ob__ != 'undefined') {
            ob = obj.__ob__
        } else {
            ob = new Observer(obj)
        }
        return ob
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Observer.js

    /**
     * 将一个正常的 object 转换成每个层级的属性都是响应式的 object
     */
    export default class Observer {
        constructor(obj) {
            console.log('Observer constructor', obj);
            // 构造函数中的 this 不是类本身,而是表示实例
            def(obj, '__ob__', this, false)
            // 将 object 中的属性转换成响应式的属性
            this.walk(obj)
        }
        // 遍历 object 的属性,将其转换成响应式的属性
        walk(obj) {
            console.log('walk', obj);
            for (let k in obj) {
                defineReactive(obj, k)
            }
        }
    }
    
    /**
     * 对 Object.defineProperty 的封装
     */
    export const def = function (obj, key, value, enumerable) {
        Object.defineProperty(obj, key, {
            value,
            enumerable,
            writable: true,
            configurable: true
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    defineReactive.js

    /**
     * 将数据变为响应式
     */
    export default function defineReactive(obj, key, val) {
        console.log('defineReactive', key);
        if (arguments.length == 2) {
            val = obj[key]
        }
    
        // 子元素要进行 observe,至此形成递归
        // 这个递归是多个函数、类循环调用
        let childOb = observe(val)
    
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚举
            configurable: true, // 可配置
            get() {
                // console.log('get', key);
                return val
            },
            set(newVal) {
                console.log('set', key, newVal);
                if (val === newVal) return
                val = newVal
                // 当设置了新值,这个新值也要被 observe
                childOb = observe(newVal)
            }
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    效果:

    let obj = {
        a: {
            m: {
                n: 1
            }
        },
        b: 1
    }
    
    observe(obj)
    obj.b++ // 响应式
    obj.a.m.n++ // 响应式
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    数组的响应式原理

    对于数组对象,以上实现是无法监听其 pushpop 等元素修改方法的:

    let obj = {
    	c: [1, 2, 3, 4]
    }
    
    • 1
    • 2
    • 3

    改写 7 个方法:pushpopshiftunshiftsplicesortreverse

    array.js

    // 数组的原型
    const arrayPrototype = Array.prototype
    
    // 以 Array.prototype 为原型创建 arrayMethods 对象,并暴露
    export const arrayMethods = Object.create(arrayPrototype)
    
    // 要被改写的 7 个数组方法
    const methodsNeedChange = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ]
    
    // 数组方法实际是自己写的,自己写的再调用真实的数组方法,中间可以拦截数据
    methodsNeedChange.forEach(methodName => {
        // 备份原来的方法
        const originMethod = arrayPrototype[methodName]
        // 给原型定义新的方法
        def(arrayMethods, methodName, function () {
            console.log('arrayMethods', methodName);
    
            // 执行原来的函数
            const result = originMethod.apply(this, arguments)
            // arguments 是伪数组对象,转成数组对象
            const args = [...arguments]
    
            // 把数组身上的 __ob__ 取出来
            const ob = this.__ob__
    
            // 有三种方法 push / unshift / splice 可以插入新项
            // 要将插入的新项也变为 observe 的
            let inserted = []
    
            switch (methodName) {
                case 'push':
                case 'unshift':
                    inserted = args
                    break;
                case 'splice':
                    // splice(下标, 数量, 插入的新项)
                    inserted = args.slice(2)
                    break;
            }
    
            // 判断有没有要插入的新项,让新项也变为响应的
            if (inserted) ob.observeArray(inserted)
    
            return result
        }, false)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    修改 Observer.js 中构造 Observer 的代码:

    /**
     * 将一个正常的 object 转换成每个层级的属性都是响应式的 object
     */
    export default class Observer {
        constructor(obj) {
            console.log('Observer constructor', obj);
            // 构造函数中的 this 不是类本身,而是表示实例
            def(obj, '__ob__', this, false)
            // 将 object 中的属性转换成响应式的属性
            if (Array.isArray(obj)) {
                // 将数组的原型指向 arrayMethods
                Object.setPrototypeOf(obj, arrayMethods)
                // 让数组变的 observe
                this.observeArray(obj)
            } else {
                this.walk(obj)
            }
        }
        // 遍历 object 的属性,将其转换成响应式的属性
        walk(obj) {
            console.log('walk');
            for (let k in obj) {
                defineReactive(obj, k)
            }
        }
        // 数组的特殊遍历
        observeArray(arr) {
            for (let i = 0, l = arr.length; i < l; i++) {
                // 逐项进行 observe
                observe(arr[i])
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    使用效果:

    let obj = {
        c: [1, 2, 3, 4]
    }
    
    observe(obj)
    obj.c.push(55, 66, 77)
    obj.c.splice(1, 1, [1, 2])
    console.log(obj.c);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    依赖收集

    什么是依赖?需要用到数据的地方,称为依赖

    • Vue 1.x 中,依赖是细粒度的,用到数据的 DOM 都是依赖
    • Vue 2.x 中,依赖是中等粒度的,用到数据的 组件 是依赖
    • 在 getter 中收集依赖,在 setter 中触发依赖

    Dep 类 和 Watcher 类:

    • Dep 类封装了依赖收集的代码,专用用来管理依赖,每个 Observer 的实例,成员中都有一个 Dep 的实例
    • Watcher 是一个中介,数据发生变化时通过 Watcher 中转,通知组件

    • 依赖就是 Watcher,只有 Watcher 触发的 getter 才会收集依赖,哪个 Watcher 触发了 getter,就会把哪个 Watcher 收集到 Dep
    • Dep 使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的 Watcher 都通知一遍
    • 代码实现的巧妙之处:Watcher 把自己设置到全局的一个指定位置,然后读取数据,因为读取到了数据,所以会触发这个数据的 getter。在 getter 中就能得到当前正在读取数据的 Watcher,并把这个 Watcher 收集到 Dep 中。

    参考文章:Vue深入响应式原理

    使用效果:

    let obj = {
        a: 1,
        b: {
            m: {
                n: 1
            }
        },
        c: [1, 2, 3, 4]
    }
    
    observe(obj)
    
    // 监控依赖
    new Watcher(obj, 'b.m.n', val => {
        console.log('Watcher 在监控 b.m.n', val)
    }) 
    
    obj.b.m.n++ 
    console.log(obj)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    Dep.js

    /**
     * 全局唯一的 依赖收集器
     */
    export default class Dep {
        constructor() {
            // console.log('Dep constructor'); 
            this.id = uid++
    
            // 用数组存储自己的订阅者,这个数组中存放 Watcher 实例
            this.subs = [] // subscribers
        }
        // 添加订阅
        addSub(sub) {
            this.subs.push(sub)
        }
        // 添加依赖
        depend() {
            // Dep.target 就是自己指定的全局的位置(window.targte 也可以,全局唯一即可)
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }
        // 通知更新
        notify() {
            console.log('Dep notify'); 
            // 浅克隆一份
            const subs = this.subs.slice()
            for (let i = 0, l = subs.length; i < l; i++) {
                subs[i].update()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    Watcher.js

    var uid = 0
    
    export default class Watcher {
        // 监听 target 对象的 expression 属性,执行 callback 回调
        constructor(target, expression, callback) {
            // console.log('Watcher constructor');
            this.id = uid++
            this.target = target
            this.getter = parsePath(expression) // 解析 expression 为一个函数
            this.callback = callback
            this.value = this.get()
        }
        update() {
            // console.log('Watcher update');
            this.run()
        }
        get() {
            // 进入依赖收集阶段,让全局 Dep.tartget 设置为 Watcher 本身
            Dep.target = this
    
            const obj = this.target
            // 只要没找到,就一直找
            let value
            try {
                value = this.getter(obj)
            } finally {
                Dep.target = null
            }
    
            return value
        }
        run() {
            this.getAndInvoke(this.callback)
        }
        getAndInvoke(cb) {
            const value = this.get()
    
            if (value !== this.value || typeof value == 'object') {
                const oldValue = this.value
                this.value = value
                cb.call(this.target, value, oldValue)
            }
        }
    }
    
    // 返回一个可以解析 "a.b.c" 格式的函数
    // let fn = parsePath('a.b.c')
    // fn({ a: { b: { c: 1 } } })
    function parsePath(str) {
        var segments = str.split('.');
        return obj => {
            for (let i = 0; i < segments.length; i++) {
                if (!obj) return
                obj = obj[segments[i]];
            }
            return obj
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    其余文件代码省略…

    完整源码参考:https://gitee.com/szluyu99/vue-source-learn/tree/master/Data_Reactive_Study

  • 相关阅读:
    现代_复习_第1章:行列式
    风格迁移篇---重用鉴别器进行编码:朝向无监督的图像到图像转换
    前端实现微信扫一扫的思路
    警惕!出现这些表现,你的亲人/朋友正在认真考虑自杀
    Rust 最常用函数
    flutter版本dart版本对应关系
    Spring——依赖注入
    算法设计_综合练习_编程题
    智慧用电监控装置:引领0.4kV安全用电新时代
    Python 自定义函数的基本步骤
  • 原文地址:https://blog.csdn.net/weixin_43734095/article/details/125488301