理解Vue响应式原理,实现一个Mini vue
MVVM(Model-View-ViewModel)是一种程序的架构设计,相比于MVC,ViewModel充当了控制层(Control),Vuejs的核心就是实现这个ViewModel,用一张图表示,就是下面这样
在JS中,Model可以看做是Object对象,View就是HTML网页。在传统开发中,当数据发生变化时,开发者需要手动更新DOM,进而改变视图层。而在Vue中,一切视图的改变都是基于数据的变动,也就是说,开发者不需要再操作DOM。ViewModel的核心就是建立视图和数据之间的联系,做到数据驱动视图的变化。所谓双向数据绑定,不过就是数据驱动视图变化,视图驱动数据变化
Vue实现ViewModel是通过Object.defineProperty实现的,关于defineProperty的基本语法可移步至此。Vue在实例化的过程中,会遍历data中的所有属性,然后通过Object.defineProperty把这些属性全部转为 getter/setter,内部用一个Observer对象监听数据的设置和读取,这个过程也叫作数据劫持。每一个Vue实例都会有一个Watcher(订阅者)对象,在模板编译过程中,需要插入数据的地方都会用getter去访问data属性,比如v-model
或者v-text
等等,Watcher会把用到的data属性记为依赖,在内部用Dep对象统一管理这些依赖,这样就建立了视图和数据之间的联系。当渲染视图的数据依赖发生变化时,setter函数会被调用,Watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。
用一张图表示就是下面这样
下面实现一个简版的Vue.js
<!DOCTYPE html>
<html lang="en">
<head>
<title>Mini Vue.js</title>
</head>
<div id="app"></div>
<body>
<script>
// vue 实例,接收一个 option(Object) 参数
class Vue {
constructor(options = {}) {
this.$options = options
// 简化了对data的处理
let data = this._data = this.$options.data
// 遍历data, 将所有data最外层属性代理到Vue实例上
// this.key 就能访问到 data 对象中的数据
Object.keys(data).forEach(key => this._proxy(key))
// 监听数据
observe(data)
// 获取dom节点
this.$el = document.querySelector(options.el)
// 渲染DOM
this._randerDom()
}
_randerDom(val) {
// 解析字符串模版,为了简单直接用了innerHTML
if (this.$el && this.$options && this.$options.template) {
this.$el.innerHTML = this.$options.template(this._data)
}
}
// 暴露一个在vue实例外使用订阅者的接口,在实例内部主要在指令中使用
// 指令的实现需要编写complie函数,为了简单,不写该函数也不影响理解vue原理
$watch(expOrFn, cb) {
// 添加订阅者
new Watcher(this, expOrFn, cb)
}
_proxy(key) {
// 把这data属性全部转为 getter/setter。
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: () => this._data[key],
set: (val) => {
this._data[key] = val
}
})
}
}
function observe(value) {
// 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
/*
* Observer.js
*/
class Observer {
constructor(value) {
this.value = value
this.walk(value)
}
walk(value) {
// 遍历传入的data, 将所有data的属性添加set&get
Object.keys(value).forEach(key => this.convert(key, value[key]))
}
convert(key, val) {
// 添加set&get方法
defineReactive(this.value, key, val)
}
}
function defineReactive(obj, key, val) {
var dep = new Dep()
// 给传入的data内部对象递归的调用observe,来实现深度监听
var chlidOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可修改
get: () => {
// Watcher实例在实例化过程中,会为Dep添加一个target属性,在读取data中的某个属性,会触发当前get方法。
// 如果Dep类存在target属性,将订阅者添加到dep实例的subs数组中
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set: (newVal) => {
// 在设置data中的某个属性,会触发当前set方法。
if (val === newVal) return
val = newVal
// 对新值进行监听
chlidOb = observe(newVal)
// 通知所有订阅者,数值被改变了
dep.notify()
}
})
}
// 订阅者管理员,管理所有订阅者队列
class Dep {
constructor() {
this.subs = [] // 订阅者队列
}
addSub(sub) {
this.subs.push(sub) // 添加订阅者
}
notify() {
// 当改变data中的属性值时,会通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
this.subs.forEach((sub) => sub.update())
}
}
/*
* Watcher订阅者
*/
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm // 被订阅的数据一定来自于当前Vue实例
this.cb = cb // 当数据更新时需要执行的回调函数
this.expOrFn = expOrFn // 被监听的数据(表达式或函数)
this.oldVal = this.get() // 维护更新之前的数据
}
// 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
update() {
this.vm._randerDom() // 检测的数据变动后,更新dom
this.run()
}
run() {
const newVal = this.get()
if (newVal !== this.oldVal) {
this.cb.call(this.vm, this.oldVal, newVal)
this.oldVal = newVal;
}
}
get() {
// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
Dep.target = this
const val = this.vm._data[this.expOrFn]
// 置空,用于下一个Watcher使用
Dep.target = null
return val;
}
}
let app = new Vue({
el: '#app',
template(data) {
return `
<p>${data.title}</p>`
},
data: {
'title': 'Hello Vue'
}
});
app.$watch('title', function(oldVal, newVal) {
console.log('oldVal:' + oldVal)
console.log('newVal:' + newVal)
})
</script>
</body>
</html>
不到200行代码,实现了Vue的核心功能,包括依赖收集、数据劫持、视图渲染。当然,由于是简版的Vue,只是为了便于理解Vue的原理,所以很多功能都剔除了。示例可以直接在chrome下运行,不需要babel编译。
运行效果