主题
Vue响应式原理
1. 导出构造函数
js
// /src/core/instance/index.js
import { initMixin } from "./init";
function Vue(options) {
this._init(options);
}
initMixin(Vue);
export default Vue;
2. _init
方法中初始化vue状态
js
// init.js
import { initState } from "./state";
export function initMixin(Vue) {
// 初始化
Vue.prototype._init = function(option) {
const vm = this;
// TODO: 选项合并
vm.$options = option || {};
// 数据观测(初始化状态)
initState(vm);
};
}
3. 根据不同属性进行初始化操作
初始化顺序
Vue中初始化的顺序为 props
methods
data
computed
watch
js
export function initState(vm){
const opts = vm.$options;
if(opts.props){
initProps(vm);
}
if(opts.methods){
initMethod(vm);
}
if(opts.data){
// 初始化data
initData(vm);
}
if(opts.computed){
initComputed(vm);
}
if(opts.watch){
initWatch(vm);
}
}
function initProps(){}
function initMethod(){}
function initData(){}
function initComputed(){}
function initWatch(){}
初始化数据(data)
js
function initData(vm) {
// 取到用户传入的data数据
let data = vm.$options.data;
// 取到数据, 并将数据挂载到vm._data上, 以方便用户通过vm._data.xxxx访问到数据
data = vm._data = typeof data === "function" ? data.call(vm) : data;
// 对数据进行观察
observer(data);
}
对象递归劫持
js
// 观察者类
class Observer {
// value为被观察的对象
constructor(value){
// 对象的劫持
this.walk(value);
}
// 对象劫持的方法
walk(data){
let keys = Object.keys(data);
// 让对象的每一个key依次进行观测, 变成响应式
keys.forEach(key=> defineReactive(data,key,data[key]);)
}
}
// 定义响应式
function defineReactive(data,key,value){
observe(value); // 对象的value也可能是对象, 递归观测
Object.defineProperty(data,key,{
get(){
return value
},
set(newValue){
if(newValue == value) return;
observe(newValue);
value = newValue
}
})
}
// 观察者
export function observe(data) {
if(typeof data !== 'object' || data == null) return;
return new Observer(data);
}
以定义响应式的方法存在的问题:
Object.keys
对于数组, 取到的key是索引, 因此会对索引进行观测, 将索引变成响应式, 这会造成性能的浪费, 且开发中很少对数组的索引进行操作
- 操作数组的push, unshift, splice方法, 新增数组元素, 新增的元素不会被观测到
数组的观测
针对以上两个问题, 需要区分数组以及对象, 对象和数组分开观测
js
import { isObject } from "../../utils/index";
import { arrayMethods } from "./array";
class Observer {
constructor(value) {
// 给每一个被观察过的对象身上添加一个__ob__属性
value.__ob__ = this;
// 区分是数组还是对象
if (Array.isArray(value)) {
// 重写数组的7个方法
value.__proto__ = arrayMethods;
// 对数组的每一项进行观测
this.observerArray(value);
} else {
// 对象的观测
this.walk(value);
}
}
// 观测对象
walk(value) {
const keys = Object.keys(value);
keys.forEach((key) => defineReavtive(value, key, value[key]));
}
// 观测数组
observerArray(ary) {
ary.forEach((item) => observer(item));
}
}
export function observer(value) {
if (!isObject(value)) return;
return new Observer(value);
}
// ......
重写数组原型上的7个方法
js
const arrayProto = Array.prototype; // 先将数组原型对象保存下来
export const arrayMethods = Object.create(arrayProto); // 创建一个空对象, 继承自数组原型对象
// 下面7个方法是会改变原数组的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 对数组重写上述的7方法
methodsToPatch.forEach((method) => {
// 注意, 这里是我们创建的对象添加数组原型上的7个方法, 等于是对数组原型上的7个方法做一个切片
// 最终调用时候会根据原型链, 先找到我们自己创建的方法, 找不到再找原型上的方法
arrayMethods[method] = function (...args) {
// 先调用原始的方法, 此时的this指的是数组
const res = arrayProto[method].apply(this, args);
// this表示当前数组 value.push(), 这里的value会走到这里
// 由于上一步我们在Observe中给每一个被观测对象上添加了__ob__属性, 因此这里可以通过this.__ob__取到
const ob = this.__ob__;
let inseted; // 取到新增的数组项
switch (method) {
case "push":
case "unshift":
inseted = args; // args是一个数组
break;
case "splice":
inseted = args.slice(2); // splice的第3个开始的参数是要新增或编辑后的元素
break;
default:
}
// 对新增属性进行观测
if (inseted) ob.observerArray(inseted);
return res;
};
});
上述步骤完成后会存在一个问题, 观测进入了死循环, 由于每一个被观察过的对象身上都会加一个__ob__
属性, 在this.walk
观测对象时候, 就会通过Object.keys
获取到__ob__
属性, 并且会对该属性进行观察, __ob__
的值是this
(observer),因此会进入递归观察,从而进入死循环
那么只需要让Object.keys
拿不到__ob__
就解决问题了, 因此在给value
身上添加__ob__
属性时, 应该让这个属性不可枚举def(data, '__ob__', this)
js
import { def } from "../../utils/index";
class Observer {
constructor(data) {
// 为每一个被监控过的对象添加一个__ob__属性
def(data, '__ob__', this)
if (Array.isArray(data)) {
// 重写数组的7个方法
// @ts-ignore
data.__proto__ = arrayMethods;
// 对数组的每一项进行观测
this.observerArray(data);
} else {
this.walk(data);
}
}
// 观测对象
walk(value) {
const keys = Object.keys(value);
keys.forEach((key) => defineReavtive(value, key, value[key]));
}
// 观测数组
observerArray(ary) {
ary.forEach((item) => observer(item));
}
}
定义不可枚举的属性
js
// src/utils/index.js
export function def(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: false, // 不可枚举
configurable: false, // 不可配置
value: val
});
}
数据代理
为了方便, 可以将vm._data
上的数据全部代理到vm
上
js
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key];
},
set(newValue){
vm[source][key] = newValue;
}
});
}
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
for(let key in data){ // 将_data上的属性全部代理给vm实例
proxy(vm,'_data',key)
}
observe(data);
}
小结
至此, 数据响应式的核心原理 手写完成, 可以通过vm.xxxx
直接访问data里面的数据了
- 通过
initMixin
扩展Vue原型方法, 内部包含_init
初始化逻辑 _init
内部先取到new Vue
时候用户传入的options
挂到vm.$options
上并且开始进行initState
initState
内部会按props、methods、data、computed、watch的顺序进行初始化,这里先写initDatainitData
内部会将data取到后挂在vm._data
上, 在将_data
上面的属性通过proxy
代理了一下, 挂到vm
上,最后调用observer
对data上的属性进行观察, 让其变为响应式- 数据响应式的重点就在
observer
里面,observer
区分了对象和数组进行观察, 核心是用Object.defineProperty
方法将递归的将对象的每一个key
重新定义, 数组中对象也一样,数组还会用切片编程的方式重写7个原型方法