响应式原理
Vue 在初始化数据时,会给 data 中的所有属性使用 Object.defineProperty 重新定义 setter 和 getter , 当页面获取到对应属性时,会触发 get 方法并进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通知相关依赖,触发对应的watcher进行更新操作 。
什么叫依赖收集 ?
通过Object.defineProperty在重新定义data属性的时候,进行拦截,再进行实际渲染 ; 那实际渲染之前的一系列处理逻辑就是依赖收集上边有说,会在依赖收集的时候为每一个属性创建一个watcher,如果属性发生变化,则通知对应的 watcher 更新视图。
来看看源码
在看源码之前呢,先考虑一个问题,我们天天写Vue,都知道它有一个从创建到销毁的过程,官方点来说,那就是Vue的生命周期,okey !
[一眼望穿流水]
new Vue实例 - 初始化data ,events - 模板编译 - 实例挂载 - 实例更新 - 实例销毁
好,我们就从 new Vue() 实例开始
源码分析
1,new Vue() 都干了些什么事儿 ?
code position : src/core/instance/index.js
// 一系列导入模块
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
// 很简短啊 , 就是调用了Vue构造函数的 _init 方法并传入了options 参数
this._init(options)
}
initMixin(Vue) // 1.初始化各个_init方法, 包含初始化 options, render, events,beforCreated 等
stateMixin(Vue) // 2.使用 Object.defineProperty 创建响应式数据, 并且初始化 $set $delete $watch 等
eventsMixin(Vue) // 3.初始化vue中的 $on $emit 等事件
lifecycleMixin(Vue) // 4.初始化生命周期
renderMixin(Vue) // 5.初始化_render方法
export default Vue
经过一系列模块的的导入,我们可以看到:function Vue (options) {...} 内部唯一行干练的代码 this._init(options),然后就是一系列的各种初始化方法调用,最后导出Vue实例。
那么这个this._init(options) 在哪里定义的 ? 在 initMixin 模块中
code position : src/core/instance/init.js
export function initMixin (Vue) {
// 直接挂载在 Vue 实例原型上
Vue.prototype._init = function (options) {
const vm = this
// 合并 options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 通过 $mount 方法挂载实例 , 后续我们分析 $mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
可以看出来这个init.js主要做了几件事,分别是将用户初始化实例的 options 与构造含函数的options 合并 , 然后调用一系列各模块初始化方法,最后通过 $mount 挂载实例 。
回到我们的主题,响应式数据原理,也就是初始化Vue数据是如何渲染并建立监听的,所以我们主要看 stateMixin 模块
2,初始化 data
进入stateMixin 模块 , 我们直接看向 initData 函数,故名思意,初始化data属性
// vm : 构造函数根实例
function initData (vm: Component) {
// 获取到用户传入的data数据
let data = vm.$options.data
// 模板语法与标准语法区分获取data数据
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
/*
中间我就跳过了,看意思是非生产环境下,对 props , methods 的一些定义,声明做的判断,不允许重复声明
另外就是添加了 proxy , es6 新增代理属性 , 包含所有 Object.defineProperty 的功能, 重要的一点是
解决了不能对,数组,对象监听的问题等。
*/
// observe data 重点在这儿
observe(data, true /* asRootData */)
}
重点 : observe(data, true /* asRootData */)
3,创建观测
其实 obsrver 方法基本没有做多少事儿 , 主要是对 data 类型做了判断,然后判断data属性是否已经被监听,如果监听了直接赋值,没有监听则创建监听 new Observer()
code position : src/core/observer/index.js 113 行
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
/*
不是对象不进行观测, 如:不管是模板语法还是标准语法data均返回一个对象
data () { 模板语法返回一个对象
return {}
}
new Vue ({ 标准语法 data 也是一个对象
data:{}
})
*/
return
}
let ob: Observer | void
// 已经被监听的,不会重复监听
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__b__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 重点,重点,重点,没有被监听的对象则创建监听
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
顺着它来 : new Observer() 创建监听 , 注意这是两个东西 observer 方法 和 Observer 类
code position : src/core/observer/index.js 37 行
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
/*
观测呢分为两种,一种是数组,一种是对象
数组:是通过改写数组原型方法,包含 push,shift,unshift,pop ,splice, sort 等
也就是说 vue 监听数组变化是通过改写原型方法 + 递归遍历实现的数据观测 。 后续我们详解
*/
if (Array.isArray(value)) { // 是数组
if (hasProto) {
protoAugment(value, arrayMethods) // 改写数组原型方法
} else {
copyAugment(value, arrayMethods, arrayKeys) // 复制数组已有方法
}
this.observeArray(value) // 深度观察数组中的每一项 , observeArray 往下看方法实体
} else {
this.walk(value) // 重新定义对象类型数据 walk 往下看方法实体
}
}
// walk : 是对象则走进这个方法,上边有判断
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 定义响应式数据,这里可以看到 defineReactive 方法 , 往下看方法实体
defineReactive(obj, keys[i]);
}
}
// 是数组则遍历数组,走进observer方法查看是否被监听, 没有监听则继续递归回调回来
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 观测数组中的每一项
observe(items[i])
}
}
}
通过上边的代码,我们看到了Observer 类主要做了一件事,就是区分数组和对象,并对数组和对象遍历,然后执行 defineReactive 方法 , 总之到最后都会走到 defineReactive 方法 接下来我们看 defineReactive 响应式数据绑定关键方法 也就是我们常常说到的 Object.defineProperty() 应用的地方 。
4,数据劫持
code position : src/core/observer/index.js 148 行
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建一个依赖收集器 - 具体Dep类实体往下移步30s
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 是数组则递归观测
let childOb = !shallow && observe(val)
// 重点 , 重点 , 重点
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 描述属性get => dep.depend()
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 收集依赖 watcher ,也就是创建观察者
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
// 数组递归收集
dependArray(value)
}
}
}
return value
},
// 描述属性 set => dep.notify()
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 触发数据对应的依赖进行更新 , 重点,重点,往下看
dep.notify()
}
})
}
4-1, Dep 订阅器
Dep 所有观察者的集合,也就是 wathcer 的集合,在创建观测的时候,为该组件创建一个 dep 收集器 const dep = new Dep(),通过 dep.depend() 为每一个data属性创建一个自己 watcher 观察者( Object.defineProperty方法的 get 里边),那么触发数据更新的 set 方法会调用 dep.notify() 触发视图更新
这里有一个重要的知识点:一个组件只有一dep , 而会有多个 wathcer 原因: Go Go Go
Dep 类可以看见有构造函数,添加 watcher , 删除 watcher 等方法,那么我们来看看Dep 收集器 和 更新方法 notify()
code position : src/core/observer/dep.js 13行
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知存储的依赖更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 依赖中对应修改属性的update方法
subs[i].update()
}
}
}
其实这里就是定义的 wachter 观察者类,里边有各种操作 wachter 观察者的方法,如:增加,修改,清除等。
5,更新视图
update () 方法在 src/core/observer/wachter.js 166行 ,主要就是修改数据,然后驱动视图,这里不做进一步分析,因为分享到这里,大家就应该能理解vue的响应式原理了;
注释:update 方法牵扯到另外一个异步队列问题,也就是API nextTick 请看后续文章。
欢迎点赞,小小鼓励,大大成长