关于vue.js:Vue2响应式原理与实现

53次阅读

共计 4549 个字符,预计需要花费 12 分钟才能阅读完成。

一、Vue 的初始化

Vue 实质上是一个 裸露在全局的名为 Vue 的函数 ,在应用的时候通过 new 这个 Vue 函数来创立一个 Vue 实例,并且 会传入一个配置对象
Vue 函数内须要做的事件就是 依据传入的配置对象进行初始化。如:

// src/index.js
function Vue(options) {this._init(options);
}

这里通过 this 调用了_init()办法,这个 this 就是创立的 Vue 实例对象,然而目前 Vue 实例上并没有这个_init()办法,所以咱们 须要给 Vue 的 prototype 上增加一个_init()办法 。为了不便模块治理,咱们须要 专门建一个独自的 init.js 用于做初始化的工作 。init.js 中须要裸露一个 initMix() 办法,该办法接管 Vue 以便在 Vue 的 prototype 上增加原型办法,如:

// src/init.js
export function initMixin(Vue) {Vue.prototype._init = function(options) {// 这里进行 Vue 的初始化工作}
}
// src/index.js
import {initMixin} from "./init";
function Vue(options) {this._init(options);
}
initMixin(Vue); // 传入 Vue 以便在其 prototype 上增加_init()办法

此时_init()办法就能拿到用户传入的 options 配置对象,而后开始进行初始化工作:
将 options 对象挂载到 Vue 实例的 \$options 属性上 ;
初始化状态数据 ;
判断用户有没有传 el 属性,如果传了则被动进行调用 \$mount()办法进行挂载 ,如果没有传,那么须要用户本人调用 \$mount() 办法进行挂载。

// src/init.js
export function initMixin(Vue) {Vue.prototype._init = function(options) {
        const vm = this;
        vm.$options = options; // 将 options 挂载到 Vue 实例的 $options 属性上
        // beforeCreate 这里执行 Vue 的 beforeCreate 生命周期
        initState(vm); // 进行状态的初始化
        // created 这里执行 Vue 的 created 生命周期
        if (options.el) { // 如果配置了 el 对象,那么就要进行 mount
            vm.$mount(options.el); // 被动调用 $mount()进行挂载}
    }
    Vue.prototype.$mount = function(el) {// 这里进行挂载操作}
}

二、Vue 状态数据的初始化

接下来就是进行状态的初始化,即 实现 initState()办法,状态的初始化是一个独立简单的过程,咱们须要将其独自放到一个 state.js 中进行,次要就是依据 options 中配置的属性进行特定的初始化操作,如:

export function initState(vm) {
    const options = vm.$options;
    if (options.data) { // 如果配置了 data 属性
        initData(vm);
    }
    if (options.computed) { // 如果配置了计算属性
        initComputed(vm);
    }
    if (options.watch) { // 如果配置了用户的 watch
        initWatch(vm);
    }
}
function initData(vm) {// 这里进行 data 属性的初始化}

function initComputed(vm) {// 这里进行 computed 计算属性的初始化}
function initWatch(vm) {// 这里进行用户 watch 的初始化}

三、将 data 数据变成响应式的

data 属性的初始化是 Vue 响应式零碎的外围,即 对 data 对象中的每一个属性进行观测监控 。用户传入的 data 可能是一个对象也可能是一个 返回对象的函数 。所以须要对 data 的类型进行判断,如果是函数,那么 传入 Vue 实例并执行这个函数拿到返回的对象作为用于观测的 data。同时为了不便 Vue 实例操作 data 中的数据,还须要 将 data 中的属性一一定义到 Vue 实例上,如:

// src/state.js 实现 initData()办法
import {proxy} from "./utils/index";
import {observe} from "./observer/index";
function initData(vm) {
    let data = vm.$options.data; // 可能是一个函数
    // 给 Vue 实例增加一个_data 属性和 $data 属性保留用户的 data
    data = vm._data = vm.$data = typeof data === "function" ? data.call(vm) : data;
    for (let key in data) { // 遍历 data 的所有属性
        proxy(vm, "_data", key); // 将 data 中的属性代理到 Vue 实例上,不便操作 data
    }
    observe(data); // 对数据进行察看
}

下面用到了一个 proxy 工具办法,用于将 data 中的属性代理到 Vue 实例上,其外部次要就是通过 Object.defineProperty()办法,将 data 中的属性代理到 Vue 实例上,如:

// src/utils/index.js
export function proxy(vm, source, key) {
    Object.defineProperty(vm, key, {get() {return vm[key];
        },
        set(newVal) {vm[key] = newVal;
        }
    });
}

这里的 vm 就是 vm._data 对象也就是 用户传入的 data,这样当用户通过 Vue 实例去操作数据的时候,实际上操作的就是用户传入的 data 对象

接着就是对整个 data 数据进行观测了,进行数据观测的时候,这个数据必须是对象或者数组 ,否则不进行观测,如果这个对象中某个 key 的属性值也未对象,那么也须要对其进行观测,所以这里会 存在一个递归操作,这也是影响 Vue 性能的重要起因。数据观测也是一个独立简单的过程,须要对其独自治理,如:

// src/observer/index.js
import {isObject} from "../utils/index";
export function observe(data) {if (!isObject(data)) { // 仅察看对象和数组
        return;
    }
    // 如果要观测的数据是一个对象或者数组,那么给其创立一个 Observer 对象
    return new Observer(data);
}
// src/utils/index.js
export function isObject(data) {return data && typeof data === "object";}

接下来 Vue 会给合乎对象或者数组的 data 进行观测,给其创立一个 Observer 对象,观测的时候,对象和数组的解决会有所不同,对于对象而言,遍历对象中的每个属性并将其定义成响应式 即可;对于数组而言,因为数组可能存在十分多项,为了防止性能影响,不是将数组的所有索引定义成响应式的 ,而是 对数组中属于对象或者数组的元素进行观测

// src/observer/index.js
class Observer {constructor(data) {
        this.data = data;
        def(data, "__ob__", this); // 给每个被察看的对象增加一个__ob__属性,如果是数组,那么这个数组也会有一个__ob__属性
        if (Array.isArray(data)) { // 对数组进行观测
            data.__proto__ = arrayMethods; // 重写数组办法
            this.observeArray(data); // 遍历数组中的每一项值进行观测
        } else {this.walk(data); // 对对象进行观测
        }
    }
    walk(data) {for (let key in data) { // 遍历对象中的所有 key,并定义成响应式的数据
            defineReactive(data, key, data[key]);
        }
    }
    observeArray(arr) {arr && arr.forEach((item) => {observe(item); // 对数组中的每一项进行观测
        }
    }
}
function defineReactive(data, key, value) {let ob = observe(value); // 对传入的对象的属性值进行递归观测
    Object.defineProperty(data, key, {get() {return value;},
        set(newVal) {if (newVal === value) {return;}
            observe(newVal); // 如果用户批改了值,那么也要对用户传入的新值观测一下,因为可能传入的是一个对象或者数组,对新值批改的时候能力检测到
            value = newVal;
        }
    })
}

对数组的观测,次要就是要从新那些会扭转原数组的办法,如: pushpopshiftunshiftsortreversesplice以便数组发生变化后可能给观察者发送告诉,并且push、unshift、splice 会给数组新增元素,咱们还须要晓得新增的是什么数据,须要对这些新增的数据进行观测。

const arrayProto = Array.prototype; // 获取数组的原型对象
export const arrayMethods = Object.create(arrayProto); // 依据数组的原型对象创立一个新的原型对象,防止办法有限循环执行
const methods = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse"
];

methods.forEach((method) => {arrayMethods[method] = function(...args) {const result = arrayProto[method].apply(this, args); // 执行数组的上本来的办法
        const ob = this.__ob__;
        let inserted; // 用于记录用户给数组插入的新元素
        switch(method) {
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                inserted = args.slice(2);// 对应 splice 第三个参数才是用户要插入的数据
                break;
            default:
                console.log("拦挡的办法不存在");
        }
        if (inserted) { // 数组办法内,惟一能拿到的就是数组这个数据,所以咱们须要给察看的数组对象增加一个 key,值为 Observer 对象,能力拿到 Observer 对象上的办法
            ob.observeArray(inserted); // 对插入的新元素进行观测
        }
        return result;
    }
});

正文完
 0