Vue2核心原理(简易版)-依赖收集

1,574 阅读5分钟

Vue2核心原理(简易版)-依赖收集

Vue的响应式数据原理

  • 如果你仔细的阅读官方文档,会发现这么一句话:

    Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

    这句话看似很牛逼,但其实什么也没有说😓。当你修改它们时,视图会进行更新。但是你应该对这句话很敏感,可是怎么更新啊?哈哈,仔细思考一下,前两节课(前两篇文章响应式模版编译)的内容是不是帮我们实现了以下两点:

    1. 我们通过对数据的观测(observe),知道了数据是什么时候修改的,也就是什么时候应该更新了。
    2. 视图通过模版编译的方式去更新dom节点。
  • 那么如果让你说一下对响应式数据的理解呢?
    注意,这里并不仅仅指的是Vue.js中的响应式数据。

    我个人认为:数据在产生变化(读、写)过程中,能够通知到我们它的行为,并且让我们有针对这些行为做出一系列的应对的能力,这样的数据被称为响应式数据。
    好,那么我们现在回到Vue.js,怎样让具备在数据变化时有对其进行反应的能力?其实质就是核心的defineReactive,落实到js里面就是Object.defineProperty。再者,什么是通知呢?在上一篇完成的代码里面,我们去尝试改变data模型里面的数据:

    <body>
      <p>p1 - {{name}}</p>
    </body>
    <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return {
          name: 'vue',
        }
      }
    })
    
    setTimeout(() => {
      vm.name = 'vue3'
    }, 1000)
    </script>
    

    结果: 哈哈,是不是什么也没发生!不,其实我们已经知道了name从'vue'变成了'vue3',但是我们没有去通知我们的_render函数,需要重新render啦(update)。
    由此一来,便有了我们今天的话题,依赖收集!

Vue的响应式数据依赖收集

更新视图

为了调用render函数,更新视图,这是我们之前的做法:

// 更新函数 数据变化后 会再次调用此函数
let updateComponent = () => {
    // 调用render函数,生成虚拟dom
    vm._update(vm._render()); // 后续更新可以调用updateComponent方法
    // 用虚拟dom 生成真实dom
}
updateComponent();

仔细推敲不难发现,因为我们仅仅是在mount阶段调用了一次updateComponent,所以我们之后对data数据进行的各种修改,当然不会重新触发render,视图也就不会更新啦。那么我们是不是需要有一种方法,可以自动的帮我们完成这些操作!

观察者模式

lěi le lěi le:

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。

什么意思呢?在Vue.js里,也就是说,给每一个组件页面添加一个观察者,给我们的data里的每一个需要侦听的目标都添加上这个观察者的实例,让这个观察者存储所有的依赖。这样,当依赖发生改变行为时,就可以通知(notify)给观察者。

a. 依赖的数据是观察目标
b. 视图、计算属性、侦听器这些是观察者

观察者模式的实现

我们注意到上面的那张图所描述的,render => "touch" => data.getter => Collect as Dependency。这句话的意思是,在初始阶段,把需要用到的响应式数据收集成为当前组件的依赖。这就是我们所说的依赖收集。所以到了这一步,你应该知道我们为什么要做依赖收集了把?只有依赖收集了,才能对应到那个观察者,才可以实时调用render函数,更新视图
那么, 具体该怎么做呢?

// updateComponent();
// 观察者模式: 属性是“被观察者”  刷新页面:“观察者”
new Watcher(vm,updateComponent,()=>{
    console.log('更新视图了')
},true); // 他是一个渲染watcher  后续有其他的watcher

首先就是将我们之前手动执行的updateComponent,换成是新建当前页面的Watcher实例。也就是把这个页面对应到一个观察者。

其次,每一个被模版依赖的对象/属性,都应该去订阅当前观察者。

// Object.defineProperty
 get(){
    // 取值时我希望将watcher和dep 对应起来
    if(Dep.target){ // 此值是在模板中取值的
        dep.depend() // 让dep记住watcher
        // 可能是数组 可能是对象,对象也要收集依赖,
        // 后续写$set方法时需要触发他自己的更新操作
        if(childOb){ 
            // 就是让数组和对象也记录watcher
            childOb.dep.depend(); 
            //取外层数组要将数组里面的也进行依赖收集
            if(Array.isArray(value)){ 
                dependArray(value);
            }
        }
    }
    return value
},

而当前观察者,也要反向记录自己的依赖项。

depend(){
  // Dep.target  
  // dep里要存放这个watcher,watcher要存放dep 
  if(Dep.target){
    Dep.target.addDep(this);
  }
}

此时,如果依赖项发生改变,我们让它通知它的观察者们,作出反应。也就是重新render组件。

// observer/index.js => Object.defineProperty
set(newV){ 
  if(newV !== value){
    // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
    observe(newV); 
    value = newV;
    // 告诉当前的属性存放的watcher执行
    dep.notify(); 
  }
}

// observer/dep.js => notify
notify(){
  this.subs.forEach(watcher=>watcher.update());
}

// observer/watcher.js => update
update(){ // vue中的更新操作是异步的
 // 每次更新时 this
 // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
 queueWatcher(this); 
}    

详尽的流程我画了一个思维导图和一张流程图,请仔细阅读:

完 🎉

下一讲,异步更新原理