手写vue(二)响应式实现

名词解释:

vm:指Vue实例

一、目标效果

  1. vue定义

(1)新建vm时,可以通过一个data对象,或者data函数,其属性可以通过vm直接访问,而data对象可以通过vm._data获取

(2)修改vm._data.xxx时,等价于修改this.xxx

  1. 响应式对象

(1)能够监听到data对象属性的修改

(2)能够监听到data下对象属性的属性

  1. 响应式数组

(1)能够监听到通过数组方法修改数组

(2)数组中保存的对象,也能被监测修改

(3)不监听数组通过下标修改

测试先行:

    
    

二、Vue实例定义

  1. 参考Vue源码,使用构造函数的方式,通过创建传入一个配置对象生成实例

  1. option传给vue之后,直接通过this.$option挂载到示例上,方便后面,读取配置项进行初始化

  1. 初始化单独在一个init文件中进行

import init from "./init"

function Vue(option) {
    // 挂载到vms上
    this.$option = option
    init(this)
};

export default Vue

三、在Vue实例中定义data属性

data中的值,可以通过多个地方修改,但修改的结果时同步的,此时就不能直接粗暴地挂载在vm上:例如:

要把data.foo挂载到this.foo,如果直接使用this.foo = data.foo,则执行this.foo = newVal之后,data.foo还是旧的值。

由上面的反例可得,我们要挂载的内容不是foo的值,而是data的foo这个属性,通过this.foo修改值,应该同步到data.foo上去

通过Object.defineProperty可以动态地给对象添加属性,读取的值和返回的值都可以通过getter、setter自己定义,所以我们获取、修改值时,直接去操作data对象。

// init.js
function initData(vm) {
    const opt = vm.$option
    let data = opt.data
    if (typeof data === 'function') {
        data = data()
    }
    // 在vm上定义_data和data中的数据
    defineGlobal(vm, '_data', { _data: data })
    Object.keys(data).forEach(key => {
        defineGlobal(vm, key, data)
    })
}

/**
 * 在vm上定义代理对象
 * @param {*} vm vue实例
 * @param {*} key 需要在实例上定义的key
 * @param {*} source 需要被代理的源对象
 */
function defineGlobal(vm, key, source) {
    Object.defineProperty(vm, key, {
        configurable: true,
        enumerable: true,
        get() {
            return source[key]
        },
        set(v) {
            source[key] = v
        }
    })
}

四、对象监听

与vue挂载data类似,可以使用Object.defineProperty去代理data中的所有属性,并且递归地去代理所有的子属性,通过setter就可以监听属性的修改。

具体实现:创建一个Observer类,需要监听的对象作为参数传入构造函数,然后在这个类的构造函数中去给对象添加响应式。添加响应式监听之后,把类实例对象挂到监听对象中去,作为一个已经被监听的标志位,也直接使用监听的相关方法,否则,其实直接通过方法处理就足够了,并不需要作为一个类。

// init.js
function initData(vm) {
    ...
    // 观察data中内容
    observe(data)
    ...
}

// observe/index.js
class Observer {
    constructor(obj) {
        if (Array.isArray(obj)) {
            ...
        } else {
            this.observeObj(obj)
        }
        // 在被监听的对象上,定义已被监听的标志位,指向Observer对象自身
        // 在其它文件中,也方便直接使用监听的相关方法
        Object.defineProperty(obj, '__ob__', {
            value: this
        })
    }

    /**
     * 监听对象
     * @param {Object} obj 需要观察的对象
     */
    observeObj(obj) {
        Object.keys(obj).forEach(key => {
            const val = obj[key]
            // 尝试监听对象的属性
            observe(val)
            // 定义响应式
            defineReactive(obj, key)
        })
    }
}

// 导出的方法

/**
 * 
 * @param {Object} obj 需要监听的对象
 * @returns 
 */
export function observe(obj) {
    if (typeof obj !== 'object') {
        return null
    }
    if (obj.__ob__) {
        return obj.__ob__
    }
    return new Observer(obj)
}

/**
 * 给对象添加响应式
 * @param {Object} obj 需要定义响应式的对象
 * @param {String} key 对象的属性key
 */
export function defineReactive(obj, key) {
    let value = obj[key]
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: true,
        get() {
            return value
        },
        set(val) {
            console.log('set data', obj, key, val);
            value = val
        }
    })
}

五、数组监听

由于数组长度可能比较大,如果对数组的每个对象都进行响应式监听,则性能损耗太高,比如数组长度为1万,则需要对一万个属性的进行响应式监听:Object.defineProperty(arr, i, {})

由于数组对数组进行操作,主要时通过push、pop、splice等等方法去修改的,通过下标直接修改比较少,因此只监听数组的方法调用进行监听。

具体实现:

举例,我们要监听对象arr的push方法,不能直接重写原型对象方法arr.__proto__.push = XXX,因为arr._proto指向的是Array.prototype, 直接修改后,所有数组对象的push方法都会被修改

利用原型链中会逐级查找属性的特点,我们可以创建一个对象,在这个对象中重新定义push等方法,然后,需要监听的数组,只需要原型对象指我们创建的数组原型重写对象,就可以实现监听

而这个对象的原型对象应该指向Array对象,因为我们并不需要重写所有的数组方法,而是只需要重写一部分会修改原数组的方法,并且,我们重新定义的方法,最后也是要调用Array中的原方法是实现方法逻辑的,相当于对方法进行了一层代理,每次调用时,能够通知到我们,做一些操作。

// array.js
// 需要监听的方法
const observeMethods = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]
const arrayProto = Array.prototype
// 创建一个空对象,原型对象指向Array的原型属性上
const obArrayProto = Object.create(arrayProto)

// 重写需要监听的方法
observeMethods.forEach(methodName => {
    const originalMethod = arrayProto[methodName]
    obArrayProto[methodName] = function (...args) {
        let inserted
        // 新增元素的方法,需要给新元素添加响应式
        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
            default:
                break;
        }

        const observe = this.__ob__
        if (inserted) {
            observe.observeArray(inserted)
        }
        console.log(methodName, args);
        // 需要使用call,因为originalMethod方法没有通过XXX对象去调用,因此,如果直接执行,方法中的this会指向window,在严格模式下会指向undefined
        // 通过call方法调用,让originalMethod方法内的this指向数组对象,因为这个方法是通过arr.XXX()调用的
        return originalMethod.call(this, ...args)
    }

})

/**
 * 监听数组方法
 * @param {Array} arr 
 */
export function observeArrayMethod(arr) {
    // 数组的原型对象指向重写了部分方法的新的原型对象
    Object.setPrototypeOf(arr, obArrayProto)
}

// ********************** observe/index.js ********************
import { observeArrayMethod } from "./array"

class Observer {
    constructor(obj) {
        if (Array.isArray(obj)) {
            this.observeArray(obj)
        } else {
            ...
        }
        ...
    }

    /**
     * 监听数组
     * @param {Array} arr 需要观察的数组
     */
    observeArray(arr) {
        // 监听数组的修改方法
        observeArrayMethod(arr)
        // 尝试监听数组内的所有元素
        arr.forEach(observe)
    }
}

总结:

目录结构:

手写vue(二)响应式实现_第1张图片

gitee源码地址:

https://gitee.com/ZepngLin/my-vue/tree/%EF%BC%88%E4%BA%8C%EF%BC%89%E5%93%8D%E5%BA%94%E5%BC%8F%E5%AE%9E%E7%8E%B0

你可能感兴趣的:(Vue,vue.js,javascript)