Open vaakian opened 2 years ago
以下例子当price
被修改时,会输出信息。
watcher(() => {
console.log('detected price change: %s', data.price)
})
let app = ''
function appReRender() {
app = `
<div>${data.price}</div>
<div>${total}</div>
<div>${salePrice}</div>
`
// 然后渲染到页面上
}
watcher(appReRender)
data.price = 5
console.log(app)
这样实现了一个简易的,当状态改变,则页面也会改变。
当然这种简单的字符串拼接方式粒度太粗,任何数据更新就是整个页面的重构过程。Vue
比这个复杂的多。
粗粒度更新版mini vue
class Dep {
constructor() {
this.subscribers = []
}
depend(vm) {
if (vm.notifer && !this.subscribers.includes(vm.notifer)) {
this.subscribers.push(vm.notifer)
}
}
notify() {
this.subscribers.forEach(sub => sub())
}
}
function Vue(options) {
const data = options.data
const vm = this
Object.keys(data).forEach(key => {
let internalValue = data[key]
vm[key] = internalValue
// dep属于这个key
const dep = new Dep()
// 把this所对应的所有data属性代理
Object.defineProperty(vm, key, {
get() {
dep.depend(vm)
return internalValue
},
set(newVal) {
internalValue = newVal
dep.notify()
}
})
})
function watcher(notifer) {
// 存
vm.notifer = notifer
// 记录依赖
vm.notifer()
// 清
vm.notifer = null
}
const computed = options.computed
for (let key of Object.keys(computed)) {
watcher(() => {
// computed[key] 访问了哪个属性,就自动收集到了依赖
vm[key] = computed[key].apply(vm)
})
}
// 收集模板上的依赖
watcher(options.render.bind(vm))
}
Vue.prototype.$mount = function (el) {
// 把模板渲染到页面上
console.log(this)
this.$el = document.querySelector(el)
this.$el.innerHTML = this.template
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>miniVue</title>
</head>
<body>
<div id="app">
</div>
</body>
<script src="vue.js"></script>
<script>
window.onload = function () {
window.vm = new Vue({
render() {
this.template = `
<div>price: ${this.price}</div>
<div>quantity: ${this.quantity}</div>
<div>total: ${this.total}</div>
`
if (this.$el) {
this.$el.innerHTML = this.template
}
// 当data改变,就会触发render
return this.template
},
data: {
price: 25,
quantity: 9
},
computed: {
total() {
return this.price * this.quantity
}
}
})
vm.$mount('#app')
}
</script>
</html>
通过以上的例子,computed属性不是reactive的,所以直接更改computed属性不会有任何效果。仅仅computed对应的是依赖变化了,才会产生新的vdom->diff->patch->render?
12-22日 周三 15:30
通过proxy重写miniVue
// 全局的target函数,用于暂存依赖函数
let update = null
function watcher(hander) {
// 存:供depend访问
update = hander
// Record:执行依赖函数
update()
// 清
update = null
}
class Dep {
constructor() {
this.subs = []
}
depend(sub) {
if (sub && !this.subs.includes(sub)) this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub())
}
}
function Vue(options) {
const instance = this
// 初始化数据
for (let key in options.data) {
instance[key] = options.data[key]
}
// 初始化data/computed对应的dep依赖收集器
const _deps = {}
for (let key in options.data) {
_deps[key] = new Dep()
}
for (let key in options.computed) {
_deps[key] = new Dep()
}
// 代理实例的所有数据
const proxiedInstance = new Proxy(instance, {
get(target, key) {
// 保存依赖对应的更新器
_deps[key]?.depend(update)
return target[key]
},
set(target, key, value) {
let oldValue = target[key]
target[key] = value
// 通知key对应的依赖去更新:先修改,再通知。
_deps[key]?.notify()
// 通知watch
options.watch[key]?.call(instance, value, oldValue)
// 返回
return target[key]
}
})
// 初始化所有computed属性,且computed属性应该也要有dep
for (let key in options.computed) {
// watcher将函数执行一遍,触发set,然后进行依赖收集
watcher(() => {
proxiedInstance[key] = options.computed[key].call(proxiedInstance)
})
}
return proxiedInstance
}
const vm = new Vue({
data: {
price: 15,
quantity: 3,
discount: 0.74
},
computed: {
totalPrice() {
return this.price * this.quantity
},
salePrice() {
//totalPrice同样依赖与上一个计算属性。
// 那么totalPrice改变(price或者quantity改变)则会导致salePrice改变
return this.totalPrice * this.discount
}
},
watch: {
price(newVal, oldVal) {
console.log('price change detected: ', { newVal, oldVal, vm: this })
}
}
})
console.log(vm)
vm.price = 20
// console.log(vm.totalPrice)
修改price
,导致两个计算属性totalPrice/salePrice
发生改变。
修改discount
,仅导致salePrice
发生改变。
通过手写计算属性computed
和watch
方法,理解了两者的本质区别,computed
计算属性是惰性的、被缓存的,仅当依赖发生改变才会重新执行更新函数。而watch
方法每当该属性发生改变就触发执行函数。完全不可混为一谈。
TODO: 以上代码存在一个问题,仅仅是浅层代理,更深层次的代理需要进行递归处理。
收集依赖->监视getters
通知更新->监视setters
TODO: 实现vdom
的diff
比对,对真实页面进行patch
操作。
隐式依赖收集,即不需要像
react hooks
一样,需要明确写明回调函数的依赖收集方式。 通过一次初始化调用,造成watcher
内的回调函数访问data
,然后直接记录收集到该函数所有的访问记录,即依赖。也就是Vue所使用的依赖性收集方式。
代码示例:
例子中
total
,salePrice
也就是相当于Vue实例中的计算属性。可以看到,要实现隐式依赖收集,那么访问的函数必须要被先调用一次。 那么
Vue
中,watch
方法,并没有被先调用一次,那么是如何实现的依赖收集?当然是跟Vue的写法有关,如果要
watch
一个状态,那么函数名一定是和该变量名一样,所以相当于是一种显式依赖收集,不需要
先调用一次。