Vue响应
- 1. 初始化Vue
- 1.1 使用Vue
- 1.2 初始化Vue
- 2. 数据劫持
- 2.1 对象的单层劫持
- 2.2 对象的深层劫持
- 2.3. 数组的劫持
- 2.4 数据代理
- 2.5 数组的深层劫持
1. 初始化Vue
Vue的官网解释
-
Vue 是一套用于构建用户界面的渐进式框架,
-
Vue 并没有完全支持 MVVM 模型,但 Vue 的设计受到了它的启发,
-
变量名 vm 是 vue model 的缩写,表示 vue 实例;
1.1 使用Vue
-
在index.html中初始化Vue
<script><!-- 初始化 Vue,传入 options 对象 -->let vm = new Vue({el: '#app',// 1,data 是对象data: {msg: "zhiyu"}// 2,data 是函数,返回一个对象// data() {// return { msg: "zhiyu" }// }}); </script>
Vue在初始化时,会传入el挂载点、data数据等,在初始化完成之后,data中的数据会变成响应式数据(在根组件时data可以使对象也可以是函数,因为根组件不会被共享,而非根组件data必须是函数,否则数据会被多组件共享)
1.2 初始化Vue
-
在src下新建index.js文件,然后在Vue原型上扩展一个_init方法,用于Vue的初始化操作
/*** vue中的所有功能,都是通过原型扩展的方式添加的* @param {*} options new Vue时传入的 options 配置对象*/ function Vue(options){this._init(options); // 调用Vue原型上_init方法 } // 在Vue原型上扩展一个原型方法_init,用于vue的初始化操作 Vue.prototype._init = function(options) {} export default Vue;
-
在src下新建init.js文件,用于初始化操作的原型方法_init,单独抽离成一个独立的initMixin,导入src/index.js文件使用
// index.js import { initMixin } from "./init"; function Vue(options){// 初始化this._init(options); } initMixin(Vue); export default Vue
// init.js // src/init.js export function initMixin(Vue) {Vue.prototype._init = function (options) {console.log(options)} }
注意:原型方法_init的this指向当前vm实例
-
用户通过new Vue实例化时会传入options对象,为了便于Vue中其他方法便于获取options对象,直接将options选项挂载到vm实例上,即vm.$options = options
注意:
vm.$xxx
变量命名方式,表示 vue 内部变量; -
由于options里不仅有data, 还有props, watch, computed…所以需要一个统一的函数,对数据的初始化进行集中处理,initState状态初始化方法(src/initState.js)
// src/index.js...// 在 new Vue 时,传入的 options 选项中包含 el 和 datavm.$options = options;// 状态的初始化initState(vm); // 处理数据渲染并挂载到elif (vm.$options.el) {console.log("有el,需要挂载")}} }// initState.js export function initState(vm) {let ops = vm.$options;if(ops.props) {initProps(vm);}if(ops.data) {initData(vm);}// 还有其他比如methods、watch等 }; // vue2对 data初始化: function initData(vm) {}; function initProps() {};
2. 数据劫持
2.1 对象的单层劫持
Vue 响应式原理核心是通过Object.defineProperty为属性添加 get、set 方法,从而实现对数据操作的劫持
-
首先在initData中可以获取到data数据,通过
vm.$options.data
获取 -
然后处理data的两种情况(对象和函数)
- 如果data是函数,执行此函数,并得到函数内部返回的对象(此时this指向的是window,所以data是函数时,要改变this指向,使其指向当前vm实例)
- 如果data是对象,无需处理
data = typeof data === "function" ? data.call(vm) : data;
-
对数据进行观测:通过模块observer,创建入口文件src/observer/index.js, 经过initState.js文件处理之后,data一定是对象,所以在观测时在对data进行一次类型检测
// src/observer/index.js export function observer(data) {if(typeof data != "object" || data == null) {return data;} }// initState.js import { observer } from "./observer/index"; export function initState(vm) {let ops = vm.$options;// 判断实例上有没有这些属性if(ops.data) {initData(vm);} }; // vue2对 data初始化: function initData(vm) {// 首先判断data是对象还是函数let data = vm.$options.data;// 注意:函数this本来指向全局对象window,所以需要改变this指向data = vm._data = typeof data === "function" ? data.call(vm) : data;// 对数据进行劫持observer(data); };
-
对对象进行观测:创建observer类,遍历data对象,使用
Object.defineProperty
重新定义data对象中的所有属性export function observer(data) {...return new Observer(data) } class Observer {// 类的构造函数 constructor(value){// 遍历对象中的属性,使用 Object.defineProperty 重新定义this.walk(value);}// 循环 data 对象,使用 Object.keys 不循环原型方法walk(data){// Object.keys(data).forEach(key => { // defineReactive(data, key, data[key]);// });let keys = Object.keys(data); // 把对象中的所有属性转化为一个数组for(let i = 0; i < keys.length; i++) {// 对每个属性进行劫持let key = keys[i];let value = data[key];defineReactive(data, key, value);}} }/*** 使用Object.defineProperty重新定义data对象中的属性* @param {*} obj 需要定义属性的对象* @param {*} key 给对象定义的属性名* @param {*} value 给对象定义的属性值*/ function defineReactive(obj, key, value) {Object.defineProperty(obj, key, {get(){ return value;},set(newValue) {if (newValue === value) return;value = newValue;}}) }
2.2 对象的深层劫持
描述:如果data数据中的对象存在多层嵌套(比如:return { obj: {name: "zhiyu"} }
),使用前面的方法将不会被劫持
实现:在defineReactive中进行修改
function defineReactive(data, key, value) {// 对 key 进行观测前,调用 observer方法,如果属性值为对象则会继续向下找,实现深层递归观测observer(value); // 深度代理/深度劫持 {a: {b: 1}}Object.defineProperty(data, key, {get() {// console.log("获取");return value;},set(newValue) {// console.log("设置");if(newValue == value) return;// 当值被修改时,通过 observe 实现对新值的深层观测,此时,新增对象将被观测observer(newValue);value = newValue;}})
}
2.3. 数组的劫持
前言:其实通过前面对象的劫持也是可以实现数组的劫持,但是在 Vue2.x 中,是不支持通过修改数组索引或长度来触发更新的(出于性能的考虑)
对数组进行劫持的核心目标,还是要实现数组的响应式:
- 在 Vue 中,认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort;
- 对以上 7 个方法进行特殊处理,使他们能够劫持到数组的数据变化,就能够实现数组的响应式;
实现思路:
-
根据分析,数组和对象不能采用相同的处理方式,在observer初始化时会walk遍历属性实现观测,所以,在此需要单独采取对应的逻辑
import { ArrayMethods } from "./arr";class Observer {constructor(value) {if(Array.isArray(value)){// 对数组类型进行单独处理:重写 7 个变异方法}else{this.walk(value);}} }
-
新建observer/arr.js,实现数组方法重写、
// 方法函数劫持,劫持数组方法 arr.push() // 重写数组 // 1. 获取数组的原型方法 let oldArrayProtoMethods = Array.prototype // 2. 原型继承 export let ArrayMethods = Object.create(oldArrayProtoMethods); // 3. 重写能够导致原数组变化的七个方法 let methods = ["push","pop","unshift","shift","splice","reverse","sort" ]; // // 在数组自身上进行方法重写,以实现对链上同名方法的拦截效果 methods.forEach(item => {ArrayMethods[item] = function(...args) {// console.log("劫持数组");} })
-
在new observer时, 对数组类型的数据进行链上方法的重写
... constructor(value) {// 分别处理 value 为数组和对象两种情况if(Array.isArray(value)){value.__proto__ = ArrayMethods; // 更改数组的原型方法}else{this.walk(value);}} ...
-
数组数据变化时需要在劫持到数据变化后,进行处理
- 通过oldArrayPrototype[method].call(this, …args)执行push原生方法逻辑并绑定当前上下文,实现原数组的更新操作;
- 收集通过splice、push、unshift方法新增的数据,放入inserted数组;
- 遍历inserted数组,当数据为对象类型时,需要继续进行观测;
// arr.js methods.forEach(item => {ArrayMethods[item] = function(...args) {// 绑定到当前调用上下文let result = oldArrayProtoMethods[item].apply(this, args);// 数组追加对象的情况 arr.push({a: 1})let inserted = null; // 收集新增的数据switch(item) {case "push":case "unshift":inserted = args;break;case "splice":inserted = args.splice(2); // 获取新增数据:从第三个参数开始都是新增数据break;}// 遍历 inserted 数组中的新增数据,对象类型需要继续进行观测} })
-
由于Observer类中的原型方法observeArray实现了数组的深层劫持,但此方法并未对外导出;所以,在当前模块中遍历
inserted
数组时,就无法调用到Observer
类中的observeArray
方法实现数据观测 -
为了让当前数组或对象与
Observer
实例产生一个关联关系,在Observer
初始化时,为当前数组或对象value
添加自定义属性__ob__
,使value
和Observer
实例之间产生关联- value:为当前数组或对象,添加自定义属性
__ob__ = this
;(在 observe 方法中,只有值为对象类型时即数组或对象,才会执行new Observer创建实例,因此value必为数组或对象) - this:为当前Observer实例,通过实例可以调用到observeArray方法
// src/observer/index.js constructor(value) {// 给value定义一个属性Object.defineProperty(value, "__ob__", {enumerable: false, // 不能够进行枚举value: this, // 指向实例})...}// arr.js methods.forEach(item => {ArrayMethods[item] = function(...args) {...let ob = this.__ob__;if(inserted) {ob.observerArray(inserted); // 对添加的对象进行劫持}return result;} })
- value:为当前数组或对象,添加自定义属性
-
对象被重复观测:当对象被添加
__ob__
属性标识后,代表着当前对象已经被创建过Observer
实例了,即当前对象已经被深层观测过了,在之后的处理中应避免重复观测export function observer(data) {// 1. 对象的数据处理,判断是不是对象以及是否为空if(typeof data != "object" || data == null) {return data;}if(data.__ob__){console.log("当前数据已经被观测过了,data = "+ data)return;}// 通过类进行劫持return new Observer(data); }
2.4 数据代理
含义:就是实现vm.msg
和$options.data.msg
等效,所以要想办法将vm
实例操作“代理”到$options.data
上;这样,就实现了 “Vue 的数据代理”
实现思路:
- 首先,先做一次代理,将data挂载到
vm._data
下,这样vm实例就可以在外部通过vm._data.msg
获取到vm.data
。 - 之后,再做一次代理,将
vm
实例操作vm.msg
代理到vm._data
上,这样,外部就可以直接通过vm.msg
获取到data.msg
代码实现:
-
在Vue初始化阶段,通过observer()实现数据响应式之后,通过
Object.defineProperty
对_data
中的数据操作进行劫持;将vm.xxx
在vm
实例上的取值操作,代理到vm._data.xxx
上// initState.js function initData(vm) {...// 对数据进行劫持observer(data);// 将data上的所有属性代理到实例上vm {a: 1, b: 2}for(let key in data) {proxy(vm, "_data", key);} }; // vm: vm实例 source: 代理目标 key: 属性名 function proxy(vm, source, key) {Object.defineProperty(vm, key, {get() {return vm[source][key];},set(newValue) {vm[source][key] = newValue;}}); };
2.5 数组的深层劫持
分析:通过调试,发现之前的代码不能实现数组嵌套的劫持,而数组嵌套又分为数组嵌套数组和数组嵌套对象两种,想要对数组嵌套实现数据观测,就需要对数组内部的数据继续进行递归处理
-
在Observer类中,创建observerArray方法,对数组进行深层观测
class Observer {constructor(value) {// 对数据进行判断是数组还是对象if(Array.isArray(value)) {value.__proto__ = ArrayMethods// 如果是数组对象this.observerArray(value); // 处理数组嵌套 [{a: 1}]} else {this.walk(value); // 进行遍历}}observerArray(value) {// 对数组中的每一项调用 observe 方法,继续进行深层观测处理;// observe 方法内:如果是对象类型,继续 new Observer 进行递归处理for(let i = 0; i < value.length; i++) {observer(value[i]);}} };