vue中通过数组索引修改数据,会引起视图的变化吗?
带着这个问题,跟我一起手写vue响应式数据原理,一探究竟吧。
1. 导出vue构造函数
import {initMixin} from './init';
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
2. init方法中初始化vue状态
首先在 Vue 原型上挂载 init 方法, 用户传入的 options 挂载到 vm 的$options 上
注:vm 指代 new 出来的 Vue 实例
import { initState } from "./state";
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
initState(vm);
};
}
3. 根据不同属性进行初始化操作
初始化数据
再通过initSate 对vm的数据进行初始化(包括 props,data,computed,watch) 今天我们就只讲data初始化,其他的我放下篇文章再讲。
export function initState(vm) { // 状态的初始化
const opts = vm.$options;
if (opts.data) {
initData(vm);
}
// if(opts.computed){
// initComputed();
// }
// if(opts.watch){
// initWatch();
// }
}
function initData(vm) {
let data = vm.$options.data;
//data有两种写法,有属性和函数两种写法
//所以我们在传入观测数据前要对data进行类型判断
data = vm._data = isFunction(data) ? data.call(vm) : data;
//data是函数,则使用call改变将this指向vm,并立即执行data函数,把函数返回的 data 对象赋值给 vm.\_data,否则直接返回 data
// 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
observer(data);
}
export function observe(data) {
//如果是对象才观测
if (!isObject(data)) {
return;
}
//是对象则返回一个Observer类的实例来观测数据
return new Observer(data);
}
4. 数据劫持
step1.劫持data.key的每一个key
先使用walk方法对data的进行遍历,将data每个属性用defineProperty重新定义,加上get、set属性访问器。
需要注意的一点:
直接给 v._data.b=1
这样给v._data
添加一个新的属性b是没办法 observe 的。因为进行数据遍历的时候用的是初始data
上的已经存在
的key。
step2.劫持data[key]
再对data[key]中的数据进行劫持,数组和对象的劫持方法不同。
第一种情况:data[key]为对象时,observe对象的每一项
有两个需要注意的点:
- 如果是对象里面包
对象
的数据格式,那么还要对对象进行 observe,通过递归来实现。 - 如果给一个旧值赋值成一个
对象
的话,也要对这个新值进行 observe。
import { isObject } from "../utils";
import { arrayMethods } from "./array";
// 1.如果数据是对象 会将对象不停的递归 进行劫持
// 2.如果是数组,会劫持数组的方法,并对数组中不是基本数据类型的进行检测
class Observer {
constructor(data) { // 对对象中的所有属性 进行劫持
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false //设置为不可枚举,就不能遍历,可以解决多次遍历__ob__导致栈溢出的问题。
})
//因为向数组新增的时候还要observe新增的数据,所以需要调用Observer类上的observeArray方法,所以vue通过__ob__==this,把实例传过来,这样我们就在数组方法重写那个地方调用observeArray方法了。
if(Array.isArray(data)){
// 数组劫持的逻辑
// 对数组原来的方法进行改写, 切片编程 高阶函数
data.__proto__ = arrayMethods;
// 如果数组中的数据是对象类型,需要监控对象的变化
this.observeArray(data);
}else{
this.walk(data); //对象劫持的逻辑
}
}
observeArray(data){ // 对我们数组的数组 和 数组中的对象再次劫持 递归了
// [{a:1},{b:2}]
data.forEach(item=>observe(item))
}
walk(data) { // 对象
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
}
// vue2 会对对象进行遍历 将每个属性 用defineProperty 重新定义 性能差
function defineReactive(data,key,value){ // value有可能是对象
observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
Object.defineProperty(data,key,{
get(){
return value
},
set(newV){
// todo... 更新视图
observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
value = newV;
}
})
}
export function observe(data) {
// 如果是对象才观测
if (!isObject(data)) {
return;
}
if(data.__ob__){
return;
}
// 默认最外层的data必须是一个对象
return new Observer(data)
}
第二种情况:data[key]为数组时:重写数组原型方法
用户很少通过索引去操作数组,arr[3]=100,vue 直接监听数组索引,性能消耗严重.所以内部数组监听不采用 objectProperty,通过重写数组方法来监听数组
push shift unshift pop reverse sort splice
如果用户调用的是以上七个方法,会调用 vue 重写的,否则用原来的数组方法。
为什么只有这七个呢?
因为这七个方法会改变数组方法,而其他像concat这种就不会改变原数组。- 数组没有监控索引的变化,但是如果数组中的数据是
对象
类型,会对对象则需要observe对象变化.所以如果arr:[{name:"chibaozi"}]
,那么像这样vm.arr[0].name="早上吃包子"
通过下标修改 是可以被监听到的。 - 数组新增的数据是
对象
类型时,也需要observe新增的对象。
let oldArrayPrototype = Array.prototype
export let arrayMethods = Object.create(oldArrayPrototype);
// arrayMethods.__proto__ = Array.prototype 继承
let methods = [
'push',
'shift',
'unshift',
'pop',
'reverse',
'sort',
'splice'
]
methods.forEach(method =>{
// 用户调用的如果是以上七个方法 会用我自己重写的,否则用原来的数组方法
arrayMethods[method] = function (...args) { // args 是参数列表 arr.push(1,2,3)
oldArrayPrototype[method].call(this,...args); // arr.push(1,2,3);
let inserted;
let ob = this.__ob__; // 根据当前数组获取到observer实例
switch (method) {
case 'push':
case 'unshift':
inserted = args ; // 就是新增的内容
break;
case 'splice':
inserted = args.slice(2)
default:
break;
}
// 如果有新增的内容要进行继续劫持, 我需要观测的数组里的每一项,而不是数组
// 更新操作.... todo...
if(inserted) ob.observeArray(inserted)
}
})
5. 数据代理
我们还需要将 vm._data.key 代理到 vm.key上,也是通过defineProperty的方法。
function vmProxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key];
},
set(newV) {
data[source][key] = newV;
},
});
}
function initData(vm) { //
let data = vm.$options.data; // vm.$el vue 内部会对属性检测如果是以$开头 不会进行代理
// vue2中会将data中的所有数据 进行数据劫持 Object.defineProperty
// 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
data = vm._data = isFunction(data) ? data.call(vm) : data;
// 用户去vm.xxx => vm._data.xxx
for(let key in data){ // vm.name = 'xxx' vm._data.name = 'xxx'
proxy(vm,'_data',key);
}
observe(data);
}
小结
看到这里,相信你也有答案了。 在vue2中修改数组的索引和长度是无法监控到的。
Vue2中data
对象内部通过defineReactive
方法,对对象的每个属性进行遍历,使用Object.defineProperty
将属性进行劫持。(只会劫持已经存在的属性)。数组因为考虑性能原因,没有使用Object.defineProperty
对数组的每一项进行拦截。数组是通过重写数组方法来实现。需要通过pop splice shift unshift reverse sort
这七个变异方法修改数组,才会触发数组对应的watcher
更新,数组中如果是对象数据类型也会递归劫持。
vue3改为使用proxy
来实现响应式数据。
内部依赖收集,是通过每个属性都拥有自己dep
属性,存放他所依赖的watcher
,当属性变化时,通知自己对象的watcher
去更新。
在更加深入理解响应式原理后,我们也可以总结出一些vue代码优化
的方法:
- 对象层级不要嵌套太多层。因为vue2中多层对象是通过递归来实现劫持,所以对象层级过深,性能就会差。
- 不需要响应式的内容不要放到data中。
- 可以使用object.freeze()冻结数据 下一篇文章再手写模版编译,嘿嘿。