1. 前言
本文主要是梳理一下vue中响应式的处理与实现,部分代码是经过删减或是自己理解的,并不等于vue中的源码
2. 了解一下MVVM
2.1 什么是MVVM?
MVVM是Model-View-ViewModel的简写
- M代表model(数据,模型)
- V代表view(视图,模板)
- VM代表ViewModel(连接model和view,从而实现视图和数据分离) View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的,View可以通过事件绑定改变数据,Model通过数据绑定驱动视图渲染更新
2.2 Vue是如何体现MVVM思想?
Vue 就是一个典型的 MVVM 模型的框架,充当了 MVVM 开发模式中的 ViewModel 层,负责 View 和 Model 之间通信的桥梁,其中最主要是体现在以下三点:
- 数据响应式(vue如何监听到data每个属性的变化)
- 模板解析引擎:vue模板如何被解析,怎么和数据进行关联
- 渲染:vue模板如何被渲染成?数据更新后又是什么重新渲染dom
3. Vue初始化过程
从一个简单的Vue实例的代码来分析Vue的响应式原理
// html部分
<div id="app"> {{ msg }} </div>
// script部分
<script>
new Vue({
el: '#app',
data: { msg: 'hello vue' }
})
</script>
这段代码很简单,最终会在页面上显示 hello vue,它是如何实现的呢?
我们从源头:new Vue 的地方开始分析
3.1 new Vue
在使用new Vue时,是对 /src/core/instance/index.js 中的Vue进行了实例化
import { initMixin } from './init'
// Vue 构造函数
function Vue (options) {
// 调用 Vue.prototype._init 方法,该方法是在 initMixin 中定义的
this._init(options)
}
// 定义 Vue.prototype._init 方法
initMixin(Vue)
export default Vue
3.2 Vue.prototype._init
/src/core/instance/init.js,可以看到 initState(vm) 是用来初始化data,props,methods,computed以及watch
/**
* 定义 Vue.prototype._init 方法
*/
export function initMixin (Vue) {
// 这主要是Vue 的初始化过程
Vue.prototype._init = function (options) {
// vue 实例
const vm = this
// 注意:这里是删减了一些代码
// 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
initLifecycle(vm)
// 初始化自定义事件
initEvents(vm)
// 解析组件的插槽信息
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象
// 然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
initInjections(vm)
// !!!注意:这里的initState是数据响应式的重点
// 处理 data、props、methods、computed、watch
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
initProvide(vm)
// 调用 created 钩子函数
// 所以在vue的生命周期中,最早能访问data里定义的数据,是在 created
callHook(vm, 'created')
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法
// 也就是说有了 el 选项,就不需要再手动调用 $mount
// 反之,没有 el 则必须手动调用 $mount
if (vm.$options.el) {
// 调用 $mount 方法,进入挂载阶段
vm.$mount(vm.$options.el)
}
}
}
3.3 initState(vm):数据响应式的处理
initState方法是在/src/core/instance/state.js,本篇主要是梳理一下初始化data的过程
export function initState (vm) {
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化方法
if (opts.methods) initMethods(vm, opts.methods)
// 初始化data
if (opts.data) {
initData(vm)
} else {
// 该组件没有data的时候绑定一个空对象
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed)
初始化watchers
if (opts.watch) initWatch(vm, opts.watch)
}
3.4 initData(vm)
在初始化 data 的过程中,主要做了三件事:
- 在 data 中的属性进行判断,data 上的属性不能和 props、methods 对象上的属性相同
- 把 data 代码在 vm(vue的实例) 上
- Vue 将遍历 data 中的所有 属性,并使用
Object.defineProperty把这些属性全部转为 getter/setter。Object.defineProperty是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因
function initData (vm) {
// 获取data数据
let data = vm.$options.data
...
//遍历data中的数据
while (i--) {
const key = keys[i]
// 保证data中的key不与methods中的key重复,methods优先,如果有冲突会产生warning
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 与上面的methods判断同理
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
// 判断是否是保留字段
// 将data上面的属性代理到了vm实例上
proxy(vm, `_data`, keys[i])
}
}
// observe data
// 响应式处理的真正入口
// 这里通过observe实例化Observe对象,开始对数据进行绑定
observe(data, true /* asRootData */)
}
3.5 Observe
我们都知道,Vue 通过 Object.defineProperty 将 data 中声明的属性转换成getter/setter形式,Observer类则是用于进行依赖收集以及调度更新
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
...
if (Array.isArray(value)) {
// 如果是数组,覆盖数组默认的七个原型方法,以实现数组响应式
...
} else {
// value 为对象,为对象的每个属性设置响应:递归遍历所有属性
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
// walk方法会遍历对象的每一个属性,调用defineReactive方法
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
// 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
observeArray (items) {
for(let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
3.6 defineReactive
- defineReactive方法会遍历 data 中所有的属性,通过
Object.defineProperty为每个 key 设置响应式- 因为是递归遍历所有属性,性能较差。所以在定义 data 的时候,最好不要使用多层嵌套。data 的结构尽量扁平化
function defineReactive(obj, key, val) {
// 实例化 dep,一个 key 一个 dep
const dep = new Dep()
// 对象的子对象递归进行observe并返回子节点的Observer对象
let childOb = observe(val)
// 数据响应式的核心
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
// 子对象进行依赖收集
childOb.dep.depend()
}
...
}
return val
},
set(newVal) {
// 用户赋值的值可能是一个对象,所以也要进行数据劫持
observe(newVal)
val = newVal
}
})
}
3.7 数组的响应式处理
因为 Object.defineProperty存在缺陷性,无法原生监听数组的变化。所以对于数组需要进行特殊处理。用户在调用数组的
push、shift、unshift、pop、reverse、sort、splice这七个可以修改原数组的方法时,需要调用重写后的方法
// 原始的数组的原型对象
const oldProto = Array.prototype
// 通过继承的方式创建新的 arrayMethods
export const arrayMethods = Object.create(arrayProto)
// 操作数组的七个方法,这七个方法可以改变数组自身
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(method => {
arrayMethods[method] = function(...args) {
console.log(数组发送变化)
// 先执行原生方法,比如 push.call(this, ...args)
oldProto[method].call(this,...args)
let inserted
// 如果是数组新增值,需要对数组新增的值进行响应式处理
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 对新插入的元素做响应式处理
if (inserted) ob.observeArray(inserted)
// 通知更新
ob.dep.notify()
return result
}
})
3.8 Watcher
Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例回调cb进行视图的更新
src/core/observer/watcher.js
export default class Watcher {
constructor (vm, expOrFn, cb, option) {
this.vm = vm
// _watchers存放订阅者实例
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
// 把表达式expOrFn解析成getter
this.getter = expOrFn
this.value = this.lazy
? undefined
: this.get()
}
// 获得getter的值并且重新进行依赖收集
get () {
// 将自身watcher观察者实例设置给Dep.target,用以依赖收集
pushTarget(this)
let value
const vm = this.vm
/*执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
在将Dep.target设置为自生观察者实例以后,执行getter操作。
譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
那么在执行getter的时候就会触发a跟c两个数据的getter函数,
在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
将该观察者对象放入闭包中的Dep的subs中去。*/
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// 如果存在deep,则触发每个深层对象的依赖,追踪其变化
if (this.deep) {
// 递归每一个对象或者数组,触发它们的getter
// 使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系
traverse(value)
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
popTarget()
this.cleanupDeps()
return value
}
// 添加一个依赖关系到Deps集合中
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
...
// 当数据更新时,dep 会通知 watcher 更新
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this)
}
}
run () {
if (this.active) {
/* get操作在获取value本身也会执行getter从而调用update更新视图 */
const value = this.get()
if (
/*
即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
*/
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
/*设置新的值*/
this.value = value
/*触发回调*/
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// 获取观察者的值
evaluate () {
this.value = this.get()
this.dirty = false
}
// 收集该watcher的所有deps依赖
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
...
}
3.9 Dep
data 的属性被Observer后,在触发 getter 时,Dep 就会收集依赖的 Watcher ,其实 Dep 可以接受多个订阅者的订阅,当 data 中某个属性变动时,就会通过 Dep 给 Watcher 发通知进行更新
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/**
* 一个 dep 对应一个 obj.key
* 在读取响应式数据时,负责收集依赖,每个 dep(或者说 obj.key)依赖的 watcher 有哪些
* 在响应式数据更新时,负责通知 dep 中那些 watcher 去执行 update 方法
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 在 dep 中添加 watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 像 watcher 中添加 dep
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
/**
* 通知 dep 中的所有 watcher,执行 watcher.update() 方法
*/
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 遍历 dep 中存储的 watcher,执行 watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/**
* 当前正在执行的 watcher,同一时间只会有一个 watcher 在执行
* Dep.target = 当前正在执行的 watcher
* 通过调用 pushTarget 方法完成赋值,调用 popTarget 方法完成重置(null)
*/
Dep.target = null
const targetStack = []
// 在需要进行依赖收集的时候调用,设置 Dep.target = watcher
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 依赖收集结束调用,设置 Dep.target = null
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
4. 总结
通过上面的代码和描述,结合vue官网的说明,大家应该带vue的初始化过程和响应式处理有了一定的了解,此时再看官网上的这张图,相信大家已经有了自己的理解。
- Vue 的初始化过程(new Vue(options))都做了什么?
- initLifecycle:初始化组件实例的关系
- initMethods: 初始化自定义事件
- initRender在:解析组件的插槽
- callHook(vm, 'beforeCreate'):调用 beforeCreate
- initInjections:初始化组件的 inject 配置项
- initState:数据响应式的重点,处理 props、methods、data、computed、watch
- initProvide: 解析组件配置项上的 provide 对象,将其挂载到vm._provided
- callHook(vm, 'created'): 调用 created
- 数据响应式的处理与原理
- 响应式的核心是通过
Object.defineProperty拦截对数据的访问和设置 - 从 new Vue 开始,设置 get、set 监听 Data 中的数据变化,同时创建 Dep 用来搜集使用该 Data 的 Watcher
- 响应式的分类:
-
对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter
- 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher
- 设置数据时由 dep 通知相关的 watcher 去更新
-
数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
- 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
- 删除数据时,也要由 dep 通知 watcher 去更新
-
- 响应式的核心是通过