vue 深入系列

手写Vue核心原理

一、使用Rollup搭建开发环境

  1. 什么是Rollup? Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js更专注于Javascript类库打包 (开发应用时使用Webpack,开发库时使用Rollup)
  2. 搭建环境

安装环境

npm install rollup rollup-plugin-babel rollup-plugin-serve  @babel/preset-env @babel/code cross-env -D

新建rollup.config.js

import babel from 'rollup-plugin-babel';
import serve from 'rollup-plugin-serve';

export default {
    input: './src/index.js', // 以那个文件夹作为打包入口
    output: {
        file: 'dist/umd/vue.js', // 出口路径
        name: 'Vue', // 指定打包后全局变量的名字
        format: 'umd', // 统一模块规范
        sourcemap: true, // es6 -> es5  开启源码调试,可以找到源码报错的地方
    },
    plugins: [
        babel({
            exclude: "node_modules/**"
        }),
        process.env.ENV === 'development' ? serve({
            open: true, // 自动打开网页
            openPage: '/public/index.html', // 默认打开路径
            port: 3000,
            contentBase: ''
        }) : null
    ]
}

新建.babelrc文件

{
    "presets": [
        "@babel/preset-env"
    ]
}

配置执行脚本

"scripts": {
    "build:dev": "rollup -c",
    "server": "cross-env ENV=development rollup -c -w"
}

二、Vue响应式原理

  1. 导出 Vue 构造函数
import { initMixin } from './init'
// Vue 的核心代码 只是Vue的一个申明
function Vue(options) {
    // 进行Vue的初始化操作
    this._init(options)
}

// 通过引入文件的方式,给Vue原型上添加方法
initMixin(Vue);

export default Vue;
  1. init 方法中初始化 Vue 状态
import {initState} from './state'
// 在原型上添加一个init方法
export function initMixin(Vue) {
    // 初始化流程
    Vue.prototype._init = function(options) {
        // 数据劫持
        const vm = this; // vue中使用this.$options 指代的就是用户传递的属性
        vm.$options = options;
        // 初始化状态 
        initState(vm);
}
  1. 根据不同属性进行初始化数据操作
export function initState(vm) {
    const opts = vm.$options;
    // vue的数据来源:属性、方法、数据、计算属性、watch
    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() {}
function initMethods() {}
function initData(vm) {}
function initComputed() {}
function initWatch() {}
  1. 初始化数据
import {observe} from './observe.js'

function initData(vm) {
    // 初始化数据工作
    let data = vm.$options.data; // 用户传递的data
    // data有可能是对象有可能是函数  用户可以通过_data获取数据
    data = vm._data = typeof data === 'function' ? data.call(this) : data
    // 对数据进行劫持
    // Object.defineProperty() 给属性添加get和set
    observe(data); // 响应式原理
}
  1. 递归劫持每个属性
import {isObject} from './util/index'
class Observer {
    constructor(value) {
        // vue 如果数据层数过多,需要递的去解析对象中的属性,依次增加set和get方法
        this.walk(value);
    }
    walk(data) {
        let keys = Object.keys(data);
        keys.forEach(key => {
            defineReactive(data, key, data[key]);
        })
    }
}

function defineReactive(data,key,value) {
    observe(value); // 多层对象,深度递归
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            return value;
        },
        set(newVal) {
            if(newVal === value) return;
            observe(newVal); // 用户有可能设置的值是对象也需要监控
            value = newVal;
        }
    })
}

export function observe(data) {
    if(!isObject(data)) return;

    new Observer(data); // 用来观测数据的
}
  1. 数组的劫持
import{arrayMethods} from './array'
class Observer {
    constructor(value) {
        if(Array.isArray(value)) {
            // 如果是数据的话并不会对索引进行观测,因为会导致性能问题
            // 如果数组里放的是对象我再监控,不是对象就不监控了
            this.observerArray(value);
            // 注意:数组的劫持:1、劫持数组中的每一项,对数组中的7个方法进行重写
        } else {
            this.walk(value);
        }
    }

    observerArray(value) {
        for(let i=0; i<value.length; i++) {
            observe(value[i]); // 监控数组的每一项
        }
    }
}
  1. 重写数组原型上的方法
// 重写数组的方法:push pop shift unshift reverse sort splice 这些方法都可以改变数组的本身

const oldArrayMethods = Array.prototype;
export const arrayMethods = Object.create(oldArrayMethods);

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

methods.forEach(method => {
    arrayMethods[method] = function(...args) {
        console.log(`用户调用的方法:${method}`)
        const result = oldArrayMethods[method].apply(this, args); // 调用原生的方法 AOP
        const ob = this.__ob__;
        // 需要判断一下当前用户通过数组添加的值是否是对象
        let inserted; // 当前用户插入的元素
        switch (key) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice': // 新增 修改 删除的功能(新增的内容在第三个)
                inserted = args.slice(2)
            default:
                break;
        }
        if(inserted) ob.observerArray(inserted);
        return result
    }
})
  1. 增加__ob__属性
class Observer {
    constructor(value) {
        // value.__ob__ = this;不能通过这种方式,会内存溢出 // 我给每一个i监控的对象都新增一个__ob__属性
        Object.defineProperty(value, '__ob__', {
            enumerable: false,
            configurable: false,
            value: this
        })
        ...
    }
}
  1. 数据代理
function initData(vm) {
    // 初始化数据工作
    let data = vm.$options.data; // 用户传递的data
    // data有可能是对象有可能是函数  用户可以通过_data获取数据
    data = vm._data = typeof data === 'function' ? data.call(this) : data
    for(let key in data) {
        proxy(vm, '_data', key) // 将_data上的属性全部代理给vm实例
    }
    // 对数据进行劫持
    // Object.defineProperty() 给属性添加get和set
    observe(data); // 响应式原理
}

// 数据代理
function proxy(vm, source, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[source][key];
        },
        set(newValue) {
            vm[source][key] = newValue;
        }
    })
}