概述
Vue 的响应式模型指的是:
视图绑定的数据在更新后也会渲染到视图上
使用 vm.$watch()监听数据的变化, 并调用回调
使用 Vue 实例的属性 watch 注册需要监听的数据和回调
上面的三种方式追根揭底, 都是通过回调的方式去更新视图或者通知观察者更新数据
Vue 的响应式原理是基于观察者模式和 JS 的 API:Object.defineProperty()和 Proxy 对象
主要对象
每一个被观察的对象对应一个 Observer 实例, 一个 Observer 实例对应一个 Dep 实例, Dep 和 Watcher 是多对多的关系, 附上官方的图, 有助于理解:
1. Observer
一个被观察的对象会对应一个 Observer 实例, 包括 options.data.
一个 Observer 实例会包含被观察的对象和一个 Dep 实例.
- export class Observer {
- value: any;
- dep: Dep;
- vmCount: number;
- }
2. Dep
Dep 实例的作用是收集被观察对象 (值) 的订阅者.
一个 Observer 实例对应一个 Dep 实例, 该 Dep 实例的作用会在 Vue.prototype.$set 和 Vue.prototype.$del 中体现 -- 通知观察者.
一个 Observer 实例的每一个属性也会对应一个 Dep 实例, 它们的 getter 都会用这个 Dep 实例收集依赖, 然后在被观察的对象的属性发生变化的时候, 通过 Dep 实例通知观察者.
options.data 就是一个被观察的对象, Vue 会遍历 options.data 里的每一个属性, 如果属性也是对象的话, 它也会被设计成被观察的对象.
- export default class Dep {
- static target: ?Watcher;
- id: number;
- subs: Array<Watcher>;
- }
3. Watcher
一个 Watcher 对应一个观察者, 监听被观察对象 (值) 的变化.
Watcher 会维护一个被观察者的旧值, 并在被通知更新的时候, 会调用自身的 this.getter()去获取最新的值并作为要不要执行回调的依据.
Watcher 分为两类:
视图更新回调, 在数据更新 (setter) 的时候, watcher 会执行 this.getter()-- 这里 Vue 把 this.getter()作为视图更新回调(也就是重新计算得到新的 vnode).
普通回调, 在数据更新 (setter) 的时候, 会通知 Watcher 再次调用 this.getter()获取新值, 如果新旧值对比后需要更新的话, 会把新值和旧值传递给回调.
- export default class Watcher {
- vm: Component;
- expression: string;
- cb: Function;
- id: number;
- deep: boolean;
- user: boolean;
- lazy: boolean;
- sync: boolean;
- dirty: boolean;
- active: boolean;
- deps: Array<Dep>;
- newDeps: Array<Dep>;
- depIds: SimpleSet;
- newDepIds: SimpleSet;
- before: ?Function;
- getter: Function;
- value: any;
- }
使 options.data 成为响应式对象的过程
Vue 使用 initData()初始化 options.data, 并在其中调用了 observe 方法, 接着:
源码中的 observe 方法是过滤掉不是对象或数组的其它数据类型, 言外之意 Vue 仅支持对象或数组的响应式设计, 当然了这也是语言的限制, 因为 Vue 使用 API:
Object.defineProperty()
来设计响应式的.
通过 observe 方法过滤后, 把传入的 value 再次传入
new Observer(value)
在 Observer 构造函数中, 把 Observer 实例连接到 value 的属性__ob__; 如果 value 是数组的话, 需要修改原型上的一些变异方法, 比如 push,pop, 然后调用 observeArray 遍历每个元素并对它们再次使用 observe 方法; 如果 value 是普通对象的话, 对它使用 walk 方法, 在 walk 方法里对每个可遍历属性使用 defineReactive 方法
在 defineReactive 方法里, 需要创建 Dep 的实例, 作用是为了收集 Watcher 实例(观察者), 然后判断该属性的
property.configurable
是不是 false(该属性是不是不可以设置的), 如果是的话返回, 不是的话继续, 对该属性再次使用 observe 方法, 作用是深度遍历, 最后调用
Object.defineProperty
重新设计该属性的 descriptor
在 descriptor 里, 属性的 getter 会使用之前创建的 Dep 实例收集 Watcher 实例(观察者)-- 也是它的静态属性 Dep.target, 如果该属性也是一个对象或数组的话, 它的 Dep 实例也会收集同样的 Watcher 实例; 属性的 setter 会在属性更新值的时候, 新旧值对比判断需不需要更新, 如果需要更新的话, 更新新值并对新值使用 observe 方法, 最后通知 Dep 实例收集的 Watcher 实例 --dep.notify(). 至此响应设计完毕
看一下观察者的构造函数 --
constructor (vm, expOrFn, cb, options, isRenderWatcher)
,vm 表示的是关联的 Vue 实例, expOrFn 用于转化为 Watcher 实例的方法 getter 并且会在初始化 Watcher 的时候被调用, cb 会在新旧值对比后需要更新的时候被调用, options 是一些配置, isRenderWatcher 表示这个 Watcher 实例是不是用于通知视图更新的
Watcher 构造函数中的 expOrFn 会在被调用之前执行 Watcher 实例的 get()方法, 该方法会把该 Watcher 实例设置为 Dep.target, 所以 expOrFn 里的依赖收集的目标将会是该 Watcher 实例
Watcher 实例的 value 属性是响应式设计的关键, 它就是被观察对象的 getter 的调用者 --
value = this.getter.call(vm, vm)
, 它的作用是保留旧值, 用以对比新值, 然后确定是否需要调用回调
总结:
响应式设计里的每个对象都会有一个属性连接到 Observer 实例, 一般是__ob__, 一个 Observer 实例的 value 属性也会连接到这个对象, 它们是双向绑定的
一个 Observer 实例会对应一个 Dep 实例, 这个 Dep 实例会在响应式对象里的所有属性的 getter 里收集 Watcher 实例, 也就是说, 响应式对象的属性更新了, 会通知观察这个响应式对象的 Watcher 实例
在 Vue 里 Watcher 实例, 可以是视图更新回调, 也可以是普通回调, 本质上都是一个函数, 体现了 JS 高阶函数的特性
Vue 的响应式设计很多地方都使用了遍历, 递归
Vue 提供的其它响应式 API
Vue 除了用于更新视图的观察者 API, 还有一些其它的 API
1. Vue 实例的 computed 属性
构造 Vue 实例时, 传入的 options.computed 会被设计成既是观察者又是被观察对象, 主要有下面的三个方法: initComputed,defineComputed,createComputedGetter
- function initComputed (vm: Component, computed: Object) {
- // $flow-disable-line
- const watchers = vm._computedWatchers = Object.create(null)
- // computed properties are just getters during SSR
- const isSSR = isServerRendering()
- for (const key in computed) {
- const userDef = computed[key]
- const getter = typeof userDef === 'function' ? userDef : userDef.get
- if (process.env.NODE_ENV !== 'production' && getter == null) {
- warn(
- `Getter is missing for computed property "${key}".`,
- vm
- )
- }
- if (!isSSR) {
- // create internal watcher for the computed property.
- watchers[key] = new Watcher(
- vm,
- getter || noop,
- noop,
- computedWatcherOptions
- )
- }
- // component-defined computed properties are already defined on the
- // component prototype. We only need to define computed properties defined
- // at instantiation here.
- if (!(key in vm)) {
- defineComputed(vm, key, userDef)
- } else if (process.env.NODE_ENV !== 'production') {
- if (key in vm.$data) {
- warn(`The computed property "${key}" is already defined in data.`, vm)
- } else if (vm.$options.props && key in vm.$options.props) {
- warn(`The computed property "${key}" is already defined as a prop.`, vm)
- }
- }
- }
- }
- export function defineComputed (
- target: any,
- key: string,
- userDef: Object | Function
- ) {
- const shouldCache = !isServerRendering()
- if (typeof userDef === 'function') {
- sharedPropertyDefinition.get = shouldCache
- ? createComputedGetter(key)
- : userDef
- sharedPropertyDefinition.set = noop
- } else {
- sharedPropertyDefinition.get = userDef.get
- ? shouldCache && userDef.cache !== false
- ? createComputedGetter(key)
- : userDef.get
- : noop
- sharedPropertyDefinition.set = userDef.set
- ? userDef.set
- : noop
- }
- if (process.env.NODE_ENV !== 'production' &&
- sharedPropertyDefinition.set === noop) {
- sharedPropertyDefinition.set = function () {
- warn(
- `Computed property "${key}" was assigned to but it has no setter.`,
- this
- )
- }
- }
- Object.defineProperty(target, key, sharedPropertyDefinition)
- }
- function createComputedGetter (key) {
- return function computedGetter () {
- const watcher = this._computedWatchers && this._computedWatchers[key]
- if (watcher) {
- watcher.depend()
- return watcher.evaluate()
- }
- }
- }
2. Vue 实例的 watch 属性
在实例化 Vue 的时候, 会把 options.watch 里的属性都遍历了, 然后对每一个属性调用 vm.$watch()
- function initWatch (vm: Component, watch: Object) {
- for (const key in watch) {
- const handler = watch[key]
- if (Array.isArray(handler)) {
- for (let i = 0; i <handler.length; i++) {
- createWatcher(vm, key, handler[i])
- }
- } else {
- createWatcher(vm, key, handler)
- }
- }
- }
- function createWatcher (
- vm: Component,
- expOrFn: string | Function,
- handler: any,
- options?: Object
- ) {
- if (isPlainObject(handler)) {
- options = handler
- handler = handler.handler
- }
- if (typeof handler === 'string') {
- handler = vm[handler]
- }
- return vm.$watch(expOrFn, handler, options)
- }
vm.$watch 被作为一个独立的 API 导出.
3. Vue.prototype.$watch
Vue.prototype.$watch 是 Vue 的公开 API, 可以用来观察 options.data 里的属性.
- Vue.prototype.$watch = function (
- expOrFn: string | Function,
- cb: any,
- options?: Object
- ): Function {
- const vm: Component = this
- if (isPlainObject(cb)) {
- return createWatcher(vm, expOrFn, cb, options)
- }
- options = options || {}
- options.user = true
- const watcher = new Watcher(vm, expOrFn, cb, options)
- if (options.immediate) {
- cb.call(vm, watcher.value)
- }
- return function unwatchFn () {
- watcher.teardown()
- }
- }
4. Vue.prototype.$set
Vue.prototype.$set 用于在操作响应式对象和数组的时候通知观察者, 也包括给对象新增属性, 给数组新增元素.
- Vue.prototype.$set = set
- /**
- * Set a property on an object. Adds the new property and
- * triggers change notification if the property doesn't
- * already exist.
- */
- export function set (target: Array<any> | Object, key: any, val: any): any {
- if (process.env.NODE_ENV !== 'production' &&
- (isUndef(target) || isPrimitive(target))
- ) {
- warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
- }
- if (Array.isArray(target) && isValidArrayIndex(key)) {
- target.length = Math.max(target.length, key)
- target.splice(key, 1, val)
- return val
- }
- if (key in target && !(key in Object.prototype)) {
- target[key] = val
- return val
- }
- const ob = (target: any).__ob__
- if (target._isVue || (ob && ob.vmCount)) {
- process.env.NODE_ENV !== 'production' && warn(
- 'Avoid adding reactive properties to a Vue instance or its root $data' +
- 'at runtime - declare it upfront in the data option.'
- )
- return val
- }
- if (!ob) {
- target[key] = val
- return val
- }
- defineReactive(ob.value, key, val)
- ob.dep.notify()
- return val
- }
ob.dep.notify()之所以可以通知观察者, 是因为在 defineReactive 里有如下代码:
- let childOb = !shallow && observe(val)
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter () {
- const value = getter ? getter.call(obj) : val
- if (Dep.target) {
- dep.depend()
- if (childOb) {
- childOb.dep.depend()
- if (Array.isArray(value)) {
- dependArray(value)
- }
- }
- }
- return value
- },
- 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()
- }
- if (setter) {
- setter.call(obj, newVal)
- } else {
- val = newVal
- }
- childOb = !shallow && observe(newVal)
- dep.notify()
- }
- })
上面的 childOb.dep.depend()也为响应式对象的__ob__.dep 添加了同样的 Watcher 实例. 所以 Vue.prototype.$set 和 Vue.prototype.$del 都可以在内部通知观察者.
5. Vue.prototype.$del
Vue.prototype.$del 用于删除响应式对象的属性或数组的元素时通知观察者.
- Vue.prototype.$del = del
- /**
- * Delete a property and trigger change if necessary.
- */
- export function del (target: Array<any> | Object, key: any) {
- if (process.env.NODE_ENV !== 'production' &&
- (isUndef(target) || isPrimitive(target))
- ) {
- warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
- }
- if (Array.isArray(target) && isValidArrayIndex(key)) {
- target.splice(key, 1)
- return
- }
- const ob = (target: any).__ob__
- if (target._isVue || (ob && ob.vmCount)) {
- process.env.NODE_ENV !== 'production' && warn(
- 'Avoid deleting properties on a Vue instance or its root $data' +
- '- just set it to null.'
- )
- return
- }
- if (!hasOwn(target, key)) {
- return
- }
- delete target[key]
- if (!ob) {
- return
- }
- ob.dep.notify()
- }
简单实现响应式设计
实现 Watcher 类和 Dep 类, Watcher 作用是执行回调, Dep 作用是收集 Watcher
- class Watcher {
- constructor(cb) {
- this.callback = cb
- }
- update(newValue) {
- this.callback && this.callback(newValue)
- }
- }
- class Dep {
- // static Target
- constructor() {
- this.subs = []
- }
- addSub(sub) {
- this.subs.push(sub)
- }
- notify(newValue) {
- this.subs.forEach(sub => sub.update(newValue))
- }
- }
处理观察者和被观察者
- // 对被观察者使用
- function observe(obj) {
- let keys = Object.keys(obj)
- let observer = {}
- keys.forEach(key => {
- let dep = new Dep()
- Object.defineProperty(observer, key, {
- configurable: true,
- enumerable: true,
- get: function () {
- if (Dep.Target) dep.addSub(Dep.Target)
- return obj[key]
- },
- set: function (newValue) {
- dep.notify(newValue)
- obj[key] = newValue
- }
- })
- })
- return observer
- }
- // 对观察者使用
- function watching(obj, key) {
- let cb = newValue => {
- obj[key] = newValue
- }
- Dep.Target = new Watcher(cb)
- return obj
- }
检验代码
- let subscriber = watching({
- }, 'a')
- let observed = observe({
- a: '1'
- })
- subscriber.a = observed.a
- console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
- observed.a = 2
- console.log(`subscriber.a: ${subscriber.a}, observed.a: ${observed.a}`)
结果:
- subscriber.a: 1, observed.a: 1
- subscriber.a: 2, observed.a: 2
CodePen 演示
参考
深入理解 Vue 响应式原理 vue.JS 源码 - 剖析 observer,dep,watch 三者关系 如何具体的实现数据双向绑定 https://segmentfault.com/a/1190000014360080 50 行代码的 MVVM, 感受闭包的艺术 Vue.JS 技术揭秘 https://ustbhuangyi.github.io/vue-analysis/
来源: https://juejin.im/post/5bcb56dc6fb9a05cdb106f64