再谈Vue数据响应式
再谈Vue数据响应式
上文Vue数据响应式介绍了Vue通过代理和监听实现对数据的监控,从而进行响应式的渲染和更新操作。
然而变更数据时,具体又是如何进行渲染和更新的呢?本文将通过实现一个极简版的Vue响应式系统来一探究竟。
本文完整代码详见Vue数据响应式核心原理实现并附上效果预览链接
核心架构
Vue通过代理和监听实现对数据的监控,从而进行响应式的渲染和更新操作。而在变更数据时,组件的渲染和更新则是以Dep为核心,通过发布订阅模式实现的,如上图所示。
发布订阅模式
这在我们日常生活中十分常见,就以订报纸为例,我们订报纸付钱以后,邮递员会在本子上记录我们的信息(要调用的对象)和要订阅的报纸(需要执行的操作),之后每次有新的报纸(数据改变),他就按本子上的地址(找到要调用的对象),挨家挨户送报纸(执行响应的操作)。这就是发布订阅模式。
在Vue中Dep对象就是邮递员的小本,而watcher对象就是用户,watcher对象上有响应的方法用来重新渲染。
具体实现
dep
实现依赖类Dep:发布订阅模式 Dep实例通过一个去重数组set记录依赖数据的组件(set实际保存的是组件的watcher对象),采用发布订阅模式,在数据被访问时使用depend()方法添加依赖,在数据被修改时使用notify()方法在改变时遍历所有依赖组件,并执行组件watcher的update方法,即执行渲染函数重新渲染。Dep类的静态变量Dep.target记录当前正在执行的组件。
class Dep {
// 需要额外配置webpack进行转译
// static target = null
constructor() {
this.deps = new Set()
console.log(this.deps)
}
depend() {
//并非初始化组建时,Dep.target为null
if(Dep.target)
this.deps.add(Dep.target)
console.log("depend:",this.deps)
}
notify() {
for (const watcher of this.deps) {
console.log("watcher:",watcher)
watcher.update()
}
}
}
Dep.target = null
export {Dep}
targetStack
每个组件都有一个watcher对象,全局变量targetStack是一个运行栈,记录未完成加载的组件(父组件),使用Dep.target存储当前watcher,表示当前运行的组件。使用两个函数维护运行栈,push和pop,作用更新targetStack以及更新Dep.target。(targetStack在组件嵌套时体现作用)
import {Dep} from "./dep.js"
const targetStack = []
function popTarget() {
Dep.target = targetStack.pop()
}
function pushTarget(target) {
// 不会为空,不用额外检测
// if (Dep.target)
targetStack.push(Dep.target)
Dep.target = target
}
export {targetStack, popTarget, pushTarget}
watcher
watcher对象,保存当前组件渲染函数render,该对象有一个get方法,首先pushTarget,将当前组件设置为运行组件,然后调用数据render方法进行渲染,最后popTarget。在更新数据时,调用update方法,此处简化update方法内部仍是调用get方法。
import {popTarget, pushTarget} from "./targetStack"
class Watcher {
constructor(render) {
//组件渲染函数
this.render = render
this.get()
}
get() {
pushTarget(this)
this.render()
popTarget()
}
update() {
this.get()
}
}
export {Watcher}
reactive方法实现数据响应式核心逻辑
对每个数据,通过setter和getter覆盖原先的属性,实现监听,并且使用Dep记录依赖该数据的组件,在get时记录(第一次渲染时记录),在set时,即改变数据时,通知依赖该数据的组件,触发重新执行渲染函数。
import {Dep} from "./dep"
function isObject(data) {
return data != null && typeof data == "object";
}
function defineReactive(data, key) {
const dep = new Dep()
let value = data[key]
//用getter/setter覆盖原属性实现监听
Object.defineProperty(data, key, {
get() {
dep.depend()
return value
},
set(newVal) {
value = newVal
dep.notify()
}
})
//如果值是对象,则递归调用reactive使其内部数据均为响应式数据
if (isObject(value)) {
reactive(value)
}
}
function reactive(data) {
if(isObject(data)){
for (let key of Object.keys(data)) {
defineReactive(data, key)
}
}
return data
}
export {reactive}
测试接口
将data暴露给window.data,可以通过控制台修改对象和数组,查看数据响应式效果,测试各个接口是否正常工作。
...
<h1>data</h1>
<h2>data已暴露给window.data,可以通过控制台修改对象和数组,查看数据响应式效果</h2>
<label>
data.msg
<input type="text" id="msg">
</label>
<div id="app"></div>
...
import {reactive} from "../src/reactive"
import {Watcher} from "../src/watcher"
const data = reactive({
msg: "Hello World",
arr: [1, 2, 3],
obj:{name:'howard'}
})
new Watcher(() => {
document.getElementById("app").innerHTML = `
msg is ${data.msg}
arr is ${data.arr}
obj is ${JSON.stringify(data.obj)}
<br>
`
})
const input = document.getElementById("msg")
input.addEventListener("keyup", (event) => {
console.log(event.target.value)
data.msg = event.target.value
})
window.data = data
封装为极简版Vue
最后为了让我们的数据响应式系统和Vue有类似的使用方式和体验,我们对接口进行封装,形成极简版的Vue。
注意,这里实现Vue类,对接口进行封装,模仿Vue2语法,其中template使用正则替换简化实现,未实现真正的编译。
import {reactive} from "./reactive"
import {Watcher} from "./watcher"
class Vue {
constructor({el, data, template}) {
console.log(el)
console.log(data)
console.log(template)
this.el = el
let val = reactive(data)
this.template = template
//代理data
for (let key of Object.keys(data)) {
console.log(key)
Object.defineProperty(this, key, {
get() {
console.log("proxy")
return val[key]
},
set(newVal) {
console.log(newVal)
val[key] = newVal
}
})
// console.log(this[key])
}
return this
}
render() {
//编译器的简化,目的是能够使用template
let str = this.template
const reg = /{{([a-z]+)}}/g
const matches = [...this.template.matchAll(reg)]
console.log(matches)
for (let match of matches) {
str = str.replace(match[0], JSON.stringify(this[match[1]]))
}
document.getElementById(this.el).innerHTML = str
}
$mount() {
new Watcher(this.render.bind(this))
return this
}
}
export {Vue}