说明
在Vue2.x中,利用了对原型链的理解,巧妙的利用JavaScript中的原型链,实现了数组的pop、push、shift、unshift、reverse、sort、splice
等的拦截.
你可能需要的知识
- 参考 - MDN
原型链
JavaScript常被描述为一种基于原型的语言(prototype-based language),每个对象拥有一个原型.
数组类型也不例外.验证如下:
let arr = [];
console.log(arr)
/*{length: 0__proto__: {length: 0constructor: f Array()...__proto__: Object}}
*/
可见数组的原型是继承于Object。
响应式
参考 - MDN
响应式的核心是使用Object.defineProperty
在对数据进行读取或者写入时进行劫持操作.
let o = {}
,_gender
Object.defineProperty(o, gender, {get(){return _gender},set(newVal){_gender = newVal}
})
对一个属性,同时使用get和set方法时,需要一个中间变量取存储,否则会造成循环使用.
Vue 2.x中的响应式
- Vue在使用过程中,可能会用到很多的变量,而每把一个数据进行响应式化,就需要一个变量去存储.这样有可能会污染全局作用域.
- Vue中采取的方法是使用函数的形参,来实现响应式,实现如下
function defineReactive(target, key, value, enumerable){// 注意: 此处的value与上文的_gender类型Object.defineProperty(target, key, {configurable: true,enumerable: !!enumerable,get(){console.log(`读取${value}`)return value},set(newVal){console.log(`写入: ${value} --> ${newVal}`)value = newVal}})
}
let o = {name: 'marron',age: 26,remark: 'hunt for job'
}
Object.keys(o).forEach(k => {defineReactive(o,k,o[k],true)
})
以上实现了对数据的拦截: 即对数据进行 写入/读取 操作时,会按照一定规则优先执行某些步骤.
但是以上代码还存在一些小小的瑕疵
对象深层次
以上代码不对对象的深层次进行响应式化,如下面数据
let o = {list: [ { person1: {name:'Marron',age: 18}},{person2: {name:'Mar',age: 25}}]
}
此时,需要考虑数组,和对象的子元素问题.
对于数组问题,我们修改遍历,如果是数组,则取出数组中的每个元素,进行添加响应式处理
- Object.keys(o).forEach(k =>{
- defineReactive(o, k, o[k], true)
- })
+ function reactify(o){
+ Object.keys(o).forEach(k => {
+ if(Array.isArray(o[k])){
+ o[k].forEach(val => reactive(val))
+ } else {
+ defineReactive(o, k, o[k], true)
+ }
+ })}
对于深层次对象问题,我们对defineReactive
进行修改
function defineReactive(o, key, value, enumerable){if(typeof value =='object' && value !== null && !Array.isArray(value)){// 此处可以认为是对象reactify(value)}// 此处是最后一层,添加响应式Object.defineProperty(o, key, {configurable: true,enumerable: !!enumerable,get(){console.log(`读取${key}`)return value},set(newVal){console.log(`写入${key} => ${newVal}`)value = newVal}})
}
Vue2.x对数组部分方法的拦截
上面的响应式无法对数组的pop、push等方法进行响应
在Vue2.x中,使用了修改原型链的结构的方式来对数组的变化进行拦截.
先看下面的关系
- 原本的关系图示已经描述的很清楚了
- 我们对pop和push的拦截的原理
- 实际上是对Array原型上的
pop、push
方法进行重写 - 但是我们不可能直接在这个原型上重写(因为有些数组的实例,并不需要响应式).
- 因此我们在
arr
和Array.prototype
之间添加一层arr_methods
,改进后的关系如下
【具体的实现思路】:
先创建一个arr_methods
对象其原型是Array.prototype
.然后修改arr_methods
上需要拦截的方法(存储在数组ARRAY_METHOD
中)
const ARRAY_METHOD = ['push','pop','shift','unshift','reverse','sort','splice'
]
let arr_methods = Object.create(Array.prototype)ARRAY_METHOD.forEach(method=>{arr_methods[method] = function(){// 拦截的函数console.log(`调用${method}方法`) return Array.prototype[method].apply(this, arguments)}
})
arr.__proto__ = arr_methods
此时既不影响原生的Array.prototype,又实现了对pop、push...
方法的拦截,完成之后只需要修改前面的方法.即可完成对数组pop、push方法的拦截
function reactify(o){Object.keys(o).forEach(k => {if(Array.isArray(o[k])){// 数组方法的响应式o[k].__proto__ = array_method
. o[k].forEach(val => reactive(val))} else {defineReactive(o, k, o[k], true)}})}
最后,此时只是拦截,还差一步形成响应式
ARRAY_METHOD.forEach(method=>{arr_methods[method] = function(){// 拦截的函数console.log(`调用${method}方法`)for(let i =0, len = arugments.length; i < len; i++){reactify(arguments[i])}return Array.prototype[method].apply(this, arguments)}
})