Skip to content

二、响应式原理

1、定义并导出构造函数

js
// src\index.js

import { initMixin } from "./init";

// Vue构造函数
function Vue(options) {
    // 入口方法,做初始化操作
    this._init(options)
}

// 插件思想:对原型进行扩展
initMixin(Vue)

export default Vue

2、定义初始化混合方法

js
// src\init.js

import { initState } from "./state"

// 定义init方法,进行扩展Vue的原型
export function initMixin(Vue) {
    // 初始化方法 
    Vue.prototype._init = function(options) {
        // 拿到当前实例
        const vm = this
		// 拿到配置参数挂载到$options
        vm.$options = options

        // 初始化状态:将数据做一个初始化的劫持,数据改变就去更新视图
        initState(vm)

        // 其他初始化方法
        // initEvents
    }
}

3、初始化状态

js
// src\state.js

/**
 * 初始化状态
 * 顺序:props > methods > data > computed > watch
 */
export function initState(vm) {
    const opts = vm.$options
	// 按照顺序依次拆分初始化
    if (opts.props) {
        initProps(vm)
    }
    if (opts.methods) {
        initMethods(vm)
    }
    if (opts.data) {
        initData(vm)
    }
    if (opts.computed) {
        initComputed(vm)
    }
    if (opts.watch) {
        initWatch(vm)
    }
}

function initProps(vm) {}

function initMethods(vm) {}

function initData(vm) {}

function initComputed(vm) {}

function initWatch(vm) {}

4、初始化数据

js
// src\state.js

import { observe } from "./observer/index"

// 初始化数据方法
function initData(vm) {
    let data = vm.$options.data;
	// 拿到data属性,如果是函数直接执行,其余放行
    vm._data = data = typeof data === 'function' ? data.call(vm) : data;

    // 数据劫持方案
    // 对象Object.defineProperty
    // 数组 单独处理:拦截可以改变数组的方法进行操作
	// 观测数据
    observe(data);
}

5、对象劫持-递归属性

  • data本身为对象,需要观测
  • data对象里面还有对象,需要递归观测
  • 给data设值的新值也是对象,需要进行递归观测
js
// src\observer\index.js

/**
 * 数据观测类
 * 使用defineProperty 重新定义属性
 */
class Observer {
    constructor(value) {
        // 判断一个对象是否被观测过,看他有没有__ob__这个属性
        Object.defineProperty(value, '__ob__', {
            enumerable: false, // 不能被枚举,不能被循环出来
            configurable: false,
            value: this // 注入当前的实例对象
        })
		// 对象处理
		this.walk(value); 
    }

    walk(data) {
        // 遍历对象,进行循环观测
        let keys = Object.keys(data);
        keys.forEach(key => {
            defineReactive(data, key, data[key]); // 源码对应 > Vue.util.defineReactive
        })
    }
}

// ES5的双向数据绑定类
// vue2慢的核心原因就是这个方法
// vue2 应用了defineProperty需要一加载的时候 就进行递归操作,所以好性能,如果层次过深也会浪费性能
// 1.性能优化的原则:
// 1) 不要把所有的数据都放在data中,因为所有的数据都会增加get和set
// 2) 不要写数据的时候 层次过深, 尽量扁平化数据 
// 3) 不要频繁获取数据
// 4) 如果数据不需要响应式 可以使用Object.freeze 冻结属性 
function defineReactive(data, key, value) {
    // 如果值是对象进行递归观测
    observe(value);

	// 定义双向绑定,进行get和set的观测 
    Object.defineProperty(data, key, {
        get() {
            console.log('取值');
            return value
        },
        set(newValue) {
            console.log('设值');
			// 值没变化就跳过
            if (newValue == value) return;
            // 如果用户设值的值是对象,需要再次进行递归观测
            observe(newValue);
			// 更新值
            value = newValue;
        }
    })
}

/**
 * 数据观测
 */
export function observe(data) {
    // 对象数据校验:不是对象 或 null 就返回
    if (typeof data !== 'object' || data === null) {
        return data
    }
    // 如果数据被观测过,直接返回,防止重复观测
    if (data.__ob__) {
        return data
    }

    // 数据观测
    new Observer(data);
}

6、数组劫持-重写原型

  • 如果data是数组,需要重写能改变数组的七个方法
  • 如果data数组里面每个item都是对象,那么需要循环遍历,观测每一项item数据
  • 如果给data数组新增的数据也是对象,那么新增的对象也需要进行观测
js
// src\observer\index.js

import { arrayMethods } from "./array";

// 1、改写观测类
class Observer {
    constructor(value) {
       if (Array.isArray(value)) {
            // 数组处理:函数劫持、切片编程思想
			// 重写push shift pop unshift splice sort reverse
            value.__proto__ = arrayMethods;
            // 观测数组中的对象类型
            this.observeArray(value);
        } else {
            // 对象处理
            this.walk(value);
        }
    }

	// 数组观测方法
    observeArray(value) {
        // 遍历数组的每一项进行观测
        value.forEach(item => {
            observe(item);
        })
    }
}
js
// src\observer\array.js

// 拿到数组原型上的方法
let oldArrayProtoMethods = Array.prototype

// 原型继承 arrayMethods.__proto__ = oldArrayProtoMethods
export let arrayMethods = Object.create(oldArrayProtoMethods)

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

// 数组方法重写
methods.forEach(method => {
    arrayMethods[method] = function(...args) {
        console.log('数组调用');
        // this为observer里面的value
        const result = oldArrayProtoMethods[method].apply(this, args);

        let inserted;
        let ob = this.__ob__;

        switch (method) {
            case 'push':
            case 'unshift':
                // 如果追加的内容也是对象,需要再次进行对象劫持
                inserted = args;
                break;
            case 'splice':
                // 截取参数下标为2到末尾:arr.splice(0, 1, {a: 1})
                inserted = args.slice(2);
            default:
                break;
        }

        // 如果给数组新增的值是对象要继续进行观测
        if (inserted) ob.observeArray(inserted)

        return result
    }
})

7、数据代理

  • 将取值全部代理到vm上面 vm.message = vm._data.message
js
// src\state.js

// 代理方法
function proxy(vm, data, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[data][key]; // vm_data.a
        },
        set(newValue) { // vm.a = 100
            vm[data][key] = newValue; // vm._data.a = 100
        }
    })
}

// 初始化数据
function initData(vm) {
    let data = vm.$options.data;
    vm._data = data = typeof data === 'function' ? data.call(vm) : data;
    // 用代理,从vm取属性,代理到vm_data上
    for (let key in data) {
        proxy(vm, '_data', key)
    }
	// 观测数据
    observe(data);
}

Released under the MIT License.