菜鸟学Vue源码之defineProperty响应式数据原理实现

1,477 阅读4分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

接上文数据初始化完成之后,就可以对数据进行劫持。Vue2中对数据进行劫持采用了一个Api叫Object.defineProperty()

准备工作

在这里需要提供一个方法去观测data变化,这个方法是一个核心模块(响应式模块),我们单独建一个文件夹来存放在/src/observe/index.js

// src/state.js
import { observe } from "./observe/index"

export function initState(vm){
    // 对数据需要进行劫持
    const opts = vm.$options //获取所有选项
    if (opts.data){
        initData(vm)
    }
}
function initData(vm){
    // 对数据进行代理
    let data = vm.$options.data
    // data可以是函数或者对象,根实例可以是对象,组件data必须是函数
    data = typeof data === 'function' ? data.call(vm) : data
    // 对数据进行劫持 Vue2采用的一个api object.defineproperty
    observe(data)
}
// src/observe/index.js
export  function observe(data){
  debugger
  console.log(data);
}

执行/dist/index.html,当控制台出可以输出{name: 'i东东', age: 18}说明前面的代码没有问题,接下来就可以开始下面的操作了。

第一步 对对象进行劫持

当拿到了data,就可以对data数据进行劫持,如果说他不是对象就不用劫持,所以还需要进行一个判断。

// src/observe/index.js
export  function observe(data){   // 对这个对象进行劫持
  if(typeof data !=='object'|| data == null){
    return // 只对对象进行劫持
  }
}

那么紧接着如何劫持这个对象呢?

如果一个对象被劫持过了,那么就不需要再次被劫持了(要判断一个对象是否被劫持过,可以增添一个实例,来判断是否被劫持过),所以在内部创造了一个类去观测数据,如果数据被观测过那他的实例就是这个类。

// src/observe/index.js
class Observer{
  constructor(data){ //所有数据
    this.walk(data) // 因为data是一个对象,所以就需要对data进行比遍历
  }
  walk(data){ // 循环对象 对属性依次劫持
     Object.keys(data).forEach(key=>defineReactive(data,key,data[key])) //重新定义属性
  }
}
export function defineReactive(target,key,value){ // 闭包  属性劫持
Object.defineProperty(target,key,{
  get(){ //取值的时候会执行get
    return value
  },
  set(newValue){ // 修改的时候执行set
    if(newValue === value) return
    value = newValue
  }
})
}
export  function observe(data){   // 对这个对象进行劫持
  if(typeof data !=='object'|| data == null){
    return // 只对对象进行劫持
  }
  return new Observer(data); // 对这个数据进行观测
}

因为要对每个属性进行劫持,但是Objece.defineProperty()只能劫持已经存在的属性,后增加的或者删除的是不知道的,(Vue2里面会为此单独写一些api 比如:setset delete),所以需要对data进行遍历 this.walk()对属性依次劫持,重新定义属性(性能会差,Vue3中proxy就会好很多),就可以调用defineReactive,因为这个方法可以单独去使用,所以直接导出。

完成之后执行index.html中console.log(vm),会发现vm上只有用户的选项,并没有刚才劫持过的属性,是因为在state.js中我们只是data传入了observe函数,所以就考虑,在vm上增加一个属性,叫_data,这样就相当于把_data对象放在了实例vm上,并且又把这个对象进行了观测,观测的时候依旧回去循环这个对象。

// src/state.js
function initData(vm){
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data
    vm._data = data // 新增这一句
    observe(data)
}

这样再次输出,会发现控制台输出了_data,并且给age,name都增加上来get和set方法,现在说明这个事情就成了。

这个时候就可以通过vm._data.name进行取值

// dist/index.html
const vm = new Vue({
  data(){
    return {
      name:'i东东',
      age:18
    }
  }
})
vm._data.name = 'i东东修改'
console.log(vm._data.name); 

// 用户设置值了
// index.js:15 用户取值了
// index.html:29 i东东修改

第二步 修改取值方法

紧接着就会发现正常我们取值都是vm.name,但是上面的访问还是vm._data.name,所以下面需要将取值的方法进行一下优化。需要在state.js中将vm._data用vm代理。

// state.js
function proxy(vm,target,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[target][key]  // vm._data.name
        },
        set(newValue){
            vm[target][key] = newValue
        }
    })
}
function initData(vm){
    // 对数据进行代理
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data
    vm._data = data
    observe(data)
    // 新增 将vm._data用vm代理
    for(let key in data){
        proxy(vm,'_data',key)
    }
}

这样在index.html中我们就可以用过vm.name重钢访问到数据,也可以通过vm.name = 'i东东修改'去设置值,虽然这样性能是不太好的,但是他用起来会很方便的。所以在这里面相当于代理了两次第一次把用户的数据进行了属性劫持,第二次就是proxy当取值和设置值的时候代理到某个人身上。

第三步 深度属性劫持

// index.html
const vm = new Vue({
  data(){
    return {
      name:'i东东',
      age:18,
      say:{
        hobby:'学习'
      }
    }
  }
})
console.log(vm);

假如说我再增加一个对象say,输出vm会发现hobby并没有被劫持,原因是因为我们只劫持了name、age、say三个属性,如果属性是个对象的话,我们就需要再次劫持。这样我们只需要在defineReactive()里面再次调用observe再次建立劫持,形成递归这样就可以完成对对象的深度属性劫持。

// src/observe/index.js
export function defineReactive(target,key,value){ // 闭包  属性劫持
  observe(value) // 新增 对所有的对象都进行属性接触
  Object.defineProperty(target,key,{
  get(){ //取值的时候会执行get
    console.log('用户取值了');
    return value
  },
  set(newValue){ // 修改的时候执行set
    console.log('用户设置值了');
    if(newValue === value) return
    value = newValue
  }
})
}