终于到了渲染watcher
,看完这篇文章的内容后,大家就可以实现一个响应式系统了,并且能够在页面上有所体现。
源码地址:gitee
系列文章:
- 基本原理
- 数组的处理
持续更新中。。。
Vue项目总结系列文章:
- 基础架构
- 登录与权限控制
持续更新中。。。
什么是渲染Watcher
vue
中有多种watcher
,我们之前实现的watcher
类似于Vue.$watch
,当依赖变化时执行回调函数。而渲染watcher
不需要回调函数,渲染watcher
接收一个渲染函数而不是依赖的表达式,当依赖发生变化时,自动执行渲染函数
new Watcher(app, renderFn)
那么如何做到依赖变化时重新执行渲染函数呢,我们要先对Watcher
的构造函数做一些改造
constructor(data, expOrFn, cb) {
this.data = data
// 修改
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb
this.value = this.get()
}
// parsePath的改造,返回一个函数
function parsePath(path) {
const segments = path.split('.')
return function (obj) {
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
}
这样,this.getter
就是一个取值函数了,get
修改
get() {
pushTarget(this)
const data = this.data
const value = this.getter.call(data, data) // 修改
popTarget()
return value
}
要想依赖变化时重新执行渲染函数,就要在派发更新阶段做一个更新,因此,update
方法也要进行修改:
update() {
// 重新执行get方法
const value = this.get()
// 渲染watcher的value是undefined,因为渲染函数没有返回值
// 因此value和this.value都是undefined,不会进入if
// 如果依赖是对象,要触发更新
if (value !== this.value || isObject(value)) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
function isObject(target) {
return typeof target === 'object' && target !== null
}
大家可能会有疑问了,为什么不能直接用this.getter.call(this.data)
来重新执行渲染函数呢,这就涉及到下文要提到的重新收集依赖了。但是在此之前,要先解决一个问题:依赖的重复收集
重复的依赖
看这样一个例子
<div>
{{ name }} -- {{ name }}
</div>
如果我们渲染这个模板,那么渲染watcher
就会依赖两次name
。因为解析该模板时,会读取两次name
的值,就会触发两次getter
,此时Dep.target
都是当前watcher
,在depend
方法中,
depend() {
if (Dep.target) {
dep.addSub(Dep.target)
}
}
依赖会被收集两次,name
变化时就会触发两次重新渲染。因此vue
采用了以下方式
首先为每个dep
添加一个id
let uid = 0
constructor() {
this.subs = []
this.id = uid++ // 增加
}
watcher
修改的地方比较多,首先为增加四个属性deps, depIds, newDeps, newDepIds
this.deps = [] // 存放上次求值时存储自己的dep
this.depIds = new Set() // 存放上次求值时存储自己的dep的id
this.newDeps = [] // 存放本次求值时存储自己的dep
this.newDepIds = new Set() // 存放本次求值时存储自己的dep的id
我们的思路是,当需要收集watcher
时,由watcher
来决定自己是否需要被dep
收集。在上面的例子中,假设对name
取值时,watcher
被dep1
收集,第二次对name
取值时,watcher
发现自己已经被dep1
收集过了,就不会重新收集一遍,代码如下
// dep.depend
depend() {
if (Dep.target) {
Dep.target.addDep(this) // 让watcher来决定自己是否被dep收集
}
}
// watcher.addDep
addDep(dep) {
const id = dep.id
// 如果本次求值过程中,自己没有被dep收集过则进入if
if (!this.newDepIds.has(id)) {
// watcher中记录收集自己的dp
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
现在解释一下最后一个if
,考虑重新渲染的情况:watcher
依赖name
,name
发生了变化,导致watcher
的get
方法执行,会重新对name
取值,进入addDep
方法时,newDepIds
是空的,因此会进入if
,来到最后一个if
,因为第一次取值时,dep
已经收集过watcher
了,所以不应该再添加一遍,这个if
就是这个作用。
再执行get
方法最后会清空newDeps,newDepIds
cleanUpDeps() {
// 交换depIds和newDepIds
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
// 清空newDepIds
this.newDepIds.clear()
// 交换deps和newDeps
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
// 清空newDeps
this.newDeps.length = 0
}
依赖的重新收集
我所理解的依赖重新收集包括两部分内容:收集新的依赖和删除无效依赖。其实收集新依赖再上面的代码中已经有所体现了,虽然前面的代码中对重复依赖做了很多判断,但是能够收集到依赖的基本前提是Dep.target
存在,从Watcher
的代码中可以看出,只有在get
方法执行过程中,Dep.target
是存在的,因此,我们在update
方法中使用了get
方法来重新触发渲染函数,而不是getter.call()
。并且重新收集依赖是必要的,比如使用了v-if
的情况,因此,现在的响应式系统比之前的固定依赖版本
又有了很大进步。
至于删除无效依赖部分,可以在cleanUpDeps
中添加如下代码
cleanUpDeps() {
// 增加
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
// ...
}
在求值结束(也就是依赖收集结束)后,如果本次求值过程中,发现有些dep
在上次求值时收集了自己,但是这次求值时没有收集自己,说明该数据已经不需要自己了,将自己从dep
中删除即可
// Dep.js
removeSub(sub) {
remove(this.subs, sub)
}
function remove(arr, item) {
if (!arr.length) return
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
这样,我们的响应式系统就比较完整了
总结
其实所谓的渲染watcher
和其他的watcher
区别不大,只是依赖变化时自动执行渲染函数而已,上文中提到的重复依赖的处理,依赖重新收集是通用的。
下一篇文章将会做一个简单的模板编译器,让我们的响应式系统与页面渲染相结合,并且会实现v-model
的双向绑定,请大家关注。
如果各位看官感觉文章还可以的话,就请点个赞吧!!!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!