二、Vue响应式原理:对象的劫持

126 阅读3分钟

1、编写vue数据

创建一个vue的实例 vm.

 <script>
        const vm = new Vue({
            data:{
                name:"xxxx",
                age:20,
                other:{
                school:"xxx"
                }
            }
        })
    </script>

2、导出一个vue构造函数

1)编写一个Vue构造函数并导出

function Vue(options){
}
export default Vue;

2)从options中获取传入的参数

import { initMixin } from "./init";

function Vue(options){
    this._init(options);
}

initMixin(Vue);

export default Vue;

3)为Vue添加init 原型方法

//init.js
import initState from "./state";

//初始化vue数据
export function initMixin(Vue){

    Vue.prototype._init = function(options){
        const vm =this;
        vm.$options = options; //将用户的选项挂载到实例上

        //初始化状态
        initState(vm);


    }
}

4)初始化数据状态

export default function initState(vm){
    // 从vm上获取所有的选项 如果选项上含有 data 属性
    //则执行initData 方法初始化数据
    const opts = vm.$options; 
    if(opts.data){
        initData(vm);
    }
}

5)initData 方法对数据进行劫持 此时从vm上获取到的data可能是个函数,也可能是个对象,如果是函数,则取函数的返回值,如果是对象则直接获取data 例如:

 //data在根组件中的写法可以是 对象格式{name:xxx},也可以是函数形式
 // data(){return {name:xxx}}
 
import { observe } from "./observe/index";

function initData(vm){
    let data =  vm.$options.data; 
    vm._data = data = typeof data === "function" ? data.call(this) : data;
     // 用户使用 vm._data来获取有些麻烦, 我们希望可以通过vm.xxx -> vm._data.xxx
    for(let key in data){
        proxy(vm,'_data',key); // 循环代理属性, 为了用户使用的时候 直接可以通过vm.xxx
    }
    //对数据进行劫持 defineProperty 
    observe(data)
}

function proxy(target,key,property){ // vm.xxx -> vm._data.xxx
    Object.defineProperty(target,property,{
        get(){
            return target[key][property]
        },
        set(newValue){
            target[key][property] = newValue;
        }
    })
}



3、对数据进行劫持

1)只会对对象进行劫持,如果不是对象则直接返回

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

    //如果一个对象已经被劫持过了,那么就不需要再次劫持
    // todo...
    return new Observer(data)
}

2)定义一个类 Observe,获取到所有的属性,并且遍历现有属性重新定义成响应式的数据(通过数据劫持的方式Object.defineProperty),这样会使得data中的所有数据被重新定义,所有性能会变得差; 因为这个原因我们平时写的一些变量层级最好不要太深,如果某个变量不需要具备响应式,也没有必要定义在data中。

//./observe/index.js
class Observer{
    constructor(data){
        this.walk(data);
    }
    walk(target) {
        Object.keys(target).forEach(key => {
            defineReactive(target, key, target[key]);
        })
    }
};

3)定义响应式数据 在Js中,使用Object.defineProperty来侦测一个对象的变化, 进行数据劫持或数据代理。 这里vue在初始化数据时,对于不存在的属性,不会被劫持,不能被定义成响应式;

例如一个对象形式

const vm = new Vue({
           name:xxx,
           age:12,
           other:{
             school:"xxx"
             }
           })

在上面的vm中,通过 Object.defineProperty定义的属性,会给每个属性添加getter和setter 方法,形式会是

      age: (...)
      name: (...)
      otherObject
      get age: ƒ ()
      set age: ƒ (newValue)
      get name: ƒ ()
      set name: ƒ (newValue)
      get other: ƒ ()
      set other: ƒ (newValue)

3.1)但是在vm.other中的school中并不会有get和set方法,如此如果通过vm.other.school="***"改变了school的值,并不会被监测到,为了解决这个问题,就需要对属性进行递归类型监测;(例如以下#111处) 3.2)如果在对vm.other整个属性重新赋值的时候(vm.other={kk:"xxx"}),改变了引用地址,里面的属性就需要再次被观测,重新定义成响应式 (例如以下#222处)

//./observe/index.js
function defineReactive(target, key, value) { // 定义响应式
    // 不存在的属性不会被defineProperty
    observe(value); // 递归对象类型检测 #111
    Object.defineProperty(target, key, { // 将属性重新定在对象上,增加了get和set(性能差)
        get() {
            return value;
        },
        set(newValue) {
            if (newValue === value) return
            observe(newValue); // 设置的值如果是对象,那么就再次调用observe让对象变成响应式的 #222
            value = newValue;
        }
    })
}



4、测试数据

定义一个实例,并且取值和设置值

 <script>
        const vm = new Vue({
            data:{
                name:"xxxx",
                age:20,
                other:{
                school:"xxx"
                }
            }
        })
        vm.other={sex:"xx"}
       console.log(vm)
        
    </script>

查看输出结果

  age12
  name"xxxx"
  otherObject
    sex: (...)
      get sex: ƒ ()
      set sex: ƒ (newValue)
  get age: ƒ ()
  set age: ƒ (n)
  get name: ƒ ()
  set name: ƒ (n)
  get other: ƒ ()
  set other: ƒ (n)

5、完成对象的响应式

thank you for your reading !!!