Vue2源码学习 - 2.初始化数据,属性劫持,对象的响应式原理,数组的函数劫持

目录

1,初始化数据

2,属性劫持

3,对象的响应式原理

4,数组的函数劫持


1,初始化数据

首先要创建一个Vue的实例vm,由于我们需要构建一个类来将所有的方法耦合在一起,以前我们会用 class Vue{ } 的方法,将方法功能 xxx() 写在类里面,但是一般不会这么做。Vue本身采用构造函数来扩展方法 function Vue{ },最后暴露这个Vue,就是new Vue。

<script>
        // 响应式的数据变化,数据变化了可以监控到数据的变化
        // 数据的取值和更改值 我们要监控到
        const vm = new Vue({
            data() {
                return {   //代理数据
                    name:'zf',
                    age:20,
                    address:{
                        num:30,
                        content:'回龙观'
                    }
                }
            },
        })
        console.log(vm);
</script>

我们在new Vue时,会传入一个对象options,就是用户的选项,根据以上,这里的options指的只有data,若有computed,则还包括computed等等。

拿到选项后,要做的就是Vue的初始化,这里就会原型的方式 Vue.prototype._init 写一个初始化的功能,用于初始化操作,new Vue的时候,就会自动调用初始化。

由于后期功能会越来越多,我们希望每个功能都是独立的,所以将初始化的功能单独放在 init.js 文件中,而且由于和Vue构造函数不是在同个文件中,会拿不到Vue,这里也需要在 init.js 文件中暴露另一个函数 initMixin ,将初始化的功能写在里面,再在入口文件index.js 中引入。

new Vue的时候自动调用初始化 initMixin ,这时传入options参数,将options绑定到this上,这个this下文使用vm代替,也就是将用户的选项挂载到实例上,方便后续其他函数需要拿到options。(扩展:为什么用$options?我们使用的vue的时候  $nextTick $data  $attr.....,在前面加上$符是默认这些就是Vue自己的属性,假设在data中,有一个$name的属性,则vm是拿不到这个$name的。)

import { initMixin } from "./init"

// 将所有的方法都耦合在一起
function Vue(options){    //options就是用户的选项,目前这里只有data,当new Vue里有别的例如computed,那也包含computed
    // 默认就调用了init,将用户选项传过来,去调用初始化
    this._init(options)
}

initMixin(Vue);   //扩展了init方法

export default Vue
import { initState } from "./state";

export function initMixin(Vue){   //就是给Vue增加init方法的
    Vue.prototype._init = function(options){    //用于初始化操作
        // vue  vm.$options  就是获取用户的配置
        const vm = this ;
        vm.$options = options   //将用户的选项挂载到实例上

        // 接下来就是对数据进行处理,也就是初始化状态 props,data,computed....
        initState(vm); 
    }
    /* Vue.prototype.xxx = function(){
        当这里要拿options的时候就拿不到了,所以要在上面vm.$options = options
    } */
}

接下来要做的就是初始化数据 initState,要先看有没有data属性,如果有,就对data进行初始化,再对数据进行劫持。

2,属性劫持

export function initState(vm){
    // 因为vm身上有$options,所以这里可以拿到
    const opts = vm.$options;   //获取所有的选项,要进行数据劫持
    if(opts.data){
        initData(vm);    //对data进行初始化
    }
}

给vm增加一个属性_data,也就是将对象data放在实例上,并且下面会对对象进行观测,所以读取data属性的时候需要用vm._data.xxx,所以接下来要做的就是把vm.xxx代理到vm._data.xxx

function initData(vm){
    let data = vm.$options.data;   //data可能是函数也可能是对象
    data = typeof data === 'function'? data.call(vm) : data;   //如果是函数,就让data这个函数执行,返回对象,但是这样this有问题,我们希望this指向vm实例。否则data就是数据
    // console.log(data);  //这里输出的是对象,因为data函数return的是对象

    vm._data = data;   //将返回的对象放到了_data上
    // 对数据进行劫持  vue2里采用了一个api  defineProperty
    observe(data)   //观测的方法,从这里开始就是响应式模块

    // 将vm._data 用vm来代理就可以
    for(let key in data){
        proxy(vm,'_data',key);
    }  
}
export function observe(data){

    // 对这个对象进行劫持
    if(typeof data !== 'object' || data == null){
        return;   //只对对象进行劫持
    }
    
    return new Observer(data);
}

如果一个对象被劫持过了,那就不需要再被劫持了,要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过。所以在内部创造一个类 class Observer,专门观测数据,如果这个数据被观测过,则这个实例就是这个类。

3,对象的响应式原理

class Observer{
    constructor(data){
        // Object.defineProperty只能劫持已经存在的属性,后增的或者删除的不知道(vue里面会为此单独写一些api $set $delete)
        // 遍历对象
        this.walk(data);
    } 
    walk(data){    //循环对象 对属性依次劫持
        // 拿到key后重新定义属性,调用defineReactive方法,把某个数据定义成响应式的
        Object.keys(data).forEach(key=>defineReactive(data,key,data[key]))  //定义的数据是data,属性是key,值是data[key]
    }
}
export function defineReactive(target,key,value){   //闭包, get和set的时候都能拿到value,当前函数的作用域和执行栈没有被销毁  属性劫持
    // 当value是个对象时,例如data是个对象,里面再套个对象,所以这里需要再走observe
    observe(value)   //对所有的对象进行属性劫持
    Object.defineProperty(target,key,{
        // 取值和修改的时候都是用到value,也就是函数Object.defineProperty用到外部函数defineReactive的变量,所以这个变量不能被销毁,也就是闭包
        get(){   //取值的时候,会执行get
            return value
        },
        set(newValue){   //修改的时候,会执行set
            if(newValue === value) return    //如果newValue和value一样则不执行
            value = newValue
        }
    })
}

如果 initData 函数没有操作  vm._data = data ,则此时打印vm只有用户的选项,没有劫持过的数据。因为只是把data传进去。加上  vm._data = data,则是把data这个对象挂载到实例上,并且对data进行观测 observe(data),观测的时候依旧会去循环对象,调用 defineReactive方法,将它定义成响应式。

 

 

此时当我们去取值必须用 vm._data.name,取值的时候会调用get方法。为了使取值时直接用 vm.name。我们定义了一个proxy方法,使得在 initData 方法中,调用proxy,相当于把这个对象的属性重新代理。

state.js

// 将vm._data 用vm来代理就可以
    for(let key in data){
        proxy(vm,'_data',key);
    }

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

总结:循环对象,用defineReactive 方法把属性重新定义成响应式,如果值还是对象的话,我们需要对这个对象进行递归操作,这样用户在取值和修改的时候我们可以监控到。但是我们这个对象被劫持完之后,为了能方便获取,此时我们把data放到了vm上,但是这样写还是比较麻烦。所以让用户到vm上取值的时候,就直接去vm._data上取值。 

4,数组的函数劫持

用户平时修改数组,很少用索引来操作,当数组里面太多数据时,做循环和劫持性能较差,一般都是通过方法来修改,push,shift等等。所以这里我们就不走walk方法去循环,当data里是个数组时,hobby:['eat','drink',{a:1}],我们就在data上套一层判断是否是数组。如果是数组,就重写数组的方法。而且数组里面有可能还有对象,所以除了修改数组之外,还要对数组里的引用类型进行劫持。如果判断不是数组,就还是走walk方法,循环对象。

observe  index.js

class Observer{
    constructor(data){
        //这个this指的是Observer的实例,把Observer的实例赋值到对象的自定义属性上
        data.__ob__ = this;     //给数据加了一个标识,如果数据上有_ob_,则说明这个属性被观测过

        if(Array.isArray(data)){
            // 这里可以重写数组中的方法,7个变异方法,是可以修改数组本身的   
            data.__proto__ = newArrayProto     
            this.observeArray(data);   //如果数组中放的是对象,可以监控到对象的变化
        }else{
            // 遍历对象
            this.walk(data);
        }        
    } 
    walk(data){    //循环对象 对属性依次劫持
        // 重新定义属性
        Object.keys(data).forEach(key=>defineReactive(data,key,data[key]))
    }
    observeArray(data){    //观测数组
        data.forEach(item => observe(item))
    }
}

由于数组里面有可能还有对象,所以要对数组里的每一个值都进行观测,这里写一个方法observeArray,将里面的每一项都变成响应式的。这样通过 vm.hobby[2].a ,可以监控到对象的变化,但是用 vm.hobby.push('1') 只能触发get,无法触发修改。这里就需要重写当前数组的所有方法。例如重写一个push方法,给当前的数组重写一个对象__proto__,给当前data的原型链指向一个新的原型。(Array/String/Boolean/Object/Function.__proto__===Function.prototype

data.__proto__ = {
    push(){
        console.log('重写的push')
    }
}

但是这样做不合理,会把原始的push方法覆盖掉,所以要保留数组原有的特性,并且可以重写部分方法。新建 array.js 重写数组部分方法。 

要先获取数组的原型 Array.prototype,但是不能用 Array.prototype.push = function(){ } 直接修改,这样会把原来的push方法覆盖。所以先将这个数组原型 oldArrayProto 拷贝一份,这样不会影响原来的,生成一个新的数组方法,也就是最后要暴露出去的newArrayProto。这时候 newArrayProto 可以通过原型链拿到 oldArrayProto,即 newArrayProto.__proto__ = oldArrayProto。在newArrayProto上重写方法比如 newArrayProto.push= function(){ },不会影响到 oldArrayProto,所以也不用担心会被覆盖掉。

声明一个关于 push pop等七个变异方法的数组,然后以属性的方式添加到新的原型上,重写的方法其实就是调用之前保留的旧原型方法,同时需要传参数,此时会导致this指向有误,要用 call 更正this。谁调用push,this指向谁。内部调用原来的方法,这种模式叫做函数的劫持。暴露之后data.__proto__ 就能拿到newArrayPoto,无论调七个方法的任意一个,都可以被监控到,这就实现数组的劫持。 

但是这样操作,假设用 vm.hobby.unshift({a:1}) ,新增的对象没有被劫持到,原因是我们只是拦截了方法,并没有对新增的这些选项做处理,所以还需要对新增的数据再次进行劫持,声明一个数组 inserted,将能新增元素的三个方法 push,unshift,splice 所新增的数据放在 inserted,后续用obsereArray 对该数组进行观测,看新增的是不是对象。

此时,由于在不同的文件拿不到该方法,此时能拿到的只有this,即调用数组的arr,这个this和Observer里的data是同一个 ,所以在data上放一个自定义的属性__ob__把this放上去,即data.__ob__=this,这个this指的是Observer的实例,把Observer的实例赋值到对象的自定义属性__ob__上,这样 array.js 就可以拿到 this.__ob__,声明个变量ob接收,这样就可以直接调用观测数组的方法ob.observeArray(inserted)。

array.js

let oldArrayProto = Array.prototype;   //获取数组的原型

// newArrayProto.__proto__ = oldArrayProto 
export let newArrayProto = Object.create(oldArrayProto);    //拷贝一份

let methods = [   //找到所有的变异方法,即能改变原数组的方法
    'push',
    'pop',
    'shift',
    'unshift',
    'reverse',
    'sort',
    'splice'
    // concat slice 都不会改变原数组
]

methods.forEach(method=>{
    // arr.push(1,2,3)
    newArrayProto[method] = function(...args){    //这里重写了数组的方法
        // 直接调用的话push(),this不一样,要改成arr,谁调的push this指向谁
        const result =  oldArrayProto[method].call(this,...args)  //这里继续用oldArraryProto是因为是在原有的方法上重写
        // 需要对新增的数据再次进行劫持
        let inserted;
        let ob = this.__ob__;   //这里的this和data是同一个,所以在data上放__ob__,这里也可以取到Observer的实例,再拿到observeArray
        switch (method) {
            case 'push':    //arr.push(1,2,3)  args放的就是追加的内容
            case 'unshift':   
                inserted = args;
                break;
            case 'splice':   //arr.splice(0,1,{a:1},{a:1})
                inserted = args.slice(2)
            default:
                break;
        }
        console.log(inserted);   //新增的内容
        if(inserted){
            //对新增的内容进行观测  inserted是个数组
            ob.observeArray(inserted);
        }
        return result
    }
})

这样同时也给数据加了一个标识,如果数据上有_ob_,则说明这个属性被观测过了,这样observe上也可以再加一个判断 。

export function observe(data){

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

    if(data.__ob__ instanceof Observer){//说明这个对象被代理过了
        return data.__ob__;
    }
    return new Observer(data);
}

当data不是数组而是对象时,也会添加__ob__标识,再走walk对这个对象做循环,循环时也会去遍历__ob__属性,就会产生递归。

所以要让循环时不能遍历到__ob__,就让这个属性变成不可枚举的,不可循环,不可取值。

//data.__ob__ = this;     //给数据加了一个标识,如果数据上有_ob_,则说明这个属性被观测过

Object.defineProperty(data,'__ob__',{
       value:this,
       enumerable:false   //将__ob__变成不可枚举(循环的时候无法获取到)
})

 这就实现数组的劫持,核心就是重新数组的方法,并且观测数组中的每一项。如果是数组,我们需要针对数组新增的属性做判断,并把数组的每一项再进行观测。如果调的是concat等没有重写的方法,那调的就是数组原来的方法。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值