前言
Vue 源码一直是前端面试必问的题目,熟悉源码能让我们在面试过程中脱颖而出,熟悉源码也能让我们在开发过程中知道整个框架是如何运转的,遇到了问题,我们能第一时间知道是哪里出了问题。
今天开始,打算手写一下 Vue2 的源码,主要包括 Vue 的响应式原理、模板转化为 ast 语法树、虚拟 Dom转成真实 Dom、异步更新、Diff 算法、watch、computed 等等,生成一个可以使用的简易版 Vue2 框架 mini-Vue2。
今天第一篇文章就从实现 new Vue() 开始,看看 new Vue() 需要进行哪些操作。
搭建项目
生成 package.json
npm init -y
打包工具选择的是 rollup,原因是rollup比较轻量,打包体积小,适合做插件、小型框架和库的开发。
安装 rollup
npm install rollup --save-dev
还需要安装 babel
npm install rollup-plugin-babel @babel/core @babel/preset-env --save-dev
安装 rollup 配置插件
npm install rollup-plugin-livereload rollup-plugin-serve rollup-plugin-uglify --save-dev
devDependencies 内的包都要装上,简单说下一些包的作用:
- @babel/core:babel核心实现
- @babel/preset-env:es6转es5,使用这个包要基于 @babel/core
- rollup-plugin-babel:babel 插件
- rollup-plugin-livereload:热更新插件
- rollup-plugin-serve:服务器插件,用于开启本地服务器
- rollup-plugin-uglify:压缩代码
配置 rollup
在项目根目录创建rollup.config.js,这里先做一些基本的配置:
import babel from 'rollup-plugin-babel'
import { uglify } from 'rollup-plugin-uglify'
import serve from 'rollup-plugin-serve'
import livereload from 'rollup-plugin-livereload'
export default {
input: 'src/index.js', // 入口文件
output: {
format: 'umd',
file: 'dist/vue.js', // 打包后输出文件
name: 'Vue', // 打包后的内容会挂载到window,name就是挂载到window的名称
sourcemap: true // 代码调试 开发环境填true
},
plugins: [
babel({
exclude: 'node_modules/**' // 排除node_modules下的所有文件
}),
// 压缩代码
uglify(),
// 热更新 默认监听根文件夹
livereload(),
// 本地服务器
serve({
open: true, // 自动打开页面
port: 8000,
openPage: '/public/index.html', // 打开的页面
contentBase: ''
})
]
}
配置 babel
在项目根目录创建 .babelrc
{
"presets": [
"@babel/preset-env"
]
}
修改运行脚本命令
"scripts": {
"dev": "rollup -c -w"
},
-c表示执行配置文件,-w表示监测更新。
运行项目,执行 npm run dev
看到根目录已经生成了打包出来的 dist 文件夹以及文件下的 vue.js 和 vue.js.map 文件。
我们在 public/index.html 引入打包出来的 vue.js:
在 src/index.js 随便写两行 es6 代码:
再次运行 npm run dev:
dist/vue.js:
可以看到,打包之后的文件内容语法已经转成了 es5,全局对象挂载了 Vue。
index.html 打印 Vue 内容:
这样,我们项目框架就搭建完成。
数据初始化
创建 Vue 构造函数
在实际开发过程中我们使用 new Vue() 去实例化 vue 的实例,那我们就需要创建一个 Vue 的类或者构造函数。这里我选择构造函数的方式去创建:
let vm = new Vue({
data: {
name: 'ts',
age: 18
}
})
在 src/instance/index.js 里面写入我们的 Vue 构造函数,传入参数 options 为一个对象,里面的属性就是我们常见的 data、computed、methods 等等。
/**
* Vue 构造函数
* @param options 为传入的对象,如:{data:{},computed:{},methods:{}}
*/
function Vue(options) {
}
export default Vue
Vue 构造函数原型添加 _init 方法
接下来,就需要在构造函数 Vue 里面做一些初始化的操作,不同的初始化操作逻辑可以放在不同的 js 文件里。
src/instance/index.js:
import { initMixin } from './init'
/**
* Vue 构造函数
* @param options 为传入的对象,如:{data:{},computed:{},methods:{}}
*/
function Vue(options) {
this._init(options)
}
initMixin(Vue)
export default Vue
src/instance/init.js:
// 给Vue原型添加_init方法
export const initMixin = (Vue) => {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options; // 将options挂载在实例上,以$开头,和$set、$nextTick一样的命名规则
initState(vm) // 初始化状态
}
}
初始化 Vue 实例的时候会执行 initMixin() 和内部的 _init() 方法, initMixin 将 _init() 挂载到了 Vue 原型方法上,并将 options 放在了实例的 属性上。后续初始化操作,我们只需要对 options 进行处理。
接下来,我们来实现初始化状态 initState 方法。
实现 initState 和 initData
src/instance/state.js:
/**
* Vue 初始化状态,进行data computed watch props等属性的初始化
* @param vm 实例对象
*/
export const initState = function (vm) {
const options = vm.$options
if (options.data) {
initData(vm)
}
// ...还会有其他属性的初始化操作
}
function initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.apply(vm) : data // 判断data类型,可能是函数也可能是对象
console.log(data)
}
小结:
数据初始化主要是对传入的参数 options 进行处理,每个不同类型的函数操作都需要抽离单独的 js 文件进行管理,通过传递 vm 让每个函数在处理的时候都能拿到实例对象,进而拿到 $options。目前,我们只做 data 的初始化,拿到传入的 data 之后,接下来我们将对 data 进行数据劫持,实现数据响应式。
实现对象响应式原理
vue2 使用的是 0bject.defineProperty 进行数据劫持实现响应式。我们在 initData 函数拿到了 data 值,在这里我们可以开始实现响应式。
observe 方法和 Observe 类
src/instance/state.js:
import { observe } from './observe/index'
/**
* Vue 初始化状态,进行data computed watch props等属性的初始化
* @param vm 实例对象
*/
export const initState = function (vm) {
const options = vm.$options
if (options.data) {
initData(vm)
}
// ...还会有其他属性的初始化操作
}
function initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.apply(vm) : data // 判断data类型,可能是函数也可能是对象
vm._data = data
observe(data)
}
src/observe/index.js:
export function observe(data) {
if (typeof data !== 'object' && data !== 'null') return // data不是对象就不用劫持
return new Observe(data)
}
class Observe {
constructor(data) {
this.walking(data)
}
walking(data) {
Object.keys(data).forEach((key) => defineReactive(data, key, data[key])) // 遍历 data 对象属性,依次执行defineReactive方法进行数据劫持
}
}
这里调用 observe 方法,先对 data 数据类型进行判断,只对 object 类型进行数据劫持。然后对 data 对象进行遍历,依次去执行 defineReactive 方法。
Object.defineProperty
/**
* defineReactive 通过Object.defineProperty api 对数据进行数据劫持
* @param target 目标数据对象
* @param key 属性
* @param value 值
*/
export function defineReactive(target, key, value) {
Object.defineProperty(target, key, {
// 访问属性时候执行
get() {
return value
},
// 修改属性值时候执行
set(newValue) {
if (value === newValue) return // 新值和旧值相等就不用赋值
value = newValue
}
})
}
在 defineReactive 方法中,我们通过 Object.defineProperty 将 data 的所有属性通过 get 和 set 进行了数据劫持。
我们打印 index.html 中的 vm:
可以看到 _data 下的 age 和 name 都已经成功被劫持,但是我们访问是 vm._data.age,和我们正常访问属性 vm.age 不一样,我们需要做到访问 vm.age 的时候,实际访问的是 vm._data.age,这里可以通过代理的方式实现。
src/instance/state.js:
import { observe } from './observe/index'
/**
* Vue 初始化状态,进行data computed watch props等属性的初始化
* @param vm 实例对象
*/
export const initState = function (vm) {
const options = vm.$options
if (options.data) {
initData(vm)
}
// ...还会有其他属性的初始化操作
}
function initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.apply(vm) : data // 判断data类型,可能是函数也可能是对象
vm._data = data
observe(data)
for (const key in data) {
handleProxy(vm, '_data', key)
}
}
function handleProxy(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key]
},
set(newValue) {
vm[target][key] = newValue
}
})
}
通过函数 handleProxy, 我们对 data 上的属性进行代理,当我们访问 vm.name 的时候,实际访问的是 vm._data.name,这样我们就能调用 vm.属性 进行访问属性了。
深层次对象劫持
现在我们只要做了一层对象的属性劫持,如果属性下面又是对象,我们就需要用递归执行 observe 方法。
添加递归逻辑:
export function defineReactive(target, key, value) {
observe(value) // 深层次对象递归
Object.defineProperty(target, key, {
get() {
// 访问属性时候执行
return value
},
set(newValue) {
// 修改属性值时候执行
if (value === newValue) return // 新值和旧值相等就不用赋值
value = newValue
}
})
}
总结
这篇文章,主要是介绍了 new Vue 阶段做了哪些事情,总结一下就是以下几点:
- vue.prototype._init(option)
- initState(vm)
- initData(vm)
- observer(vm.data)
- new Observer(data)
- 调用 walk 方法,遍历 data 中的每个属性,监听数据的变化
- 执行 Object.defineProperty 监听数据读取和设置