前言
Vue 是目前流行前端框架,其独特的特性是其非侵入性的响应式系统。当侦测到数据的变化来更新视图,原理核心是使用 Object.defineProperty
方法。本文对响应式原理进行分析,参照 vue 源码实现简易版的数据响应式。
代码实现
本文完整代码点击这里
import { def, isObject } from '../util/index'
class Observer {
constructor (value) {
this.value = value
// 通过 defineProperty 为对象添加 __ob__ 属性,并且配置为不可枚举
// 这样做的意义是对象遍历时不会遍历到 __ob__ 属性
def(value, '__ob__', this)
this.walk(value)
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive (obj, key, val) {
// 如果 val 是对象的话递归监听
observe(val)
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get: function reactiveGetter () {
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val || (newVal !== newVal && val !== val)) {
return
}
val = newVal
// 如果赋值是对象的话也要递归监听
observe(newVal)
console.log('侦测到数据变化', newVal);
}
})
}
export function observe (value) {
// 类型判断,不是对象类型直接返回
if (!isObject(value)) {
return
}
let ob = new Observer(value)
return ob
}
上面这段代码主要作用在于:observe
函数传入一个 value
(需要被追踪变化的对象),作为 Observer
类的参数实例化,遍历所有属性对该对象的每一个属性都通过 defineReactive
处理, 在 defineReactive
方法内 observe
会进行递归调用,以此来达到实现侦测对象变化。
接下来实现 Vue
类,对 options 中的 data 传入 observe 开发进行初始化数据侦测。
import { observe } from '../observer/index'
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: function () {},
set: function () {}
}
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function Vue (options) {
let vm = this
let data = options.data
vm._data = data
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
// 可以让 vm._data.x 通过 vm.x 访问
proxy(vm, `_data`, key)
}
observe(data)
}
export default Vue
实例化 Vue 类,查看控制台的输出 vm。
import Vue from './instance/index'
const vm = new Vue({
data: {
message: 'hello',
location: { x: 100, y: 100 },
arr: [1]
}
})
window.vm = vm
// 在控制台输入: vm.location = { x: 10, y: 10 }
// 输出:侦测到数据变化 {__ob__: Observer}
// 输入:vm.location.z = 10
// 输出:10
// 输入:vm.arr.push(2)
// 输出:2
从中可以看到几个问题:
- 无法检测到对象属性的添加或删除
通过 Object.defineProperty
来将对象的key转换成 getter/setter
的形式来追踪变化,但 getter/setter
只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。
实现 Vue 提供的 set
和 delete
方法向嵌套对象添加/删除响应式属性。
- 不能监听数组的变化,需要进行数组方法的重写
解决以上问题,具体代码如下:
// array.js
// 获得数组原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 创建一个自己的原型 并且重写 methods 这些方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
arrayMethods[method] = function (...args) {
const result = original.apply(this, args)
return result
}
})
在 observer/index.js
中导入 arrayMethods
重写的原型对象。
import { arrayMethods } from './array'
import { def, isObject } from '../util/index'
class Observer {
constructor (value) {
this.value = value
def(value, '__ob__', this)
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
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])
}
}
}
以上代码实现了数据劫持,接下来需要实现收集依赖以及数据更新时派发更新,其中的核心思想就是发布-订阅模式
。关于订阅者 Dep
和观察者 Watcher
相关代码。
订阅者 Dep
import { remove } from '../util/index'
export default class Dep {
constructor () {
/* 用来存放 Watcher 对象的数组 */
this.subs = []
}
/* 在 subs 中添加一个 Watcher 对象 */
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
/* 通知所有 Watcher 对象更新视图 */
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/* 在 watcher.js 中调用,将 Watcher 实例赋值给 Dep.target */
Dep.target = null
观察者 Watcher
import Dep from './dep'
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = expOrFn || function () {}
this.cb = cb
this.value = this.get()
}
get () {
Dep.target = this
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
addDep(dep) {
dep.addSub(this)
}
update () {
console.log('更新value:', this.value)
}
}
在执行构造函数的时候将 Dep.target
指向自身,从而使得收集到了对应的 Watcher
,在派发更新的时候取出对应的 Watcher
,然后执行 update
函数。
最后对 defineReactive
函数和 Vue
类进行改造
export function defineReactive (obj, key, val) {
const dep = new Dep()
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 将 Watcher 添加到订阅
dep.depend()
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val || (newVal !== newVal && val !== val)) {
return
}
val = newVal
childOb = observe(newVal)
// 执行 watcher 的 update 方法
dep.notify()
}
})
}
function Vue (options) {
let vm = this
let data = options.data
vm._data = data
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data)
/* 新建一个 Watcher 观察者对象,这时候 Dep.target 会指向这个 Watcher 对象 */
new Watcher(data, val => val)
}
当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。
总结
本文在阅读 Vue 源码后,根据自己的理解加上参考其他文章,编写的一个精简代码实现。其中代码实现并不严谨以及自身的理解不到位,在此深表惭愧。