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

参考: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

该方法的 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

自定义一个 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
        }
    })
}
let obj = {
    
    }

// 实现了数据响应式
defineReactive(obj, 'a', 1)
console.log(obj.a); // 1
obj.a = 10
console.log(obj.a); // 10

递归检测对象全部属性

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

let obj = {
    
    
    a: {
    
    
        m: {
    
    
            n: 1
        }
    },
    b: 1
}

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

observe(obj)
obj.b++ // 响应式
obj.a.m.n++ // 响应式

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

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
}

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
    })
}

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)
        }
    })
}

效果:

let obj = {
    
    
    a: {
    
    
        m: {
    
    
            n: 1
        }
    },
    b: 1
}

observe(obj)
obj.b++ // 响应式
obj.a.m.n++ // 响应式

数组的响应式原理

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

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

改写 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)
})

修改 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])
        }
    }
}

使用效果:

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);

依赖收集

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

  • 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)

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()
        }
    }
}

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
    }
}

其余文件代码省略…

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

猜你喜欢

转载自blog.csdn.net/weixin_43734095/article/details/125488301