vue 深入系列
手写Vue核心原理
一、使用Rollup搭建开发环境
- 什么是Rollup? Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js更专注于Javascript类库打包 (开发应用时使用Webpack,开发库时使用Rollup)
- 搭建环境
安装环境
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响应式原理
- 导出
Vue
构造函数
import { initMixin } from './init'
// Vue 的核心代码 只是Vue的一个申明
function Vue(options) {
// 进行Vue的初始化操作
this._init(options)
}
// 通过引入文件的方式,给Vue原型上添加方法
initMixin(Vue);
export default Vue;
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);
}
- 根据不同属性进行初始化数据操作
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() {}
- 初始化数据
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); // 响应式原理
}
- 递归劫持每个属性
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); // 用来观测数据的
}
- 数组的劫持
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]); // 监控数组的每一项
}
}
}
- 重写数组原型上的方法
// 重写数组的方法: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
}
})
- 增加__ob__属性
class Observer {
constructor(value) {
// value.__ob__ = this;不能通过这种方式,会内存溢出 // 我给每一个i监控的对象都新增一个__ob__属性
Object.defineProperty(value, '__ob__', {
enumerable: false,
configurable: false,
value: this
})
...
}
}
- 数据代理
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;
}
})
}