likes
comments
collection
share

vue2源码解析(二):数据劫持

作者站长头像
站长
· 阅读数 21

你好呀,我是小九,很高兴见到你。

摘要

vue2源码学习之路。

第一篇文章中介绍了环境搭建,点击查看

vue2源码解析(一):环境搭建

项目结构

本文项目结构

vue2源码解析(二):数据劫持

初始化

1.创建类

vue2中,使用关键字new来创建一个vue实例,因此需要一个Vue类。

vue2源码中没有使用class,而是使用构造函数的形式。

一般来说,class是一个整体,需要把所有的方法都写在一起。

使用构造函数的形式扩展方法更灵活一些。

源码截图

vue2源码解析(二):数据劫持

按照这种形式,在src目录下创建一个index.js文件,创建一个Vue类,并将这个类导出。

完整代码

import { initMixin } from "./instance/init";

function Vue(options) {
    this._init(options);
}

// 将initMixin引入,并将Vue传过去,相当于扩展了init方法
initMixin(Vue)

// 导出Vue
export default Vue;

options 是传给Vue的配置项。

2.初始化init

一般来说,如果初始化需要调用很多代码,就会封装一个初始化函数。

vue2源码中,创建一个单独的初始化文件,解耦合,便于管理。

创建init.js文件,路径src-->instance-->init.js

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {}
}

options这个参数,后面很多地方都会用到,所以需要把options放到实例上。

在_init函数内部,需要初始化状态,将数据做一个初始化劫持,当修改数据时应该更新视图。

完整代码

import { initState } from "./state";

export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this;
        vm.$options = options;
        initState(vm);
    };
}

3.初始化状态

创建state.js文件,路径src-->instance-->state.js

初始化状态函数中,主要是针对不同情况做不同的初始化。

例如传入data,传入props,传入methods等等,需要分别初始化。

源码截图

vue2源码解析(二):数据劫持

代码实现,此处只关注data部分

import { isFunction } from "../shared/until";

export function initState(vm) {
    // 获取options
    const opts = vm.$options;

    // 针对不同情况做不同初始化
    
    // 判断是否传递了data
    if (opts.data) {
        initData(vm);
    }
    // 判断是否传递了props
    if (opts.props) {}
    // 判断是否传递了methods
    if (opts.methods) {}
    // 判断是否传递了watch
    if (opts.watch) {}
    // 判断是否传递了computed
    if (opts.computed) {}
}


function initData(vm) {
    data = isFunction(data) ? data.call(vm) : data || {};
    if (!isPlainObject(data)) {
        data = {};
        console.error("[Vue warn] data functions should return an object");
    }
}

function initProps() {}
function initMethods() {}
function initWatch() {}
function initComputed() {}

initData、initProps、initMethods等,不是公共的方法, 只是特定针对某个属性操作,所以这些方法不需要挂载到原型上,

vue2中data有两种情况,一种是对象,一种是函数。

根实例可以是对象,可以是函数,组件中data必须是函数。

判断data是不是函数,如果是函数,让data执行,并且让data中的this指向Vue实例。

无论哪种情况,data值的最外层,都是一个对象。

new Vue({
    data: {}
)

new Vue({
    data() {
        return {}
    }
)

// 组件
export default{
    data() {
        return {}
    }
}

创建until.js文件,路径src-->shared-->until.js

这个文件主要是一些公共的方法。

// 判断是不是函数类型
export function isFunction(value) {
    return typeof value === "function";
}

// 判断是不是对象类型
const _toString = Object.prototype.toString;
export function isPlainObject(obj) {
    return _toString.call(obj) === "[object Object]";
}

vue的核心是响应式处理, 因此需要做数据劫持。

数据劫持

1.对象劫持

vue2中,劫持对象使用的是Object.defineProperty(),重新设置set和get方法,取值和设置值之前可以自定义处理操作。

源码中定义了一个defineReactive函数,其中封装了Object.defineProperty()。

对象的操作有几种情况

(1)对象可能有多层级,如果某个属性值也是对象,这个值需要劫持

(2)修改某个属性值,将值设置为一个新的对象,这个新对象需要劫持

(3)对象新增了一个属性,值是一个新对象

(4)对象删除了某个属性

由于Object.defineProperty只能劫持已经存在的属性,新增的和删除的无法劫持,所以vue中单独增加了些api,解决这个问题,比如,$set$delete

此处先不做介绍。

export function defineReactive(data, key, value) {
    observer(value);
    Object.defineProperty(data, key, {
        get() {
            return value;
        },
        set(newValue) {
            // 新值和原来的值相等,不进行操作,直接返回
            if (newValue === value) return;
            observer(newValue);
            value = newValue;
        },
    });
}

这个函数是一个闭包的形式,生成不销毁的栈内存,value的值在get和set内部都可以使用。

闭包有两个作用,一是保护,二是保存。

关于闭包的更多内容,请点击查看攻克作用域和闭包

然后,创建一个Observer类,对数据进行监测。

如果是对象,就遍历对象的所有属性,对每个属性进行劫持

如果对象中包含数组,就调用数组的监测方法

class Observer {
    constructor(data) {
        if (Array.isArray(data)) {
           this.observeArray(data);
        } else {
            this.observeObject(data);
        }
    }
    observeObject(data) {
        let keys = Object.keys(data);
        keys.forEach((key) => {
            defineReactive(data, key, data[key]);
        });
    }
    observeArray(data) {
        // 数组中也可能包含对象,依次遍历数组
        data.forEach((item) => observer(item));
    }
}

对象劫持有两个条件,一是数据是对象,二是没有劫持过,劫持过就不需要重复操作。

所以添加一个标识,判断是否监测过。

在Observer类中添加一个__ob__,属性值设置为this(当前Observer的实例)。

这个属性需要设置成不可枚举,也就是说在遍历对象的时候不能被遍历到。

否则,遍历时,__ob__的属性this也是一个对象,然后会继续遍历,导致死循环。

Object.defineProperty(data, "__ob__", {
    enumerable: false,
    configurable: false,
    value: this,
});

不能直接写成下面的形式,也会产生死循环

value.__ob__ = this;

2.数组劫持

使用Object.defineProperty,数据的层级越深,性能就越不好。

所以出于性能方面考虑,vue2源码中并未对数组劫持,而是重写了数组原型上能够改变数组的几个方法。

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

为了避免将原型上的方法覆盖,所以对原型进行了继承

let oldArrayProtoMethods = Array.prototype;
export let arrayMethods = Object.create(oldArrayProtoMethods);

然后对methods中的几个方法进行重写。

原理是调用重写的方法会调用原来原型上的方法,然后将参数传过去。

数组中新增的项也需要监测,新增方法有三个:push、unshift、splice。

splice可以用来添加,修改,删除操作。添加时,传给splice的第二项是添加的内容。

methods.forEach((method) => {
    arrayMethods[method] = function (...args) {
        const result = oldArrayProtoMethods[method].call(this, ...args);
        
        let insert = null;
        let ob = this.__ob__;
        
        switch (method) {
            case "push":
            case "unshift":
                insert = args;
                break;

            case "splice":
                insert = args.slice(2);
            default:
                break;
        }
        if (insert) ob.observeArray(insert);
        
        return result;
    };
});

3.数据代理

为了方便用户操作数据,vue源码中实现了数据代理。

const app = new Vue({
    data:{
        a:1
    }
)

可以直接通过app.a来获取或修改a的值。

源码中定义了一个proxy函数(不是新增的Proxy,是自己手写的proxy函数)。

代理的本质是将vm._data用vm代理。

for (const key in data) {
    proxy(vm, "_data", key);
}

获取某个值时,相当于直接从vm._data获取,设置值相当于修改vm._data。

proxy函数

function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[target][key];
        },
        set(newValue) {
            vm[target][key] = newValue;
        },
    });
}

完整流程

首先创建了一个Vue类,可以扩展原型上的方法。

每次执行new Vue,都会调用init初始化方法,初始化函数其中有一步就是初始化状态。

状态分为很多,包括data,methods,props等,需要分别处理。

data可能是函数也可能是对象,将data处理后的结果挂载vm._data上,同时为了用户能够方便使用取值,做了一层代理。

对数据进行劫持,有两个原则,一是数据是对象,二是没有劫持过,给每个数据都增加一个观测的实例。

如果是数组,就把数组的方法重写,并且对数组里每个对象都进行劫持。

如果调用能改变原数组方法,就会调用重写的方法;如果新增值的也是数组,就通过Observer实例的方法再次对数组进行代理。

如果是对象,将数据进行劫持,如果对象的值是对象,也要劫持,如果修改的值也是对象,还要劫持。

完整代码

src/index.js

import { initMixin } from "./instance/init";

function Vue(options) {
    this._init(options);
}
initMixin(Vue)
export default Vue;

src/instance/init.js

import { initState } from "./state";

export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this;
        vm.$options = options;
        initState(vm);
    };
}

src/instance/state.js

import { observer } from "../observer/index";
import { isFunction, isPlainObject } from "../shared/until";

export function initState(vm) {
    const opts = vm.$options;
    if (opts.data) {
        initData(vm);
    }
}
// 设置一个数据代理,取值和设置值的时候直接操作vm._data
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[target][key];
        },
        set(newValue) {
            vm[target][key] = newValue;
        },
    });
}

function initData(vm) {
    let data = vm.$options.data;

    // 将data方到实例vm上
    vm._data = data = isFunction(data) ? data.call(vm) : data || {};
    if (!isPlainObject(data)) {
        data = {};
        console.error("[Vue warn] data functions should return an object");
    }

    observer(data);
    // 将vm._data用vm代理
    for (const key in data) {
        proxy(vm, "_data", key);
    }
}

src/observer/index.js

import { isObject } from "../shared/until";
import { arrayMethods } from "./array";

class Observer {
    constructor(data) {
        Object.defineProperty(data, "__ob__", {
            enumerable: false, 
            configurable: false,
            value: this, // 此处的this是Observer的实例
        });

        if (Array.isArray(data)) {
            data.__proto__ = arrayMethods;
            this.observeArray(data);
        } else {
            this.observeObject(data);
        }
    }
    observeArray(data) {
        data.forEach((item) => observer(item));
    }
     
    observeObject(data) {
        let keys = Object.keys(data);
        keys.forEach((key) => {
            defineReactive(data, key, data[key]);
        });
    }
}
export function defineReactive(data, key, value) {
    observer(value);
    Object.defineProperty(data, key, {
        get() {
            return value;
        },
        set(newValue) {
            if (newValue === value) return;
            observer(newValue);
            value = newValue;
        },
    });
}

export function observer(data) {
    if (!isObject(data)) {
        return;
    }
    // 如果被劫持过,就不需要再劫持了
    if (data.__ob__) {
        return data;
    }
    return new Observer(data);
}

src/observer/array.js

// 数组原来原型上的方法
let oldArrayProtoMethods = Array.prototype;

// 继承方法
export let arrayMethods = Object.create(oldArrayProtoMethods);

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

methods.forEach((method) => {
    arrayMethods[method] = function (...args) {
        // 此处的this是调用方法的数组
        const result = oldArrayProtoMethods[method].call(this, ...args);

        let insert = null;
        let ob = this.__ob__;
        switch (method) {
            case "push":
            case "unshift":
                insert = args;
                break;

            case "splice":
                insert = args.slice(2);
            default:
                break;
        }
        if (insert) ob.observeArray(insert);
        return result;
    };
});

src/shared/until.js

export function isFunction(value) {
    return typeof value === "function";
}

const _toString = Object.prototype.toString;
export function isPlainObject(obj) {
    return _toString.call(obj) === "[object Object]";
}

export function isObject(obj) {
    return obj !== null && typeof obj === "object";
}

关于this的指向,请点击查看如何判断this的指向

测试

const app = new Vue({
    data: {
        a: 100,
        obj: {
            name: "tom",
            arr: [1, 2, 3],
        },
    },
});
console.log(app.a);
console.log(app.obj.name);

app.obj.arr.push(10);
console.log(app.obj.arr);

app.obj1 = { name: "jack" };
console.log(app.obj1);

结果

vue2源码解析(二):数据劫持

至此,能够实现基本的数据代理和劫持。


文章就到这里,下次再见!

转载自:https://juejin.cn/post/7137574093147013157
评论
请登录