Vue响应

1. 初始化Vue

Vue的官网解释

  • Vue 是一套用于构建用户界面的渐进式框架,

  • Vue 并没有完全支持 MVVM 模型,但 Vue 的设计受到了它的启发,

  • 变量名 vm 是 vue model 的缩写,表示 vue 实例;

1.1 使用Vue
  1. 在index.html中初始化Vue

    <script>
        <!-- 初始化 Vue,传入 options 对象 -->
        let vm = new Vue({
            el: '#app',
            // 1,data 是对象
            data: {
                msg: "zhiyu"
            }
            // 2,data 是函数,返回一个对象
            // data() {
            //   return { msg: "zhiyu" }
            // }
        });
    </script>
    

    Vue在初始化时,会传入el挂载点、data数据等,在初始化完成之后,data中的数据会变成响应式数据(在根组件时data可以使对象也可以是函数,因为根组件不会被共享,而非根组件data必须是函数,否则数据会被多组件共享)

1.2 初始化Vue
  1. 在src下新建index.js文件,然后在Vue原型上扩展一个_init方法,用于Vue的初始化操作

    /**
     * vue中的所有功能,都是通过原型扩展的方式添加的
     * @param {*} options  new Vue时传入的 options 配置对象
     */
    function Vue(options){
        this._init(options); // 调用Vue原型上_init方法
    }
    // 在Vue原型上扩展一个原型方法_init,用于vue的初始化操作
    Vue.prototype._init = function(options) {
    
    }
    export default Vue;
    
  2. 在src下新建init.js文件,用于初始化操作的原型方法_init,单独抽离成一个独立的initMixin,导入src/index.js文件使用

    // index.js
    import { initMixin } from "./init";
    function Vue(options){
      // 初始化
      this._init(options);
    }
    initMixin(Vue);
    export default Vue
    
    // init.js
    // src/init.js
    export function initMixin(Vue) {
      Vue.prototype._init = function (options) {
        console.log(options)
      }
    }
    

    注意:原型方法_init的this指向当前vm实例

  3. 用户通过new Vue实例化时会传入options对象,为了便于Vue中其他方法便于获取options对象,直接将options选项挂载到vm实例上,即vm.$options = options

    注意:vm.$xxx 变量命名方式,表示 vue 内部变量;

  4. 由于options里不仅有data, 还有props, watch, computed…所以需要一个统一的函数,对数据的初始化进行集中处理,initState状态初始化方法(src/initState.js)

    // src/index.js
    	...
        
        // 在 new Vue 时,传入的 options 选项中包含 el 和 data
        vm.$options = options;
    
        // 状态的初始化
        initState(vm);  
    
        // 处理数据渲染并挂载到el
        if (vm.$options.el) {
          console.log("有el,需要挂载")
        }
      }
    }
    
    // initState.js
    export function initState(vm) {
      let ops = vm.$options;
      if(ops.props) {
        initProps(vm);
      }
      if(ops.data) {
        initData(vm);
      }
      // 还有其他比如methods、watch等
    };
    // vue2对 data初始化:
    function initData(vm) {};
    function initProps() {};
    
2. 数据劫持
2.1 对象的单层劫持

Vue 响应式原理核心是通过Object.defineProperty为属性添加 get、set 方法,从而实现对数据操作的劫持

  1. 首先在initData中可以获取到data数据,通过vm.$options.data获取

  2. 然后处理data的两种情况(对象和函数)

    • 如果data是函数,执行此函数,并得到函数内部返回的对象(此时this指向的是window,所以data是函数时,要改变this指向,使其指向当前vm实例)
    • 如果data是对象,无需处理
      data = typeof data === "function" ? data.call(vm) : data;
    
  3. 对数据进行观测:通过模块observer,创建入口文件src/observer/index.js, 经过initState.js文件处理之后,data一定是对象,所以在观测时在对data进行一次类型检测

    // src/observer/index.js
    export function observer(data) {
      if(typeof data != "object" || data == null) {
        return data;
      }
    }
    
    // initState.js
    import { observer } from "./observer/index";
    export function initState(vm) {
      let ops = vm.$options;
      // 判断实例上有没有这些属性
      if(ops.data) {
        initData(vm);
      }
    };
    // vue2对 data初始化:
    function initData(vm) {
      // 首先判断data是对象还是函数
      let data = vm.$options.data;
      // 注意:函数this本来指向全局对象window,所以需要改变this指向
      data = vm._data = typeof data === "function" ? data.call(vm) : data;
      // 对数据进行劫持
      observer(data);
    };
    
  4. 对对象进行观测:创建observer类,遍历data对象,使用Object.defineProperty重新定义data对象中的所有属性

    export function observer(data) {
        ...
        return new Observer(data)
    }
    class Observer {
        // 类的构造函数 
        constructor(value){
            // 遍历对象中的属性,使用 Object.defineProperty 重新定义
            this.walk(value);
        }
    
        // 循环 data 对象,使用 Object.keys 不循环原型方法
        walk(data){
            // Object.keys(data).forEach(key => { 
            //     defineReactive(data, key, data[key]);
            // });
            let keys = Object.keys(data); // 把对象中的所有属性转化为一个数组
            for(let i = 0; i < keys.length; i++) {
                // 对每个属性进行劫持
                let key = keys[i];
                let value = data[key];
                defineReactive(data, key, value);
            }
        }
    }
    
    /**
     *    使用Object.defineProperty重新定义data对象中的属性
     * @param {*} obj   需要定义属性的对象
     * @param {*} key   给对象定义的属性名
     * @param {*} value 给对象定义的属性值
     */
    function defineReactive(obj, key, value) {
        Object.defineProperty(obj, key, {
            get(){           
                return value;
            },
            set(newValue) {
                if (newValue === value) return;
                value = newValue;
            }
        })
    }
    
2.2 对象的深层劫持

描述:如果data数据中的对象存在多层嵌套(比如:return { obj: {name: "zhiyu"} }),使用前面的方法将不会被劫持

实现:在defineReactive中进行修改

function defineReactive(data, key, value) {
    // 对 key 进行观测前,调用 observer方法,如果属性值为对象则会继续向下找,实现深层递归观测
    observer(value); // 深度代理/深度劫持 {a: {b: 1}}
    Object.defineProperty(data, key, {
        get() {
            // console.log("获取");
            return value;
        },
        set(newValue) {
            // console.log("设置");
            if(newValue == value) return;
            // 当值被修改时,通过 observe 实现对新值的深层观测,此时,新增对象将被观测
            observer(newValue);
            value = newValue;
        }
    })
}
2.3. 数组的劫持

前言:其实通过前面对象的劫持也是可以实现数组的劫持,但是在 Vue2.x 中,是不支持通过修改数组索引或长度来触发更新的(出于性能的考虑)

对数组进行劫持的核心目标,还是要实现数组的响应式:

  • 在 Vue 中,认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort;
  • 对以上 7 个方法进行特殊处理,使他们能够劫持到数组的数据变化,就能够实现数组的响应式;

实现思路:

  1. 根据分析,数组和对象不能采用相同的处理方式,在observer初始化时会walk遍历属性实现观测,所以,在此需要单独采取对应的逻辑

    import { ArrayMethods } from "./arr";
    
    class Observer {
      constructor(value) {
        if(Array.isArray(value)){
          // 对数组类型进行单独处理:重写 7 个变异方法
        }else{
          this.walk(value);
        }
      }
    }
    
  2. 新建observer/arr.js,实现数组方法重写、

    // 方法函数劫持,劫持数组方法 arr.push()
    // 重写数组
    // 1. 获取数组的原型方法
    let oldArrayProtoMethods = Array.prototype
    // 2. 原型继承
    export let ArrayMethods = Object.create(oldArrayProtoMethods);
    // 3. 重写能够导致原数组变化的七个方法
    let methods = [
        "push",
        "pop",
        "unshift",
        "shift",
        "splice",
        "reverse",
        "sort"
    ];
    // // 在数组自身上进行方法重写,以实现对链上同名方法的拦截效果
    methods.forEach(item => {
        ArrayMethods[item] = function(...args) {
            // console.log("劫持数组");
        }
    })
    
  3. 在new observer时, 对数组类型的数据进行链上方法的重写

    ...
    constructor(value) {
        // 分别处理 value 为数组和对象两种情况
        if(Array.isArray(value)){
          value.__proto__ = ArrayMethods; // 更改数组的原型方法
        }else{
          this.walk(value);
        }
      }
    ...
    
  4. 数组数据变化时需要在劫持到数据变化后,进行处理

    • 通过oldArrayPrototype[method].call(this, …args)执行push原生方法逻辑并绑定当前上下文,实现原数组的更新操作;
    • 收集通过splice、push、unshift方法新增的数据,放入inserted数组;
    • 遍历inserted数组,当数据为对象类型时,需要继续进行观测;
    // arr.js
    methods.forEach(item => {
      ArrayMethods[item] = function(...args) {
        // 绑定到当前调用上下文
        let result = oldArrayProtoMethods[item].apply(this, args);
        // 数组追加对象的情况 arr.push({a: 1})
        let inserted = null; // 收集新增的数据
        switch(item) {
          case "push":
          case "unshift":
            inserted = args;
            break;
          case "splice":
            inserted = args.splice(2); // 获取新增数据:从第三个参数开始都是新增数据
            break;
        }
        // 遍历 inserted 数组中的新增数据,对象类型需要继续进行观测
      }
    })
    
  5. 由于Observer类中的原型方法observeArray实现了数组的深层劫持,但此方法并未对外导出;所以,在当前模块中遍历inserted数组时,就无法调用到Observer类中的observeArray方法实现数据观测

  6. 为了让当前数组或对象与Observer实例产生一个关联关系,在Observer初始化时,为当前数组或对象value添加自定义属性__ob__,使valueObserver实例之间产生关联

    • value:为当前数组或对象,添加自定义属性__ob__ = this;(在 observe 方法中,只有值为对象类型时即数组或对象,才会执行new Observer创建实例,因此value必为数组或对象)
    • this:为当前Observer实例,通过实例可以调用到observeArray方法
    // src/observer/index.js 
    constructor(value) {
        // 给value定义一个属性
        Object.defineProperty(value, "__ob__", {
          enumerable: false, // 不能够进行枚举
          value: this, // 指向实例
        })
         ...
     }
         
    // arr.js
    methods.forEach(item => {
      ArrayMethods[item] = function(...args) {
        ...
        let ob = this.__ob__;
        if(inserted) {
          ob.observerArray(inserted); // 对添加的对象进行劫持
        }
        return result;
      }
    })
    
  7. 对象被重复观测:当对象被添加__ob__属性标识后,代表着当前对象已经被创建过Observer实例了,即当前对象已经被深层观测过了,在之后的处理中应避免重复观测

    export function observer(data) {
      // 1. 对象的数据处理,判断是不是对象以及是否为空
      if(typeof data != "object" || data == null) {
        return data;
      }
      if(data.__ob__){
        console.log("当前数据已经被观测过了,data = "+ data)
        return;
      }
      // 通过类进行劫持
      return new Observer(data);
    }
    
2.4 数据代理

含义:就是实现vm.msg$options.data.msg等效,所以要想办法将vm实例操作“代理”到$options.data上;这样,就实现了 “Vue 的数据代理”

实现思路:

  1. 首先,先做一次代理,将data挂载到vm._data下,这样vm实例就可以在外部通过vm._data.msg获取到vm.data
  2. 之后,再做一次代理,将vm实例操作vm.msg代理到vm._data上,这样,外部就可以直接通过vm.msg获取到data.msg

代码实现:

  1. 在Vue初始化阶段,通过observer()实现数据响应式之后,通过Object.defineProperty_data中的数据操作进行劫持;将vm.xxxvm实例上的取值操作,代理到vm._data.xxx

    // initState.js
    function initData(vm) {
      ...
      // 对数据进行劫持
      observer(data);
      // 将data上的所有属性代理到实例上vm {a: 1, b: 2}
      for(let key in data) {
        proxy(vm, "_data", key);
      }
    };
    // vm: vm实例  source: 代理目标  key: 属性名
    function proxy(vm, source, key) {
      Object.defineProperty(vm, key, {
        get() {
          return vm[source][key];
        },
        set(newValue) {
          vm[source][key] = newValue;
        }
      });
    };
    
2.5 数组的深层劫持

分析:通过调试,发现之前的代码不能实现数组嵌套的劫持,而数组嵌套又分为数组嵌套数组和数组嵌套对象两种,想要对数组嵌套实现数据观测,就需要对数组内部的数据继续进行递归处理

  1. 在Observer类中,创建observerArray方法,对数组进行深层观测

    class Observer {
      constructor(value) {
        // 对数据进行判断是数组还是对象
        if(Array.isArray(value)) {
          value.__proto__ = ArrayMethods
          // 如果是数组对象
          this.observerArray(value); // 处理数组嵌套 [{a: 1}]
        } else {
          this.walk(value); // 进行遍历
        }
      }
      observerArray(value) {
        // 对数组中的每一项调用 observe 方法,继续进行深层观测处理;
        // observe 方法内:如果是对象类型,继续 new Observer 进行递归处理
        for(let i = 0; i < value.length; i++) {
          observer(value[i]);
        }
      }
    };
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值