近十万字详解Vue实现(1):Vue2初始化、对象属性劫持、数组方法的劫持

vue2options API的,在vue3中 保留了

options api 无法 tree-shaking

vue2是一个构造函数不是类(class),通过原型的方式vue实现功能:vue.prototype = xxx

1、vue初始化

当用户 new Vue的时候 通过扩展原型的方法 调用_init方法进行vue的初始化

// vue.js
import {initMixin} from './init';

function Vue(options) {
    this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
复制代码

2、将用户输入的选项放到 vm.$options 代表 用户传入的所有属性

// init.js
import {initState} from './state';
export function initMixin(Vue){
    // 后续组件开发的时候 Vue.extend 可以创建一个子组件。子组件可以继承Vue,子组件可以调用_init方法
    Vue.prototype._init = function (options) {
        const vm  = this;
        // 把用户的选项放到vm上,这样在其他方法中可以获取到options
        vm.$options = options
        // 初始化状态
        initState(vm)
        
        // 如果有 el要将数据挂载到页面上
        if(vm.$options.el){
            console.log('页面要挂载');
        }
    }
}
复制代码

3、初始化状态initState(vm) (包含data、计算属性、方法等)

export function initState(vm){
    const opts = vm.$options;
    if(opts.props){
        initProps(vm);
    }
    if(opts.methods){
        initMethod(vm);
    }
    if(opts.data){
        // 初始化data
        initData(vm);
    }
    if(opts.computed){
        initComputed(vm);
    }
    if(opts.watch){
        initWatch(vm);
    }
}
function initProps(){}
function initMethod(){}
function initData(){}
function initComputed(){}
function initWatch(){}s
复制代码

3.1、数据的初始化initData,观测的是用户设置的data,和vm没关系

data有两种情况 :要么是函数要么是对象,对data类型进行判断 如果是 函数就获取函数返回值

获取函数执行结果的时候 用 data.call(vm) 保证 data函数中的 this 指向vue实例 其实和返回值无关

只有根实例中的data才能是对象

将data定义在vm._data属性上将data和_data关联起来,方便在外部能用vm._data获取到被观测过的数据,不写成 vm.data 是为了区分用户获取场景

// state.js initData 函数
import {observe} from './observer/index.js'
function initData(vm){
    let data = vm.$options.data;
    // 将data都定义在vm._data属性上
    // 不写成 vm.data 是为了区分用户获取场景
    data = vm._data = typeof data === 'function' ? data.call(vm) : data;
    // 需要将data变成响应式的 使用Object.defineProperty 重写data中的所有属性
    observe(data);
}
复制代码

使用defineProperty将_data中的所有属性都代理到vm上,方便直接获取,’vm.nam === vm._data.name’

取值的时候做代理,不是直接把属性添加到vm上,而且直接赋值会出现命名冲突问题

// state.js proxy 函数
function proxy(vm,key,source){ //取值的时候做代理,不是直接把属性添加到vm上,而且直接赋值会出现命名冲突问题
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key]
        },
        set(newValue){
            vm[source][key] = newValue  //vm.nam === vm._data.name
        }
    })
}
function initData(vm){
    let data = vm.$options.data;
    // 将data都定义在vm._data属性上
    // 不写成 vm.data 是为了区分用户获取场景
    data = vm._data = typeof data === 'function' ? data.call(vm) : data;
    // 循环代理所有属性
    for(let key in data){
        proxy(vm,key,'_data')
    }
    
    // 需要将data变成响应式的 使用Object.defineProperty 重写data中的所有属性
    observe(data);
    
    
}
复制代码

3.2、observe观测数据将data变成响应式的(对象类型拦截)

props 初始化在data之前

只对对象类型进行观测 非对象类型无法观测直接return

通过Observe类来实现对数据的观测,类方便扩展,类会产生实例以作为唯一标识

将对象中的所有key 重新用defineProperty定义为响应式的

如果一个数据已经被观测过,就不要在进行观测,用类来实现,观测过一个数据就添加一个标识,说明观测过了,在观测的时候先检测是否观测过,如果观测过了就跳过

// 观测数据
export function observe(data){
    // console.log(data,'observe')
    if(typeof data !== 'object' || data == null){
        return;
    }
    if(data.__ob__){  //数据上有这个属性表示已经观测过了 防止 循环引用
        return;
    }
    // 通过类来实现对数据的观测,类方便扩展,类会产生实例以作为唯一标识
    return new Observe(data)
}
复制代码

3.2.1、递归观测数据

vue2 应用了defineProperty需要一加载的时候就进行递归操作,所以耗费性能,如果层次过深也会浪费性能

如果给vm设置了一个新的对象类型数据,应该在Object.definePropertyset中再次劫持变成响应式的

Object.defineProperty监控的是对象的属性,proxy监控的是对象

新设置的可能也是对象 也要递归观测数据

性能优化原则:

  • 1)不要把所有的数据都放在data中,因为所有的数据都会增加get和set
  • 2)数据尽量扁平化,不要嵌套过深,递归会耗费性能
  • 3)不要频繁的获取被观测的数据,数据的get拦截器中会有很多操作
  • 4)如果数据不需要响应式,用Object.freeze冻结属性,原理是将属性的configurable设为false
// observe.js
class Observer { // 观测值
    constructor(value){
        this.walk(value);
    }
    walk(data){ // 使用 defineProperty 对对象上的所有属性依次进行观测
        let keys = Object.keys(data);
        for(let i = 0; i < keys.length; i++){
            let key = keys[i];
            let value = data[key];
            defineReactive(data,key,value);
        }
    }
}
function defineReactive(data,key,value){// vue2慢的他原因主要就是这个方法的原因
    // 形成了闭包 因为return了上个作用域的value 
    observe(value);  // value可能也是对象 递归观测数据
    Object.defineProperty(data,key,{ 
        get(){
            return value
        },
        set(newValue){
            if(newValue == value) return;
            observe(newValue); // 新设置的可能也是对象 递归观测数据
            value = newValue
        }
    })
}
export function observe(data) {
    if(typeof data !== 'object' || data == null){
        return;
    }
    if(data.__ob__){  //数据上有这个属性表示已经观测过了 防止 循环引用
        return;
    }
    // 通过类来实现对数据的观测;类:方便扩展,类会产生实例以作为唯一标识
    return new Observer(data);
}
复制代码

3.3、数组类型拦截:通过改写数组原型,监听(改写)数组自身的方法来实现push、pop、shift、unshift、soplice、reverse、sort 七种方法

按照3.2的逻辑也用循环defineProperty的话,虽然可以实现修改索引触发更新,但是,给数组的每一项都进行了数据劫持,这样是有性能问题的,而且数组的属性可能是各种各样的,比如: 函数、length

因此源码中没有采用这个方法,所以 修改数组的索引或者length也不会触发视图更新(直接arr[0]=xx不能被监控)

image.png

class Observe {
    constructor(value){
        //* 数组如果也用循环defineProperty的话 每一项都会设置get和set 性能比较差
        if(isArray(value)){
            //更改数组原型方法 
            value.__proto__ = arrayMethods // 重写数组的方法
        } else {
            this.walk(value)  //核心就是循环对象
        }
    }
    walk(data){
        Object.keys(data).forEach(key => { //使用 defineProperty从新定义
            defineReactive(data,key,data[key])

        })
    }
}
复制代码

3.3.1、在监测数据的时候 对数组和对象分类处理,不能直接改写数组的方法,只有被vue控制的数组才需要改写

让arrayMethods 继承于Array.prototype,arrayMethods找不到的方法可以通过原型链(__proto__)去Array.prototype找

let oldArrayPrototype = Array.prototype;
let arrayMethods = Object.create(oldArrayPrototype); // 让arrayMethods 继承于Array.prototype,arrayMethods找不到去Array.prototype找
复制代码

不能直接改变原型方法

// 不能用 这是直接改变不是 继承 
arrayMethods = oldArrayPrototype

复制代码

改写方法时使用 AOP切片编程,做一些操作以后 还是要调用数组原来的方法

比如新增,我们需要只需要在对应方法呗调用的时候去,将新增的数据放到一个数组中(也就是arguements),然后再调用observeArray就好了

let methods = [
    `push`,
    `pop`,
    `shift`,
    `unshift`,
    `reverse`,
    `sort`
]
methods.forEach(method => { //AOP切片编程
    arrayMethods[method] = function(...args){ 
        //重写数组方法
        //todu...
        
        let result = oldArrayPrototypeMethods[method].call(this,...args);  //做一些操作以后 还是要调用数组原来的方法
        return result
    }
})
复制代码

3.3.2、数组中原有的对象或者数组,通过observeArray循环调用3.2的observe方法

不用判断每一项的数据类型,因为上边的observe函数中已经做了判断

如果数组中的属性是引用数据类型 那么是响应式的

// state.js 中 class Observe的constructor中
class Observer { // 观测值
    constructor(value){
        if(Array.isArray(value)){
            value.__proto__ = arrayMethods; // 重写数组原型方法
            this.observeArray(value); // 递归遍历数组,对数组内部的对象进行观测
        }else{
            this.walk(value);
        }
    }
    observeArray(value){
        for(let i = 0 ; i < value.length ;i ++){
            observe(value[i]);
        }
    }
    // 或者 
    observeArray(value){
        value.forEach(item => observe(item))
    }
}
复制代码
vm.arr[0].name = “aaa” 可以触发更新,走的是对象的逻辑,vm.arr[0] = “bb” 不行
vue2 无法劫持到不存在的属性,新增不存在的属性不会更新视图
vm.message = {name: 'aaa'}
vm.message.age = 18  // 不会触发更新
复制代码

3.3.3、 数组新增的对象需要被监测到

新增有两种:一种是push、unshift这种参数只有增加项的方法,一种是splice这种参数有好项的增加方式

因此需要获取到加入的数据,并通过observeArray拦截

对数据进行拦截的时候是先赋值再通过observeArray拦截

增加__ob__属性 方便别的地方获取

value.__ob__ = this; 这种写法会导致死循环 因为__ob__会被重复监听 所以要用Objecy.defineproperty改成不可枚举的

// index.js
class Observe {
    constructor(value){
    
        //给对象和数组 添加一个自定义属性 指向 察观类的实例 就可以通过__ob__获取到私有和公有方法
        //value.__ob__ = this;
        Object.defineProperty(value,'__ob__',{
            enumerable:false,  // 表示属性不能被枚举
            configurable:false,
            value:this
        });


        //* 数组如果也用循环defineProperty的话 每一项都会设置get和set 性能比较差
        if(isArray(value)){
            //更改数组原型方法 
            value.__proto__ = arrayMethods // 重写数组的方法
            this.observeArray(value) // 处理数组中的对象
        } else {
            this.walk(value)  //核心就是循环对象
        }
    }
    observeArray(value){
    ...
    }
    walk(data){
    ...
    }
}
复制代码

监听push、unshift、splice方法,如果有新增需要用observeArray 监听

// array.js
methods.forEach(method => { //AOP切片编程
    arrayMethods[method] = function(...args){ //重写数组方法
        //todu...
        console.log('数组改变了');
        
        
        // 数组新增的属性  要看一下是不是对象 是对象继续进行劫持
        let inserted = null
        switch(method){
            case `push`:  // 修改 删除  添加  arr.splice(位置,个数,添加内容)
            inserted = args.slice(2)
            case `pop`:
            case `unshift`:
                inserted = args  //调用 push unshift 传递的参数 就是 新增的逻辑
                break;
        }

        // 根据inserted是否有内容 来确定是否需要二次劫持
        // 如果有内容 需要用observeArray 监听
        // 通过 this.__ob__ 获取到 察观类的实例 然后再获取到公有方法 observeArray
        // this是当前调用方法的数据(对象和数组)
        let ob = this.__ob__
        if(inserted) ob.observeArray(inserted)

        let result = oldArrayPrototype[method].call(this,...args);  //做一些操作以后 还是要调用数组原来的方法
        return result
    }
})
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享