vue源码学习13:数组是如何驱动视图更新的?

·  阅读 237
vue源码学习13:数组是如何驱动视图更新的?

在前面的两篇文章中,我学习并实现了vue的响应式更新和依赖收集,并且了解了nextTick的原理。

现在的问题是:数组的变化驱动页面渲染还没有实现。

今天要学习的主要内容就这这一部分啦。😁

知识点回顾,究其原因

let methods = [
    'shift',
    'unshift',
    'pop',
    'push',
    'splice',
    'reverse',
    'sort'
]

// 改写的用自己的,没有改写的用原来的。
methods.forEach(method => {
    arrayMethods[method] = function (...args) {
        // 这里的args是传入的参数列表
        // 比如arr.push(1,2,3),则 ...args = [1, 2, 3]
        oldArrayPrototype[method].call(this, ...args)
        let inserted;
        let ob = this.__ob__;
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                inserted = args.slice(2);
            default:
                break;
        }
        if (inserted) {
            // 如果有新增的值,就继续劫持,这里要观测的是数组的每一项
            ob.observeArray(inserted)
        }
    }
})
复制代码

在之前的学习中,了解了vue中的数据劫持有下面几个特性

  • 在Vue中,对象通过defineProprety实现响应式。
  • 针对数组,是对几个变异方法进行了重写。(通过上面的一段代码可以看出,我在之前的学习文章# vue的数组为啥只能用变异方法?index和length得罪了谁?一文中,也做了详细的阐述)
  • 对象新增属性的时候,也不会被监听,因为新增属性的时候,不会走get和set,没有进行依赖收集

根据上面几个特性,在vue中使用对象和数组有两点需要注意:

1. 对于数组的操作,只能使用变异方法才可以驱动视图的变化

2. 对于对象的操作,如果是新增的话,只能使用$set处理,才可以驱动视图的变化

正文:数组的处理

对于数组,因为无法象对象那样在get的时候对依赖进行收集,从而也无法在set中去notify视图变化。

所以需要在对数组的key进行observer的时候,在这个属性本身挂载一个dep,以便于后期在操作数组的时候,调用dep的notify更新视图。

举个例子:

<div id="app">
    {{arr}}
</div>

let vm = new Vue({
    // 按找个套路,Vue就是一个类
    el: '#app',
    data() {
        return { arr: [[1, 2]] }
    }
});
复制代码

给对象或者属性挂载一个dep

在这个例子中,会对arr这个变量进行劫持,劫持的时候,给这个变量挂载一个dep

class Observer {
    constructor(data) {
        this.dep = new Dep()
        Object.defineProperty(data, '__ob__', {
            value: this,
            enumerable: false // 不可枚举
        })
        // ...
    }

    observeArray(data) {
        // 省略对数组的劫持代码
    }
    walk(data) {
        // 省略定义对象响应式的代码
    }
}
复制代码

让watcher记录dep

当vue对每一个属性使用defineProperty进行重写的时候,让这个对象或者是数组也记录当前的这个watcher

function defineReactive(data, key, value) {
    let childOb = observe(value)
    let dep = new Dep()
    Object.defineProperty(data, key, {
        get() {
            console.log('key', key)
            if (Dep.target) {
                dep.depend()
                // childOb可能是数组,也可能是对象,对象也要收集,后续写$set的时候需要用到它自己的更新操作
                if (childOb) { 
                    childOb.dep.depend() // 让数组和对象也记录watcher
                }
            }
            return value
        },
        set(newV) {
            // 省略代码...
        }
    })
}

export function observe(data) {
    //  省略代码...
    if (data.__ob__) {
        // 返回这个已经代理过的数据
        return data.__ob__
    }
    // 返回代理的数据
    return new Observer(data)
}
复制代码

用编译方法调用

当用户调用push、pop等变异方法的时候,会进入重写方法

这时候说明用户操作数据,需要调用ob.dep.notity(),重新渲染页面

// 改写的用自己的,没有改写的用原来的。
methods.forEach(method => {
    arrayMethods[method] = function (...args) {
        // 省略一系列重写方法...
        let ob = this.__ob__;
        ob.dep.notify()
    }
})
复制代码

「Tip:回顾一下notify做了什么」

notify就是让这个dep里面的subs记录的watcher全部重新更新,也就是依赖了这个变量的视图全部更新。

class Dep {
    // ...省略代码
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}
复制代码

数组套数组 [[[]]] 的形式更新

在上面的代码中,我实现了单维数组的视图响应式更新,但是多维数组响应式更新还没有实现。

思路:

当发现Observer的数据是一个数组的时候,对数组进行处理,如果是多维数组,就对其进行递归处理,逐一执行depend方法。


function defineReactive(data, key, value) {
    ...
    Object.defineProperty(data, key, {
        get() {
            if (Dep.target) { 
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    // 新增下面的代码,对数组进行处理
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        }
        ...
    })
}

function dependArray(value) {
    for (let i = 0; i < value.length; i++) {
        let current = value[i] // current 是数组里面的数组
        console.log('current', current)
        current.__ob__ && current.__ob__.dep.depend()
        if (Array.isArray(current)) {
            dependArray(current)
        }
    }
}
复制代码

截止到这里,就完成了数组的视图更新了。

后记

通过今天的学习,我更加深刻的理解了Vue数据驱动视图更新的原理,也知道了Vue中多维数组是通过递归进行对数据进行监听的。

在实践中需要注意以下几点:

  • Vue中,如果给对象不存在的属性赋值,不能使用this.a.b = 1,而应该通过this.$set(a, b, 1)
  • Vue中,对于数组的操作需要使用7中变异方法,而不应该使用index下标长度处理
  • Vue中,数组的项如果是对象,直接修改其对象,也可以更新视图
  • Vue中,尽量使用扁平化数据,如果数组或者对象的嵌套太深,会导致大量的递归,性能下降

往期文章

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改