vue响应式原理涉及到Object.defineProperty(数据劫持),当监听的元素发生变化的时候执行配置的 get / set 方法
Object.defineProperty 监听的实现
Object.defineProperty 可以传入三个值(监听对象,监听对象的属性名,配置对象),在配置对象中直接使用get / set 返回方法
const data = {
name: '小王',
age: 18,
shan:{
name:'小绿',
age:20
},
arr:[1,2,3]
}
Object.defineProperty(data,"name",{
// 读
get() {
console.log(`du`)
return value
},
// 写
set(newVal) { // set 写入方法可对数据传入
console.log(`xie`)
if (value === newVal){ // 若渲染判断前 value和后一次传入的新newval一致时,就不再执行后续的渲染和从新赋值操作
return
}
value = newVal
render()
}
// 页面渲染
function render() { // 这里不再写render渲染的代码,用执行console.long代替进行了一次页面渲染
console.log(`页面渲染`)
}
cosole.log(data.name) // 能执行配置中的get()方法
data.name = '小芳' // 能执行配置中的set()方法
data.age = 22 // 因为没有对age的属性名进行监视,故不会执行set 方法
注意事项
这时会发现Object.defineProperty 不是对所有的属性名进行监控,故需要对需要监控的对象,所有的属性名进行监控,对其后续处理我们需要理解和解决以下的问题:
- 需要遍历所有的对象从而监听所有的属性名
- 当对象中嵌套对象时,无法一次性的获取到所有的属性名,转思路对传入的 data 进行类型判断,若需要监听的数据类型是 ‘object’ 时,则进行遍历的操作。
- Object.defineProperty 的本身缺点,对象的增删的操作,是无法进行监听到的。是在于set的配置方法无法被执行!
- 关于传入对象是数组时,不在对 Object.defineProperty 进行限制时,对改变数组 length 内索引的元素时,是可以监听到元素的改变的。数组的本质也是对象,在超过数组索引时赋值,等同于创建对象,创建的对象是无法被监听
// 举个栗子:
data.arr[1] = 100 // 元素的改变 可以被监听
data.arr[100] = 100 // 相当于对象的创建,实际无法被监听
- 既然是在索引内的改变,为什么不可作为监听的一个元素呢? -------->是由于在页面中,会存在大量的数组,数组的for in 遍历非常消耗性能:性能的代价和获得的用户体验不成正比,故在Vue在 Object.defineProperty 中是会限制对数组传入的
- 针对无法对数组的改变, 从而 vue 提供了变异数组的方法 , $set 和 $ delete (set delete大多用于对象,但是数组也能使用)
解决问题
实现所有的属性名的监听以及对数组限制
创建一个observer方法观察传入的数据类型,并且进行遍历:
function observer(data) {
if (Array.isArray(data)){ // 若data传入的数据类型是数组,则直接返回
return
}
if (typeof data === 'object'){
for(let key in data){
defineReactive(data,key,data[key])
}
}
}
function defineReactive(data,key,value) {
observer(value); // 当对象中嵌套对象 再次调用observer
Object.defineProperty(data,key,{
// 读
get() {
console.log(`du`)
return value
},
// 写
set(newVal) {
console.log(`xie`)
if (value === newVal){ // 若渲染判断前 value和后一次传入的新newval一致时,就不再执行后续的渲染和从新赋值操作
return
}
value = newVal
render()
}
})
}
observer(data)
变异数组的实现
变异数组方法的实现,其实是改变了原型链上的方法,并将Object.defineProperty 上的数组的原型的7个方法改变为特有的方法(替换)
[push,pop,shift,unshift,sort,splice,reverse]这里不解释这7种方法的作用了,跟数组上的作用是等同的
变异数组实现的原理:
const oldPush = Array.prototype.push;
Array.prototype.push = function () {
oldPush.call(this,...arguments)
render() // 重新渲染页面
}
- 就是将原本Array身上原型链方法存到一个新的变量oldPush中
- 再将自身的Array原型链方法(push)从新赋值
- 赋值的过程,直接调用到 oldPush 身上方法(由于是Array的原型链 自然拥有push方法),ps:这里要注意this的指向,由于是需要push里的数组,固需要改变一下this的指向,指向push里的数组
故按照这个逻辑对7个数组的方法进行改变:
const arrayPrototype = Array.prototype;
// 克隆数组原型链上的方法
const arrMethods = Object.create(arrayPrototype);
['push','pop','shift','unshift','sort','splice','reverse'].forEach(method => {
arrMethods[method] = function () { // 更改的是克隆出来的原型
arrayPrototype.call(this,...arguments)
render()
}
})
function observer(data) {
if (Array.isArray(data)){
data.__proto__ = arrMethods; // 改变调用时候的原型链
return
}
if (typeof data === 'object'){
for(let key in data){
defineReactive(data,key,data[key])
}
}
}
$set
先说一说 $set 的使用方法,它可以传递三个参数,分别代表:
- 改变的对象
- 需要修改谁(数组中体现为要修改的索引对象)
- 要修改成什么样子
在传入数组的时候,相当于是 splice 替换掉属性值,并返回出新的 value 值,实现代码如下:
function $set (data,key,value){
if (Array.isArray(data)){
data.splice(key,1,value) // 这里执行的是数组的变异方法 所以不用再次render
return value
}
// 先被观察
defineReactive(data,key,value);
render()
return value
}
$delete
原理跟 set 相似;数组中同样是使用了splice方法,它可传入两个值,分别为:
- 改变的对象
- 要删除谁
实现代码如下:
function $delete(data,key){
if (Array.isArray(data)){
data.space(key,1) // 这里执行的是数组的变异方法 所以不用再次render
return;
}
delete data[key]
render();
}
完整实现代码 放心食用测试
const data = {
name: '小王',
age: 18,
shan:{
name:'小绿',
age:20
},
arr:[1,2,3]
}
/**
*
* @param data 传入的对象
* @param key 传入的属性名
* @param value 传入的属性名对应的属性值
* @returns {*}
*/
function defineReactive(data,key,value) {
observer(value);
Object.defineProperty(data,key,{
// 读
get() {
console.log(`du`)
return value
},
// 写
set(newVal) {
console.log(`xie`)
if (value === newVal){ // 若渲染判断前 value和后一次传入的新newval一致时,就不再执行后续的渲染和从新赋值操作
return
}
value = newVal
render()
}
})
}
function observer(data) {
if (Array.isArray(data)){
data.__proto__ = arrMethods;
return
}
if (typeof data === 'object'){
for(let key in data){
defineReactive(data,key,data[key])
}
}
}
// 页面渲染
function render() {
console.log(`页面渲染`)
}
const arrayPrototype = Array.prototype;
// 克隆数组原型链上的方法
const arrMethods = Object.create(arrayPrototype);
['push','pop','shift','unshift','sort','splice','reverse'].forEach(method => {
arrMethods[method] = function () { // 更改的是克隆出来的原型
arrayPrototype.call(this,...arguments)
render()
}
})
// 创建$set方法
function $set (data,key,value){
if (Array.isArray(data)){
data.splice(key,1,value) // 这里执行的是数组的变异方法 所以不用再次render
return value
}
// 先被观察
defineReactive(data,key,value);
render()
return value
}
// 创建$delete
function $delete(data,key){
if (Array.isArray(data)){
data.space(key,1) // 这里执行的是数组的变异方法 所以不用再次render
return;
}
delete data[key]
render();
}
observer(data)