Vue全家桶的系统学习,其中包括Vue源码分析、Vue-Router的使用和原理、Vuex的用法和原理、Vue-ssr、如何进行单元测试 和 一些常见的Vue面试题。
参考官网 vue.js官网
Vue全家桶的使用,已经是一个老生常谈的话题了。
具体的使用可以去参考Vue的官网,上面有很详细的用法介绍。
这篇文章中,我们会详细介绍一下Vue的响应式原理。
准备工作
我们先要进行前期的准备工作。
首先我们要先创建一个默认的 package.json
文件。
1 | npm init -y |
配置文件创建好后,我们就需要使用 rollup 来进行代码编译了。
Rollup环境配置
因为 webpack 太大了,不方便做测试,所以我们会使用 rollup 作为打包工具。
rollup 简单的来说,就是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。rollup.js 更专注于Javascript类库打包 (开发应用时使用 Webpack,开发库时使用 Rollup)。
随后我们来安装 rollup
及其相应的插件。
1 | npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -d |
- rollup:我们需要用到的模块打包工具。
- rollup-plugin-babel:babel 与
rollup
之间互通的插件,用来对ES6代码进行转义与编译。 - @babel/preset-env / @babel/core:babel 需要用到的关联插件。
- rollup-plugin-serve:可以用来启动本地服务。
- cross-env:运行跨平台设置和使用环境变量的脚本。
随后我们来创建一个 rollup.config.js
文件作为它的配置文件。
1 | import babel from "rollup-plugin-babel"; |
配置 .babelrc
文件用来处理babel。
1 | { |
在 package.json
中进行如下配置。
1 | "scripts": { |
最终项目结构如下。
1 | |-- vue-test |
这样我们的配置文件就创建完毕了。
初始化流程
使用过Vue后都知道,Vue在作为插件库使用的时候,都是以 option Api (选项进行配置)的形式进行创建的。
1 | var vm = new Vue({ |
所以我们第一步要实现的,就是将 Vue作为构造函数,并导出。
导出构造函数
先来看一下完整的导出代码。
1 | import {initMixin} from './init'; |
(注:这里我们不采用ES6类的写法,因为它会将类或函数作为一个整体进行编写。我们希望它可以分散到不同的文件中,所以采用ES5构造函数的方式进行定义,这样在结构上看起来会更清晰。)
将创建的构造函数导出,他会接收一个参数options。
options
其实就是我们传入的属性,包括el
、data
、method
等。1
2
3
4function Vue(options) {
console.log(options); // 实例化时传入的"属性"
}
export default Vue;新增 _init方法。
options
接收到了,我们就需要通过传入的参数对 Vue进行初始化。1
2
3
4
5
6
7function Vue(options) {
this._init(options);
}
Vue.prototype._init = function(){
// ...
}
export default Vue;我们再对其进行 解耦,可以得到一个入口方法,可以使所有文件进行初始化操作。
1
2
3
4
5
6
7import {initMixin} from './init';
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
这个文件的最终目的是向 Vue原型上扩展方法。
很明显,我们目前就需要对 Vue进行初始化。
初始化Vue状态
上一步中,我们创建了 Vue类,并传入了 options参数。
这一步我们就要将需要的内容封装成插件。
1 | import {initState} from './state'; |
options
参数被传入了 vm.$options
中,这样我们就可以使用 $options
来进行配置。
initState
就是对状态进行初始化,也就是对数据进行一个初始化的劫持 (当我改变数据时,视图也会更新)。
根据属性进行初始化操作
Vue并不完全属于MVVM,它只是参考的MVVM。其包含的 $ref
就可以说明这个问题,它还是可以操作DOM的。
现在我们就需要对不同的属性进行不同的数据劫持。
1 | export function initState(vm){ |
根据不同的传入参数,来进行不同的处理。
下面我们就可以对拆分出来的不同属性,进行不同的初始化处理了。
数据(data)初始化
扩展 initData
方法之前,我们需要先知道一个概念。什么是数据劫持?
数据劫持 就是通过 Object.defineProperty
来重写对象的 getter
和 setter
。数据更新时视图会发生改变,而视图改变时数据也会跟着更新,从而达到一个 视图和数据 互相影响的效果。
这种设计模式我们又称呼它为 观察者模式。
1 | import {observe} from './observer/index.js' |
vm._data
的作用其实就是为了让 vm实例上可以获取到 data 的值。
下面我们就来利用观察者模式,实现Vue中的数据劫持。
对象属性劫持
数据劫持主要包括 对象劫持 和 数组劫持 两个方面。
先来看一下对象属性劫持的完整代码。
1 | class Observer { |
导出
observe
函数,进行类型判断。当前文件,我们只处理
Object
类型的值。不是Object
类型的值不做处理。1
2
3
4
5
6export function observe(data) {
if (typeof data !== "object" || data == null) {
return;
}
return new Observer(data);
}完善
Observer
类,让对象上的所有属性依次进行观测。1
2
3
4
5
6
7
8
9
10
11
12
13class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
// 让对象上的所有属性依次进行观测
walk(data) {
let keys = Object.keys(data); // 获取对象上的key值
keys.forEach((key) => {
defineReactive(data, key, data[key]);
});
}
}定义
defineReactive
函数,用来对Object.defineProperty
进行封装。循环调用
observe
函数,保证当前对象内的所有属性都被监听到。(注:主要是为了处理 *{a:{a:{a:1}}}** 这种情况。但这样处理,层级越深性能越差,所以在 Vue3 中将其替换成了
proxy
进行处理。*)1
2
3
4
5
6
7
8
9
10
11
12
13function defineReactive(data, key, value) {
observe(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
},
});
}在
setter
方法中我们会注意到,我们又重新监听了一次newValue
值的变化。这个是因为用户在赋值时,可能会传入一个新的Object
类型的对象,所以我们需要对新传入的值,也进行一次监听。保证所有值的get
和set
都是被重写的。
如果我们使用这种方法去处理数组,也是可以行得通的。但是只能通过索引去进行监听,我们在处理数组时一般会使用专门的方法去进行处理(如 push
、shift
、pop
等等)。为了性能考虑,我们还需要单独对数组方法进行处理。
数组方法劫持
其实数组的劫持,就是对原型上数组的方法进行重写。
首先,需要先判断当前传入的值是否是数组。
1 | import {arrayMethods} from './array'; |
observeArray
可以保证数组上的每一个值都是被检测的,包括 Object
和 其他类型。
arrayMethods
就是重写了当前数组原型上的方法。
重写数组原型方法
下面我们来看一下 arrayMethods
到底是什么。
1 | let oldArrayProtoMethods = Array.prototype; |
上面的意思是将 原始数组原型上的方法继承一份,并导出。然后将我们需要重写的方法循环出来,依次进行处理。
我们这里使用 Object.create
相当于 arrayMethods.__proto__ = oldArrayProtoMethods
,这样会只处理重写后的方法,没重写的方法不会被处理。
现在,我们需要对一些特定的方法进行处理。
1 | // ... |
push
和 unshift
都是添加项的意思,一个是在头部添加,一个是在尾部添加。splice
从第三个参数开始,就是添加的项。
因为新添加的项可能也是 Object
类型,所以我们需要再次 对新添加的项进行劫持。
但是这里就出现了一个问题,我们并没有办法使用劫持方法。又已知当前方法中的this就是被调用数组的value(上面的 apply
改变了 this
指向)。所以我们就可以在value上增加一个属性__ob__
。
增加 __ ob __ 属性
__ob__
属性的作用是给所有响应式数据增加标识,用来判断当前对象是否被劫持过。并且可以在响应式上获取Observer
实例上的方法。
1 | class Observer { |
这样我们就在每一项中都追加了一个 __ob__
属性,现在我们还需要进行一步处理。
1 | // ... |
如果当前项已经被添加了 __ob__
属性,则不进行处理。这样可以防止数据被重复劫持。
这样我们就完成了对数组的数据劫持。
(注:在Vue中不能直接修改指定项,需要通过Array数组中的方法来进行修改。Vue.$set
可以修改特定值,后续我会详细介绍一下其使用和原理。)
属性代理
我们已经对数据进行了相应的劫持,现在可以对 data
进行相应的操作了。
1 | let vm = new Vue({ |
set
和 get
方法都可以被正常触发。
但是这种操作太麻烦了,所以我们现在需要对属性进行代理,让用户可以直接通过 vm.arr
这种方式直接修改值。
1 | function proxy(vm, source, key) { |
同样是使用 Object.defineProperty
对传入的属性进行劫持监听。
当用户到 vm
中获取属性时,属性的值被代理到 vm._data
上。
至此,我们的Vue2响应式原理就介绍完毕了。
本篇文章由 莫小尚 创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点、博客园 和 掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!