1

(一)变化侦测

该系列主要对Vue源码流程分析与简单实现,会省略源码中的一些细节

  • 初始化
  • Object变化侦测
  • Array变化侦测

observe流程图

image.png

1、初始化

  • 定义Vue构造函数
  • 向Vue原型混入操作方法,方便后期扩展
  • 在初始化函数中进行 state初始化 -> data初始化
// index.js
import {initMixin} from "init.js"
const Vue = function(options){
    // 选项初始化
    this._init(options);
}
// 向Vue原型混入操作方法
initMixin(Vue);
...

export default Vue;
// init.js
import {initState} from "state.js"
export function initMixin(Vue){
    Vue.prototype._init = function(options){
        const vm = this;
        vm.$options = options;
        // 初始化状态
        initState(vm);
    }
}
// state.js 初始化状态
import {observe} from 'observe/index.js'

export function initState(vm){
    const opt = vm.$options;
    if(opt.data){
        // 初始化data
        initData(vm);
    }
}

function initData(vm){
    let data = vm.$options.data;
    // 判断data是否为函数
    data = typeof data === 'function' ? data.call(vm) : data;
    // 对统一后的data对象重新挂载在vm实例上
    vm._data = data
     // 数据侦测与劫持
    observe(data);
}

2、Object变化侦测

Object.defineProperty()缺点

  • 不能侦测新增与删除属性

2.1 数据劫持

// observe/index.js
// 对数据进行侦测/重写 返回可侦测对象
export function observe(data){
    // 非对象类型不进行劫持
    if(typeof data != 'object' || data == null) return;
    return new Observe(data);
}

// 数据侦测类
class Observe {
    constructor(data){
        this.walk(data);
    }
    // 对象数据劫持 - 相当于重写 性能瓶颈
    walk(){
        Object.keys.forEach(key => {
            defineReactive(data,key,data[key])
        })
    }
}

// 数据劫持公共方法
export const defineReactive(target,key,value){
    Object.defineProperty(target,key,{
        enumerable:true,    // 默认也为true
        configurable:true,    // 同上
        get(){
            return value;     // 闭包
        },
        set(newVal){ 
            if(newVal === value) return;
            value = newVal;
        }
    })
}
此时我们可以通过observe方法对传入的对象进行数据侦测,劫持数据的取值更改
但是数据是在_data上的,为了开发模式语法尽量简洁,这里需要数据代理

2.2 数据代理

// state.js 对initData进行补充
function initData(vm){
    //...other code
    for(let key in data){
        proxy(vm,'_data',key);
    }
}
function proxy(vm,target,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[target][key];
        },
        set(newVal){
            vm[target][key] = newVal;
        }
    })
}
const vm = new Vue({
    data(){
        return {
            name:"foo",
            age:11
        }
    }
})

当我们执行以上代码时可以在vm上读取到name属性,并且name与age都拥有gettersetter

2.3 深度侦测与新值侦测

当data中的值是嵌套的对象,以及对data属性设置对象值时,我们希望仍然对其进行侦测,
并且对于已经侦测的数据不再进行重写

// obseve/index.js
export function observe(data){
    ...
    // data已经存在了Observe的一个实例 说明已经被侦测过
    if(data.__ob__ instanceof Observe) return data.__ob__
    ...
}
class Observe {
    constructor(data){
        Object.defineProperty(data,"__ob__",{
            value:this, // 直接使用实例赋值 后面数组侦测需要该属性
            enumerable:true // 防止深度侦测时 递归爆栈
        })
        ...
    }
}
export const defineReactive = function(target,key,value){
    // 深度侦测
    observe(value);

    Object.defineProperty(target,key,{
        ...
        set(newVal){
            ...
            // 新值侦测
            observe(value);
            ...
        }
    })
}

3、Array变化侦测

上述observe流程图中的hasMethod判断是指数组调用的方法是否在被重写列表中
image.png
数组一般数据元素较多,如果逐个下标进行侦测,会浪费性能,因为相较于对下标的修改我们更常使用的是数组方法修改

注意并不是Object.defineProperty不能侦测,而是Vue在设计时抛弃了侦测下标这种方式
  • 数组中引用数据类型的元素依然使用observe进行侦测
  • 对能修改原数组的方法进行切面补充(原型链继承的方式)
// observe/index.js
import {newArrayProto} from "observe/array.js"

...
class Observe {
    constructor(data){
        ...
        //数组类型判断
        if(Array.isArray(data)){
            // 设置data的原型对象
            Object.setPrototypeOf(data,newArrayProto)
            this.observeArray(data)
        }else {
            
        }
    }
    // 数组劫持
    observeArray(data){
        // 元素为数组/对象进行递归劫持
        data.forEach(item => observe(item))
    }
}
...
// observe/array.js
let originArrayProto = Array.prototype
// 创建一个以originArrayProto为原型的对象
export let newArrayProto = Object.create(originArrayProto);
// 修改数组的7种方法
const methods = ["push","pop","unshift","shift","sort","splice","reverse"]

methods.forEach(method => {
    newArrayProto[method] = function(..args){
        // 调用原始方法
        const result = originArrayProto[method].apply(this,args);
        const ob = this.__ob__; // __ob__为上述添加的Observe实例
        // 数据劫持新数据
        let inserted;
        switch(method){
            case 'push':
            case 'unshift':
                inserted = args;
                breake;
            case 'splice':
                inserted = args.slice(2);// 第三个参数为新数据
                break;
        }
        // 新数据侦测
        if(inserted){
            ob.observeArray(inserted);
        }
        return result;
    }
})
通过原型链继承将中间层对象设置为原数据的原型对象,是一种面向切面编程的方式,只重写部分方法
__ob__是原数据的一个属性,值为Observe的实例,可以通过它劫持新增的元素数组(非常优雅 也有点恶心)

怼怼
73 声望6 粉丝