最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 0年前端的Vue响应式原理学习总结3:渲染watcher

    正文概述 掘金(争霸爱好者)   2021-02-25   637

    终于到了渲染watcher,看完这篇文章的内容后,大家就可以实现一个响应式系统了,并且能够在页面上有所体现。

    源码地址:gitee

    系列文章:

    1. 基本原理
    2. 数组的处理

    持续更新中。。。

    Vue项目总结系列文章:

    1. 基础架构
    2. 登录与权限控制

    持续更新中。。。

    什么是渲染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取值时,watcherdep1收集,第二次对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依赖namename发生了变化,导致watcherget方法执行,会重新对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的双向绑定,请大家关注。

    如果各位看官感觉文章还可以的话,就请点个赞吧!!!


    起源地下载网 » 0年前端的Vue响应式原理学习总结3:渲染watcher

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元