一、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中需要暴露一个initMixin()方法,该方法接收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[source][key];
},
set(newVal) {
vm[source][key] = newVal;
}
});
}
这里的vm[source]就是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/utils/index.js
export function def(data, key, value) {
Object.defineProperty(data, key, {
enumerable: false, // 避免该属性被枚举
get() {
return value;
},
set(newVal) {
value = newVal;
}
});
}
// src/observer/index.js
import {isObject, def} from "../utils/index";
import {arrayMethods} from "./array";
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;
}
})
}
对数组的观测,主要就是要重新那些会改变原数组的方法,如: push、pop、shift、unshift、sort、reverse、splice,以便数组发生变化后能够给观察者发送通知,并且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;
}
});
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。