Vue2核心原理(简易版)-响应式原理

578 阅读5分钟

Vue2核心原理(简易版)-响应式原理

输入了什么

  • 我们在使用vue.js的时候,options.data的api规定了data接收一个函数(返回对象)或者对象,像下面这样的代码中的data属性符合官方要求。

    new Vue({
      el: '#app',
      data: () => {
        return {
          author: 'fant',
          document: {
            article: { title: 'vue', tag: 'vue'}
          },
          tags: [{ key: 'year', value: '2020'}]
        }
      }
    })
    
    

我们的预期

  • data是Vue 实例的数据对象。Vue 将会递归将 data 的 property 转换为 getter/setter,从而让 data 的 property 能够响应数据变化。

正题,是怎么实现的?

先来看下我画的一张思维导图,先大概看一眼 vue2响应式原理

核心策略就是要把data上面,一种是值为对象的属性(包含此对象上的对象,也包含即将为某属性赋值的新对象)的值变化利用Object.defineProperty给监测到;第二种是值为数组的属性的变化,劫持数组的原型上的方法把即将要新增的数组项(如果仍是对象的话),利用Object.defineProperty进行监测。

接下来让我给你们一步一步分析吧!

  1. data初始化。由于根据官方定义,用户的输入可能是一个函数,但是我们最终想要的是一个对象进行监测,所以可以看到代码第六行,如果是函数的话我们要先拿到其返回值。data.call(vm) 这里用了call方法,其实是为了把data函数中的this指向绑定到当前的vue实例上,否则在data当中将无法访问到当前vue实例。
function initData(vm) {
    // vm.$el vue 内部会对属性检测如果是以$开头 不会进行代理
    let data = vm.$options.data; 
    // vue2中会将data中的所有数据 进行数据劫持 Object.defineProperty
    // 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
    data = vm._data = typeof data === 'function' ? data.call(vm) : data;
    // 用户使用 vm.xxx => vm._data.xxx
    for(let key in data){ // vm.name = 'xxx'  vm._data.name = 'xxx'
        proxy(vm,'_data',key); 
    }
    observe(data);
}
  1. 开始观测data。这里是观测的入口,因为进入的data也就是我们初始化过后的对象,这里只允许是一个对象,所以我们要对这个对象上所有的属性进行观测。创建了一个Observer类,把data传入进去,让他帮我们完成接下来所有的监测。
export function observe(data) {
    // 如果是对象才观测
    if (!isObject(data)) {
        return;
    }
    if(data.__ob__){
        return;
    }
    // 默认最外层的data必须是一个对象
    return new Observer(data)
}
  1. 观测(observer)-添加__ob__属性。我们设置了不可枚举的__ob__属性到对象,并且value为this,是有两大原因。
    • value=this是为了后面数组劫持原型上的方法的时候,有新增的内容要进行继续劫持需要观测的数组里的每一项。
    • 不可枚举是以防在对对象defineReactive(定义响应式)的时候会无限循环下去,爆栈。
  2. 观测(observer)-数组或者对象监测。
    • 对象(observeObject(walk方法)): 遍历每一个key,使用Object.defineProperty劫持它们的set方法

    如果劫持的key本身的value仍然是对象,则递归observe监测它
    如果set的新value值仍然是对象,则也需要递归observe监测它

    • 数组(observeArray): 这里又分为两种,一是数组本身为对象的项,这里我们直接递归调用observe去监测就可以了,那么如果是在数组上进行一些数组本身的api操作,我们就需要去劫持这些方法,插入我们自己的逻辑,就是切片编程的思想。我们可以看到我们做了如下几件事情:
      a. data.__proto__ = arrayMethods;这么做是利用了js的特性,这里的data其实是一个数组,当我们去调用他上面的方法(如splice,push...)的时候,我们实质上调用到的是我们在arrayMethods中定义好的方法。
      b. let oldArrayPrototype = Array.prototype;let arrayMethods = Object.create(oldArrayPrototype);这两句其实就是在给arrayMethods做原型继承,让他继承到Array原型上的原生方法(也就是ECMA定义的方法)。
      c. const result = oldArrayPrototype[method].call(this,...args);别忘了同时要调用原生的方法并且返回结果。
      d. if(inserted) ob.observeArray(inserted)如果有新增的数组项,则要对继续其进行监测,这里看到的ob(this.__ob__),也就是我们在Observer类构造函数里面定义的__ob__属性了,它的value值就是那个data,所以会有observeArray这个方法!
class Observer {
    constructor(data) { // 对对象中的所有属性 进行劫持
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false // 不可枚举的
        })
        // data.__ob__ = this; // 所有被劫持过的属性都有__ob__ 
        if(Array.isArray(data)){
            // 数组劫持的逻辑
            // 对数组原来的方法进行改写, 切片编程
            data.__proto__ = arrayMethods;
            // 如果数组中的数据是对象类型,需要监控对象的变化
            this.observeArray(data);
        }else{
            this.walk(data); //对象劫持的逻辑 
        }
    }
    observeArray(data){ // 对我们数组的数组 和 数组中的对象再次劫持 递归了
        // [{a:1},{b:2}]
        data.forEach(item=>observe(item))
    }
    walk(data) { // 对象
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key]);
        })
    }
}
let oldArrayPrototype = Array.prototype
export let arrayMethods = Object.create(oldArrayPrototype);
// arrayMethods.__proto__ = Array.prototype 继承

let methods = [
    'push',
    'shift',
    'unshift',
    'pop',
    'reverse',
    'sort',
    'splice'
]

methods.forEach(method =>{
    // 用户调用的如果是以上七个方法 会用我自己重写的,否则用原来的数组方法
    arrayMethods[method] = function (...args) { //  args 是参数列表 arr.push(1,2,3)
        const result = oldArrayPrototype[method].call(this,...args); // arr.push(1,2,3);
        let inserted;
        let ob = this.__ob__; // 根据当前数组获取到observer实例
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args ; // 就是新增的内容
                break;
            case 'splice':
                inserted = args.slice(2)
            default:
                break;
        }
        // 如果有新增的内容要进行继续劫持, 我需要观测的数组里的每一项,而不是数组
        // 更新操作.... todo...
        if(inserted)  ob.observeArray(inserted)
        return result
    }
})
  1. 具体怎么监测。最后了,如果不懂Object.defineProperty的话去MDN看一下吧,这里的逻辑在上一步的对象分类里面已经讲过了,不再赘述。
// vue2 会对对象进行遍历 将每个属性 用defineProperty 重新定义 性能差
function defineReactive(data,key,value){ // value有可能是对象
    observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newV){ 
            // todo... 更新视图
            observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
            value = newV;
        }
    })
}

完 🎉

下一讲,模版编译