01.vue2源码之响应式原理

103 阅读3分钟

new Vue发生了什么

new Vue 背后发生了哪些事情。我们都知道,new 关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的

function Vue(options){
    // options 为用户传入的选项
    this._init(options); // 初始化操作, 组件
}

可以看到 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法

export function initMixin(Vue) { // 表示在vue的基础上做一次混合操作
    Vue.prototype._init = function(options) {
        // el,data
        const vm = this; // var that = this;
       
        vm.$options = options; // 后面会对options进行扩展操作

      	initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm) // resolve injections before data/props
       // 对数据进行初始化 watch computed props data ...
        initState(vm)// vm.$options.data  数据劫持
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
        if(vm.$options.el){
            // 将数据挂载到这个模板上
            vm.$mount(vm.$options.el);
        }
    }
    Vue.prototype.$mount = function (el) {
        const vm = this;
        const options = vm.$options
        el = document.querySelector(el);
        vm.$el = el;
        // 把模板转化成 对应的渲染函数 =》 虚拟dom概念 vnode =》 diff算法 更新虚拟dom =》 产生真实节点,更新
        if(!options.render){ // 没有render用template,目前没render
            let template = options.template;
            if(!template && el){ // 用户也没有传递template 就取el的内容作为模板
                template = el.outerHTML;
                let render = compileToFunction(template);
                options.render = render;
            }
        }
        // options.render 就是渲染函数
        // 调用render方法 渲染成真实dom 替换掉页面的内容

        mountComponent(vm,el); // 组件的挂载流程
    }
}

总结:Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等

initState初始化数据劫持

数组和对象类型当值变化时如何劫持到。对象内部通过 defineReactive 方法,使用

Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。 多层对象是通过递归来实现劫持

状态初始化

export function initState(vm) { // 状态的初始化
    const opts = vm.$options;
    if (opts.data) {
        initData(vm);
    }
    //先不管computed和watch中的数据,先只劫持data中的数据
    // if(opts.computed){
    //     initComputed();
    // }
    // if(opts.watch){
    //     initWatch();
    // }
}
//将_data中的数据,挂载到vm上面
function proxy(vm,source,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key];
        },
        set(newValue){
            vm[source][key] = newValue
        }
    })
}

function initData(vm) { //
    let data = vm.$options.data; // vm.$el  vue 内部会对属性检测如果是以$开头 不会进行代理
    // vue2中会将data中的所有数据 进行数据劫持 Object.defineProperty

    // 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
    data = vm._data = isFunction(data) ? 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);
}

监听数据变化observe方法实现

  • 如果数据是对象 会将对象不停的递归 进行劫持
  • 如果是数组,会劫持数据的方法,并对数据中不是基本数据类型的进行检测
// 检测数据变化 类有类型 , 对象无类型
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]);
        })
    }
}

// vue2 会对对象进行遍历 将每个属性 用defineProperty 重新定义 性能差
function defineReactive(data,key,value){ // value有可能是对象
    observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newV){ 
            // todo... 更新视图
            observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
            value = newV;
        }
    })
}

//调用监听方法
export function observe(data) {
    // 如果是对象才观测
    if (!isObject(data)) {
        return;
    }
    if(data.__ob__){
        return;
    }
    // 默认最外层的data必须是一个对象
    return new Observer(data)
}

数组的方法监听

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)
        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)

        // arr.push(1,2)
        // arr.splice(0,1,xxxx)
    }
})