小白的Vue之路(1)- 响应式原理

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 不是对所有的属性名进行监控,故需要对需要监控的对象,所有的属性名进行监控,对其后续处理我们需要理解和解决以下的问题:

  1. 需要遍历所有的对象从而监听所有的属性名
  2. 当对象中嵌套对象时,无法一次性的获取到所有的属性名,转思路对传入的 data 进行类型判断,若需要监听的数据类型是 ‘object’ 时,则进行遍历的操作。
  3. Object.defineProperty 的本身缺点,对象的增删的操作,是无法进行监听到的。是在于set的配置方法无法被执行!
  4. 关于传入对象是数组时,不在对 Object.defineProperty 进行限制时,对改变数组 length 内索引的元素时,是可以监听到元素的改变的。数组的本质也是对象,在超过数组索引时赋值,等同于创建对象,创建的对象是无法被监听
// 举个栗子:
data.arr[1] = 100     // 元素的改变 可以被监听
data.arr[100] = 100     // 相当于对象的创建,实际无法被监听
  1. 既然是在索引内的改变,为什么不可作为监听的一个元素呢? -------->是由于在页面中,会存在大量的数组,数组的for in 遍历非常消耗性能:性能的代价和获得的用户体验不成正比,故在Vue在 Object.defineProperty 中是会限制对数组传入
  2. 针对无法对数组的改变, 从而 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()  // 重新渲染页面
}
  1. 就是将原本Array身上原型链方法存到一个新的变量oldPush中
  2. 再将自身的Array原型链方法(push)从新赋值
  3. 赋值的过程,直接调用到 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 的使用方法,它可以传递三个参数,分别代表:

  1. 改变的对象
  2. 需要修改谁(数组中体现为要修改的索引对象)
  3. 要修改成什么样子

在传入数组的时候,相当于是 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方法,它可传入两个值,分别为:

  1. 改变的对象
  2. 要删除谁

实现代码如下:

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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值