# Vue源码及其解析 **Repository Path**: chenjixink/VueStudy ## Basic Information - **Project Name**: Vue源码及其解析 - **Description**: Vue源码学习,还有笔记 - **Primary Language**: JavaScript - **License**: MulanPSL-1.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2020-05-12 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 环境搭建 ## 导包 1. rollup 打包工具 2. @babel/core 用babel核心模块 3. @babel/preset-env (babel将高级语法转成低级语法) 4. rollup-plugin-babel (桥梁) 5. rollup-plugin-serve (实现静态服务) 6. cross-env (设置环境变量) ## rollup.config.js ```javascript 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', //默认打开html的路径 port:3000, contentBase: '' }):null ] } ``` ## package.json ```javascript { "name": "vueStudy", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build:dev": "rollup -c", // 直接打包 "serve":"cross-env ENV=development rollup -c -w" // 设置环境变量 development 并且打包 -w 实时监听 }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.9.6", "@babel/preset-env": "^7.9.6", "cross-env": "^7.0.2", "rollup": "^2.9.1", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-serve": "^1.0.1" } } ``` # 核心代码部分 ## index.js(Vue实例) * 我们在这个js文件下创建 Vue 实例,给它原型添加各种各样的方法 ```javascript // vue 核心代码 只是Vue的一个声明 import {initMixin} from './init' function Vue(options){ // 进行vue初始化操作 this._init(options); //这个init方法在 initMixin 中 } // 通过引入文件的方式给 Vue原型添加方法 initMixin(Vue); export default Vue ``` ## init.js 该文件用于初始化vue实例,通过对index.js 传入的各种属性进行处理 ```javascript 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); // 分隔代码 } } ``` ## state.js 对传入的vue实例进行拆分,有什么属性就运行什么方法 ```javascript import {observe} from './observer/index.js' export function initState(vm){ const opts = vm.$options; // vue的数据来源 属性 方法 数据 计算属性 watch if(opts.props){ initProps(vm); } if(opts.methods){ initMethod(vm); } if(opts.data){ initData(vm); } if(opts.computed){ initComputed(vm); } if(opts.watch){ initWatch(vm); } } function initProps(){} function initMethod(){} function initData(vm){ // 数据初始化工作 let data = vm.$options.data; //用户传递的data data = vm._data = typeof data === 'function'?data.call(vm):data; //如果data是一个函数,就直接执行,并把this指向vue实例,否则就获取data对象 // 对象劫持,用户改变了数据 我希望可以得到通知 => 刷新页面 // MVVM 模式 数据变化驱动视图变化 // Object.defineProperty() 给属性添加get方法和set方法 observe(data); // 响应式原理 } function initComputed(){} function initWatch(){} ``` # 工具包util ```javascript /** * * @param {*} data 当前数据是不是对象 */ export function isObject(data) { return typeof data === 'object' && data !== null; } export function def(data, key, value) { Object.defineProperty(data, key, { enumerable: false, configurable: false, value }) } ``` # 响应式原理 我们在创建的 observer 文件夹用于存放所有响应式的代码 主要原理就是重写 Object.defineProperty() 给属性添加get方法和set方法 由于 data 中的数据 都是用 Object.defineProperty (es5的语法) 重新定义,所以 vue2 无法兼容 IE8 及以下版本 ## 主要步骤(**数据劫持**) 1. 先判断是不是对象(工具类中有个isObject) 2. 通过 this.walk 获取所有的value 3. 编写defineReactive方法,使用 defineProperty 定义响应式数据(此处运用了闭包的知识),由于用户有可能传入一个对象,所以我们需要在该方法中使用 **observe** 方法递归 ```javascript // data 中的数据 都是用 Object.defineProperty 重新定义 es5 // 所以 vue2 无法兼容 IE8 及以下版本 import { isObject,def } from '../util/index' import {arrayMethods} from './array.js' class Observer { constructor(value) { // vue 如果数据的层次过多,需要递归去解析对象中的属性,一次增加set和get方法 // proxy // value.__ob__ = this; // 我给每个监控过的对象都增加一个 __ob__ 属性 def(value,'__ob__',this); if (Array.isArray(value)){ // 如果是数组的话并不会对索引进行观测,因为会导致性能问题 // 因为前段开发中很少去操作索引 咱用的更多是 push shift unshitf value.__proto__ = arrayMethods; // 如果数组里放的是对象,我再监控 this.observerArray(value); } else { this.walk(value) } } observerArray(value) { for (let i = 0; i < value.length; i++) { observe(value[i]) } } walk(data) { let keys = Object.keys(data); // [name,age,address] keys.forEach((key) => { defineReactive(data, key, data[key]) //循环key依次添加响应式属性 }) } } function defineReactive(data, key, value) { observe(value) // 递归,对data中的对象进行响应式操作,递归实现深度检测 Object.defineProperty(data, key, { get() { // 获取值的时候做一些操作 return value; }, set(newValue) { // 也可以做一些操作 console.log("更新数据") if (newValue == value) return; observe(newValue); //继续劫持用户设置的值,因为用户设置的值有可能是一个对象 value = newValue; } }) } export function observe(data) { let isObj = isObject(data); if (!isObj) { return; } return new Observer(data); } ``` ## 数组的劫持 4. 由于我们data数据有可能是一个数组,为了不让对其索引进行监控(性能的问题),我们需要给数组添加一个属性,在原始属性之前添加一些操作(原型链的知识) 5. 这里我们通过工具类的 **def** 方法,给每个监控过的对象添加一个 __ob__属性,该属性有 observerArray() 方法,对每个新的数据变成响应式数据 ```javascript // 我要重写数组的方法 7个 push shift unshitf pop reverse sort splice 会导致数组本身发生变化 // 除了 slice() let oldArrayMethods = Array.prototype; // value.__proto__ = arrayMethods 原型链查找的问题,会向上查找,先查找我重写的,没有重写的会继续向上查找 // arrayMethods.__proto__ = oldArrayMethods export const arrayMethods = Object.create(oldArrayMethods); const methods = [ 'push', 'shift', 'unshift', 'pop', 'sort', 'splice', 'reverse' ] methods.forEach(method=>{ arrayMethods[method] = function(...args){ console.log("用户调用了push方法"); // AOP 切片编程 const result = oldArrayMethods[method].apply(this,args); //调用原生的数组方法 // push unshift 添加的元素可能还是一个对象 let inserted; // 用户插入的元素 let ob = this.__ob__; switch(method){ case 'push': case 'unshift': inserted = args; break; case 'splice': // 3 个 新增的属性 splice 有删除 新增的功能 inserted = args.slice(2) default: break; } if(inserted) ob.observerArray(inserted); // 将新增属性继续观测 return result; } }) ```