8. 数组更新的实现原理

数组更新实现原理

之前我们给每个属性添加了dep, 让dep去收集依赖

当使用push方法改变数组的时候, 并没有改变属性, 而是改变了数组本身

因此, 需要让数组,或对象和属性一样, 也能收集依赖, 并且在检测到数组变化的时候触发更新

在Observer 类中对每一个对象对进行依赖收集

class Observer {
    constructor(data) {
        // 给每个对象都进行依赖收集
        this.dep = new Dep()
        ...
    }
 }

在定义响应式方法的时候, 如果对value的再次劫持有返回值, observe(value), 说明是一个对象或数组, 需要对对象或数组进行依赖收集, 如果有数组的嵌套, 则需要递归收集

function dependArray(value) {
    for(let i = 0; i < value.length; i ++) {
        let current = value[i];
        // current 可能也是数组,或对象(对象不用管) 也可能不是数组,不是数组就没有__ob__属性
        current.__ob__ && current.__ob__.dep.depend()
        if(Array.isArray(current)) {
            dependArray(current)
        }
    }
}

export function defineReactive(target, key, value) {
    // 再次劫持
    let childOb = observe(value)    // 这个return的其实是一个Observer的实例, 只有对象和数组才有, 其他return false
    // 给每个属性都加上dep, 这个空间不会被销毁, 是个闭包
    let dep = new Dep()
    // 再次劫持
    Object.defineProperty(target, key, {
        get() {
            // 执行vm._render的时候会在这里取值, 此时Dep.target指向当前渲染watcher
            if(Dep.target) {
                // 如果有Dep.target, 执行dep上面的depend方法
                dep.depend()


                if(childOb) {
                    childOb.dep.depend()
                    if(Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        ...
    }

在数组发生改变时, 调用对应dep的notify方法,进行视图的更新

// 先copy旧的数组
let oldArrayProto = Array.prototype

// 定义并导出新的原型
export let newArrayProto = Object.create(oldArrayProto)

// 能改变数组的7种方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']

methods.forEach(method => {
    newArrayProto[method] = function(...args) {
        const result = oldArrayProto[method].call(this, ...args)

        // 需要对新增的内容做处理, 新增的方法有 : push, unshift, 和 splice
        let inserted
        // 从实例上获取__ob__属性
        let ob = this.__ob__    
        switch(method) {
            case 'push':
            case   'unshift':
                inserted = args
                break

            case 'splice':
                inserted = args.slice(2)

            default:
                break
        }

        // 方法里面的this, 是谁调用push, this指向谁
        // 这里的this其实就是 Observer 里面 constructor里面的参数data, 在data里面添加一个__ob__属性, 指向类的实例, 就能从this上面获取observeArray了
        if(inserted) {
            ob.observeArray(inserted)
        }

        console.log('mrthios:', method)

        ob.dep.notify()     // 数组变化了, 也要触发更新

        return result
    }
})

vue中给不存在的属性赋值时, 不能实现更新, 有个专门的api $set

// a: {a: 1}
// vm.a.b = 100    不能更新

vm.a.__ob__.dep.notify()
// 这个就是$set的实现原理

具体代码

新建文件 6.array.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>array</title>
</head>
<body>
    <div id="app">
        {{arr}}
        <div>{{a}}</div>
    </div>
    <script src="vue.js"></script>
    <script>
        // 新建一个vue实例
        const vm = new Vue({
            el: '#app',
            data() {
                return {
                   arr: [1,2,3, [4,5,6]],
                   a: {a: 1}
                }
            }
        })
        // 一般使用数组很少使用 arr[1] = 100, arr.length = 10 这两种方式修改数组, vue2同样也不支持
        // vue2中实现数组响应式的方法是重写能改变数组的7个方法
        // 特殊情况: 形如: arr: [1,2,3, {num: 100}], 这种数组里面有嵌套对象的, 也要能劫持对象的属性, 添加get和set
        // 特殊情况2: arr: [1,2,3], arr.push({num: 20}), 在能改变数组的方法中, 如push一个对象, 那这个对象的属性也需要被劫持
        // vm.arr.push(100)
        // vm.arr[3].push(39)
        vm.a.b = 20
        // vm.a.__ob__.dep.notify()
    </script>
</body>
</html>

observe/index.js


// 初次里面传过来的是一个对象, 后续可能是null, 或数据, 只需要对对象进行劫持, 数组不需要
// 就是传说中的Object.defineProperty方法, 不过就提的实现是在一个类中, 这里就是判断 + 返回一个class

import { newArrayProto } from "./array"
import { Dep } from "./dep"

// observe 方法 就是对对象进行监控, 添加set和get, 很多地方都有用到
export function observe(data) {
    if(typeof data !== 'object' || data === null) {
        return 
    }

    // 如果有被标识, 直接返回data__ob__, 这个data__ob__就是一个Observer实例, 同下面一样(new Observer())
    if(data.__ob__ instanceof Observer) {
        return data.__ob__
    }

    // 这里的劫持使用的是一个类, 因为里面需要给被劫持的对象添加很多属性, 
    return new Observer(data)

}


class Observer {
    constructor(data) {
        // 给每个对象都进行依赖收集
        this.dep = new Dep()

        // 2) 在data上增加一个属性__ob__, 指向this,便于在array.js里面使用Observer里面的observeArray方法
        // 同样这个属性可以作为一个标识, 表明属性是否有被劫持过, 可在observe方法中新增一个判断
        // 注意这个属性不可枚举, 不然会死循环
        Object.defineProperty(data, '__ob__', {
            value: this,
            enumerable: false
        })

        // 1) 针对对象和数组做区分
        if(Array.isArray(data)){
            // 数组需要实现两个内容: 
            // 1. 数组方法重写, 新建文件实现 array.js
            // 2. 遍历数组, 劫持其中的对象, 并称响应式
            data.__proto__ = newArrayProto
            this.observeArray(data)
        } else {
            // 对对象的属性劫持放在walk方法中
            this.walk(data)
        }
    }

    // 遍历对象, 给每一个属性添加响应式, defineReactive不在class中, 因为其他地方也可能用到, 写了外面
    walk(data) {
        Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
    }

    // 循环数组, 找出其中的对象(这异步已经封装在observe方法里面, 不用做), observe每一项
    observeArray(data) {
        data.forEach(item => observe(item))
    }
}

function dependArray(value) {
    for(let i = 0; i < value.length; i ++) {
        let current = value[i];
        // current 可能也是数组,或对象(对象不用管) 也可能不是数组,不是数组就没有__ob__属性
        current.__ob__ && current.__ob__.dep.depend()
        if(Array.isArray(current)) {
            dependArray(current)
        }
    }
}

export function defineReactive(target, key, value) {
    // 再次劫持
    let childOb = observe(value)    // 这个return的其实是一个Observer的实例, 只有对象和数组才有, 其他return false
    // 给每个属性都加上dep, 这个空间不会被销毁, 是个闭包
    let dep = new Dep()
    // 再次劫持
    Object.defineProperty(target, key, {
        get() {
            // 执行vm._render的时候会在这里取值, 此时Dep.target指向当前渲染watcher
            if(Dep.target) {
                // 如果有Dep.target, 执行dep上面的depend方法
                dep.depend()


                if(childOb) {
                    childOb.dep.depend()
                    if(Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set(newValue) {
            if(value === newValue) return
            observe(newValue)
            value = newValue
            // 设置新值的时候通知更新
            dep.notify()
        }
    })
}

array.js

// 重写数组方法
// 不能干掉旧的方法
// 重新定义方法, 就是在执行push 方法的时候, 调用旧的push方法,注意this的指向问题,以及返回值 然后在添加一些自己的内容
// 对新增的内容(肯定是数组)进行observeArray   ??? 注意进行observeArray是哪里来的


// 先copy旧的数组
let oldArrayProto = Array.prototype

// 定义并导出新的原型
export let newArrayProto = Object.create(oldArrayProto)

// 能改变数组的7种方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']

methods.forEach(method => {
    newArrayProto[method] = function(...args) {
        const result = oldArrayProto[method].call(this, ...args)

        // 需要对新增的内容做处理, 新增的方法有 : push, unshift, 和 splice
        let inserted
        // 从实例上获取__ob__属性
        let ob = this.__ob__    
        switch(method) {
            case 'push':
            case   'unshift':
                inserted = args
                break

            case 'splice':
                inserted = args.slice(2)

            default:
                break
        }

        // 方法里面的this, 是谁调用push, this指向谁
        // 这里的this其实就是 Observer 里面 constructor里面的参数data, 在data里面添加一个__ob__属性, 指向类的实例, 就能从this上面获取observeArray了
        if(inserted) {
            ob.observeArray(inserted)
        }

        console.log('mrthios:', method)

        ob.dep.notify()     // 数组变化了, 也要触发更新

        return result
    }
})


posted @ 2022-06-27 01:28  littlelittleship  阅读(74)  评论(0编辑  收藏  举报