教你如何写 Vue 源码中关于`数据驱动`的实现

350 阅读2分钟

# 前言

我们在一开始的使用Vue的时候,是通过Vue.js的形式来引入的,如下:

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

<script src="./vue.js"> </script>
<script>
    const app = new Vue({
        el: "#app",
        data:{
            counter: 1
        }
    });
    
    setInterval(_=>{
        app.counter++;
    },1000);
</script>

今天我们先实现第一步:app.counter++可以正常累加。

# 实现我们自己的 MyVue 类

这里会用到上篇文章写的数据响应式的一些实现原理,大家可以参考一下

myVue.js 文件

// 定义响应式属性
function defineReactive(obj, key, val) {
    // 递归监测
    observe(val);

    // 定义响应式
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key);
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}

// 遍历响应式处理
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    };

    Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}

class MyVue {
    constructor(options) {
        // 保存选项
        this.$options = options;
        this.$data = options.data;

        // 对 data 实现数据响应式
        observe(options.data)
    }
}

以上代码要是实现了两步:

  1. 实现选项$options$data的预存储,方便以后使用
  2. 实现对options.data中所有属性进行响应式处理

准备好以后,我们执行以下代码:

<div>
    {{counter}}
</div>
<input type="text" name="" id="">
<script src="./src/myVue.js"></script>
<script>
    const app = new MyVue({
        el: "#app",
        data: {
            counter: 1
        }
    });

    setInterval(_ => {
        app.counter++;
    }, 1000);
</script>

预期结果应该是{{counter}}未被编译,直接展示,而定时器会正常执行,并每隔一秒会输出我们定义响应式时defineReactive方法里的getset里的console输出。

结果是{{counter}}未被编译直接展示没有问题,但console未正常输出,这是为什么呢?

1650533675(1).png

我们改造下代码,把定时里的app.counter改成app.$data.counter,如下:

 setInterval(_ => {
    app.$data.counter++;
}, 1000);

改造后就可以正常输出了,如下图:

1650534105(1).png

原因也很简单:因为我们只对options.data做了响应式,没有对MyVue实例做响应式,需要我们用一个代理方法默认去把options.data中的属性代理到MyVue类中的this上,这个代理的方法我们叫作proxy,代码如下:


// 定义响应式属性
function defineReactive(obj, key, val) {
    // 递归监测
    observe(val);

    // 定义响应式
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key, obj);
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}

// 遍历响应式处理
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    };

    Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
}

// 将传入的对象中的所有key都代理到指定的对象上
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key];
            },
            set(v) {
                vm.$data[key] = v;
            }
        })
    })
}

class MyVue {
    constructor(options) {
        // 保存选项
        this.$options = options;
        this.$data = options.data;
        
        // 对 data 实现数据响应式
        observe(options.data);

        // 做代理
        proxy(this)
    }
}

这时我们再去执行定时器的代码,如下:

 setInterval(_ => {
    app.counter++
}, 1000);

可以正常执行了,如下图:

1650534105(1).png

到这里我们已经实现了简单的数据驱动,看过源码的小伙伴都知道,Vuedata分两种形式:数组和对象,我们今天就是模拟实现了对象类型的,那怎么加个判断呢?对不同的数据类型执行不同的观测逻辑,其实这就用到了另一个类:Observer,我们一起改造下代码,如下:

// 定义响应式属性
function defineReactive(obj, key, val) {
    // 递归监测
    observe(val);

    // 定义响应式
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key, obj);
            return val;
        },
        set(newVal) {
            if (newVal != val) {
                console.log('set', newVal)
                val = newVal;
            }
        }
    })
}

// 遍历响应式处理
function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return obj;
    };

    // 创建观测实例
    new Observer(obj);
}

// 将传入的对象中的所有key都代理到指定的对象上
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key];
            },
            set(v) {
                vm.$data[key] = v;
            }
        })
    })
}

class Observer {
    constructor(obj) {
        if (Array.isArray(obj)) {
            // 数组类型的响应式观测
        } else {
            // 非数组类型的响应式观测
            this.walk(obj);
        }
    }
    walk(obj) {
        Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]));
    }
}

class MyVue {
    constructor(options) {
        // 保存选项
        this.$options = options;
        this.$data = options.data;
        // 对 data 实现数据响应式
        observe(options.data);

        // 做代理
        proxy(this)
    }
}

改造完成后,再次执行如下代码,应该会正常执行,没有什么变化:

 setInterval(_ => {
    app.counter++
}, 1000);

好了到此就简单先实现了Vue数据驱动的原理,希望对你理解Vue有所帮助。下一篇 教你如何写 Vue 源码中关于数据驱动的实现 我们来实现表达式编译、依赖收集、Watcher,欢迎大神们留言评论,nice ~~