Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。之前的jquery想要改变试图,是直接操作DOM来实现。现在我们只需要关心数据的改变会让代码的逻辑变的非常清晰。
Vue 内部就有一个机制能监听到数据变化然后触发更新 本篇主要介绍响应式数据的原理
1、数据的初始化
new Vue({
el: "#app",
router,
store,
render: (h) => h(App),
});
这段代码 大家一定非常熟悉 这就是 Vue 实例化的过程 从 new 操作符 咱们可以看出 Vue 其实就是一个构造函数 没啥特别的 传入的参数就是一个对象 options,下面我们就来看下,实例的创建,我们构造函数都做了哪些操作
// src/core/instance/index.js
import { initMixin } from './init'
// Vue就是一个构造函数 通过new关键字进行实例化
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//_init方法是挂载在Vue原型的方法 通过引入文件的方式进行原型挂载需要传入Vue
// 此做法有利于代码分割
initMixin(Vue)
export default Vue
这里创建了一个构造函数Vue,里面就是执行了一个_init函数
// src/core/instance/init.js
import { initState } from "./state";
//initMixin 把_init 方法挂载在 Vue 原型 供 Vue 实例调用
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
// 这里的this代表调用_init方法的对象(实例对象)
// this.$options就是用户new Vue的时候传入的属性
vm.$options = options;
// 初始化状态
initState(vm);
};
}
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等,这里只介绍初始化data数据,别的后面文章再详细介绍
// src/core/instance/state.js
import { observe } from '../observer/index'
export function initState (vm) {
const opts = vm.$options
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {})
}
}
function initData (vm: Component) {
let data = vm.$options.data
// 实例的_data属性就是传入的data
// vue组件data推荐使用函数 防止数据在组件之间共享
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
// 把data数据绑定到vm上,在页面中可以通过this.来获取和赋值
// 修改_data的值
proxy(vm, `_data`, key)
}
observe(data)
}
// 数据代理
function proxy(object, sourceKey, key) {
Object.defineProperty(object, key, {
get() {
return object[sourceKey][key];
},
set(newValue) {
object[sourceKey][key] = newValue;
},
});
}
这里主要是把data的数据代理到this._data,后面用于this.$data,我们主要关注 initData 里面的 observe 是响应式数据核心
2.对象的数据劫持
// src/core/observer/index.js
export function observe (value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
export class Observer {
constructor (value) {
// 如果是数组的话,重新定义
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
export function defineReactive (obj, key) {
observe(value); // 递归关键
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 获取值
return value
},
set (newVal) {
//设置值
val = newVal
}
})
}
数据劫持核心是 defineReactive 函数 主要使用 Object.defineProperty 来对数据 get 和 set 进行劫持 这里就解决了之前的问题 为啥数据变动了会自动更新视图 我们可以在 set 里面去通知视图更新
上面Observer函数中对数组,我们进行了额外的处理,为什么数组,我们不进行直接的劫持呢?
如果说我们对数组的下标get,set劫持,我们也能够相应变化,但是如果数据里面有成千上万个数呢,每一个元素下标都添加 get 和 set 方法 这样对于性能来说是承担不起的 所以此方法只用来劫持对象。
3.数组的观测
// src/core/observer/index.js
import { arrayMethods } from "./array";
class Observer {
constructor(value) {
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 这里对数组做了额外判断
// 通过重写数组原型方法来对数组的七种方法进行拦截
value.__proto__ = arrayMethods;
// 如果数组里面还包含数组 需要递归判断
this.observeArray(value);
} else {
this.walk(value);
}
}
observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i]);
}
}
}
export function def (obj, key, val) {
Object.defineProperty(obj, key, {
// val值指代的就是Observer的实例
value: val,
// 不可枚举
enumerable: false,
writable: true,
configurable: true
})
}
因为对数组下标的拦截太浪费性能 对 Observer 构造函数传入的数据参数增加了数组的判断
添加 __ob__ 这段代码的意思就是给每个响应式数据增加了一个不可枚举的__ob__属性 并且指向了 Observer 实例 那么我们首先可以根据这个属性来防止已经被响应式观察的数据反复被观测 其次 响应式数据可以使用__ob__来获取 Observer 实例的相关方法 这对数组很关键
// src/core/observer/array.js
// 先保留数组原型
const arrayProto = Array.prototype;
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"reverse",
"sort",
];
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
// 这里保留原型方法的执行结果
const result = arrayProto[method].apply(this, args);
// 这句话是关键
// this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
const ob = this.__ob__;
// 这里的标志就是代表数组有新增操作
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
// 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
if (inserted) ob.observeArray(inserted);
// 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
return result;
};
});