zoukankan      html  css  js  c++  java
  • Vue$watch()源码分析

      这一段时间工作上不是很忙,所以让我有足够的时间来研究一下VueJs还是比较开心的 (只要不加班怎么都开心),说到VueJs总是让人想到双向绑定,MVVM,模块化,等牛逼酷炫的名词,而通过近期的学习我也是发现了Vue一个很神奇的方法$watch,第一次尝试了下,让我十分好奇这是怎么实现的,

    为什么变量赋值也会也会触发回调?这背后又有什么奇淫巧技?怀着各种问题,我看到了一位大牛,杨川宝的文章,但是我还是比较愚笨,看了三四遍,依然心存疑惑,最终在杨大牛的GitHub又看了许久,终于有了眉目,本篇末尾,我会给上链接

      在正式介绍$watch方法之前,我有必要先介绍一下实现基本的$watch方法所需要的知识点,并简单介绍一下方便理解:

        1)   Object.defineProperty ( obj, key , option) 方法

            这是一个非常神奇的方法,同样也是$watch以及实现双向绑定的关键

            总共参数有三个,其中option中包括  set(fn), get(fn), enumerable(boolean), configurable(boolean)

            set会在obj的属性被修改的时候触发,而get是在属性被获取的时候触发,(其实属性的每次赋值,每次取值,都是调用了函数

        2)  Es6 知识,例如Class,()=>, const, let,这些也都比较基础,但是如果不知道的话,还是还是推荐了解一下;

        3)  面向对象编程,例如Object.keys,constructor,call,以及各种花式this指向,不过这些方法,也不是特别难理解,稍加搜索,OK的。

        下面简单介绍一下$watch方法的使用

          其使用方法如下:

      

     1  //----/VUE.JS
     2           const v = new Vue({
     3             data:{
     4               a:1,
     5               b:{
     6                 c:3
     7               }
     8             }
     9           })
    10           // 实例方法$watch,监听属性"a"
    11           v.$watch("a",()=>console.log("你修改了a"))
    12                         //当Vue实例上的a变化时$watch的回调
    13           setTimeout(()=>{
    14             v.a = 2
    15             // 设置定时器,修改a
    16           },1000)

         

        怎么样?是不是很简单,而且很有用?下面我来简单的实现一下$watch这个方法;

        从实例化Vue对象开始,到调用$watch方法,再到属性变化,触发回调,我分为三个阶段

        首先第一个阶段

        new Vue(options)

       想要实现watch,当实例化Vue对象的时候,有下面三个函数,需要被调用

       

     1 class Vue { //Vue对象
     2     constructor (options) {
     3       this.$options=options;
     4       let data = this._data=this.$options.data;
     5       Object.keys(data).forEach(key=>this._proxy(key));
     6       // 拿到data之后,我们循环data里的所有属性,都传入代理函数中
     7       observe(data,this);
     8     }
     9     $watch(expOrFn, cb, options){  //监听赋值方法
    10       new Watcher(this, expOrFn, cb);
    11       // 传入的是Vue对象
    12     }
    13 
    14     _proxy(key) { //代理赋值方法
    15       // 当未开启监听的时候,属性的赋值使用的是代理赋值的方法
    16       // 而其主要的作用,是当我们访问Vue.a的时候,也就是Vue实例的属性时,我们返回的是Vue.data.a的属性而不是Vue实例上的属性
    17       var self = this
    18       Object.defineProperty(self, key, {
    19         configurable: true,
    20         enumerable: true,
    21         get: function proxyGetter () {
    22           return self._data[key]
    23           // 返回 Vue实例上data的对应属性值
    24         },
    25         set: function proxySetter (val) {
    26           self._data[key] = val
    27         }
    28       })
    29     }
    30   }

        以上是一个Class Vue ,这个Vue类身上有三个方法,分别是constructor (实例化默认方法),$watch(也就是我们今天要实现的方法)和一个_proxy(代理)方法

        constructor

            当Vue被实例化,并传入参数(options)的时候,constructor 就会被调用,并且接收Vue实例化的参数options,这个函数在这里做的事情,就是,对传进来的data进行加工 第一步做的,就是让Vue对象,和参数data,产生一个关联,好让你可以通过,this.a , 或者vm.a 来操作data属性,建立关联之后,循环data的所有键名,将其传入到_proxy方法,到这里constructor方法的主要作用就结束了,什么?你说还有observe方法?这个属于另一个方向,稍后会做出解释;

        $watch

          这个方法你一看就会明白,只是实例化Watcher对象,而Watcher对象里还有其他的什么方法,稍后我会介绍;

        _proxy

          这个方法是一个代理方法,接收一个键名,作用的对象是Vue对象,具体的作用嘛,不知道大家有没有想过,这个:    

    1 //首先我们实例化Vue对象
    2 var vm = Vue({
    3    data:{
    4        a:1, 
    5        msg:'今天学习watch'      
    6      } 
    7 })
    8 console.log(vm.msg) //打印 '今天学习watch'
    9 // 理论上来说,msg和a,应该是data上的属性,但是却可以通过vm.msg直接拿到

        原因就在于,_proxy 这个方法身上,我们可以看到defineProperty方法作用的对象,是self,也就是Vue对象,而get方法里,return出来的却是self._data[key], _data在上面的方法当中,已经和参数data相等了,所以当我们访问Vue.a的时候,get方法返回给我们的,是Vue._data.a。

        但是,表面上,他只是为了修改取值和赋值的方法,而且就算我用Vue.data.a取值,又能怎么样?但是实际上,叫它代理方法,不是没有原因的,当watch事件开启了监听属性的时候,变量的set和get方法当然是有watch来控制,毕竟人家还有回调要搞嘛,但是,当我不需要watch的时候,怎么办呢?

    那当然就是这个代理函数加上的set和get方法来代理没有watch时候的取值和赋值的方法啦。

       下面我来说一下,刚才漏掉的,opserve(data,this)

       当我们在new Vue的时候,传进去的data很可能包括子对象,例如在使用Vue.data.a = {a1:1 , a2:2 }的时候,这种情况是十分常见的,但是刚才的_proxy函数只是循环遍历了key,如果我们要给对象的子对象增加set和get方法的时候,最好的方法就是递归;

       方法也很简单,如果有属性值 == object,那么久把他的属性值拿出来,遍历一次,如果还有,继续遍历,代码如下:

        

     1      class  Observer{  //对象Observer
     2             constructor(value) {//value 就是Vue实例上的data
     3               this.value = value
     4               this.dep = new Dep()
     5               //Dep对象是联络Watcher对象和触发监听回调的对象,稍后会有描述
     6               this.walk(value)
     7             }
     8             //递归。。让每个字属性可以observe
     9             walk(value){
    10               Object.keys(value).forEach(key=>this.convert(key,value[key]))
    11             }
    12             convert(key, val){ //这里的 key value 是Vue实例data的每个键值对
    13               defineReactive(this.value, key, val)//this.value 就是Vue实例的data
    14             }
    15           }
    16 
    17 
    18 
    19 
    20           function defineReactive (obj, key, val) {//类似_proxy方法,循环增加set和get方法,只不过增加了Dep对象和递归的方法
    var dep = new Dep()
    21 var childOb = observe(val) 22 //这里的val已经是第一次传入的对象所包含的属性或者对象,会在observe进行筛选,决定是否继续递归 23 Object.defineProperty(obj, key, {//这个defineProperty方法,作用对象是每次递归传入的对象,会在Observer对象中进行分化 24 enumerable: true, 25 configurable: true, 26 get: ()=>{ 27 if(Dep.target){//这里判断是否开启监听模式(调用watch) 28 dep.addSub(Dep.target)//调用了,则增加一个Watcher对象 29 } 30 return val//没有启用监听,返回正常应该返回val 31 }, 32 set:newVal=> {var value = val 33 if (newVal === value) {//新值和旧值相同的话,return 34 return 35 } 36 val = newVal 37 childOb = observe(newVal) 38           //这里增加observe方法的原因是,当我们给属性赋的值也是对象的时候,同样要递归增加set和get方法 39 dep.notify() 40           //这个方法是告诉watch,你该行动了 41 } 42 }) 43 } 44       function observe (value, vm) {//递归控制函数 45        if (!value || typeof value !== 'object') {//这里判断是否为对象,如果不是对象,说明不需要继续递归 46 return 47 } 48 return new Observer(value)//递归 49 }

         这里写的比较乱(不会写注释啊!!),不过没关系,你只需要知道,Opserver对象是使用defineReactive方法循环给参数value设置set和get方法,同时顺便调了observe方法做了一个递归判断,看看是否要从Opserver对象开始再来一遍。就是这样,至于dep对象,大可不必关心。

        到这里,new Vue所执行的阶段就告一段落,仍然留下了一些坑,例如Dep,不过马上就会展示出来

        因为Dep起到连接的作用,所以在new Watcher之前,有必要让你们看一下:

        

     1       class Dep {
     2              constructor() {
     3               this.subs = []  //Watcher队列数组
     4              }
     5              addSub(sub){
     6                this.subs.push(sub) //增加一个Watcher
     7              }
     8             notify(){
     9                this.subs.forEach(sub=>sub.update()) //触发Watcher身上的update回调(也就是你传进来的回调)
    10              }
    11       }
    12       Dep.target = null //增加一个空的target,用来存放Watcher

          

        new Watcher

      Dep对象身上的方法和作用,大体在上面的注释写的比较清楚,很多涉及到Watcher对象,那么下面我就来介绍一下Watcher对象,在开始的时候,我们已经知道,Vue对象身上的一个方法,$watch,而这个方法做的事情也不是别的,正是new Watcher对象,那么上代码:

      

     1           //-----WATCHER
     2           class Watcher { // 当使用了$watch 方法之后,不管有没有监听,或者触发监听,都会执行以下方法
     3             constructor(vm, expOrFn, cb) {
     4               this.cb = cb  //调用$watch时候传进来的回调
     5               this.vm = vm
     6               this.expOrFn = expOrFn //这里的expOrFn是你要监听的属性或方法也就是$watch方法的第一个参数(为了简单起见,我们这里不考虑方法,只考虑单个属性的监听)
     7               this.value = this.get()//调用自己的get方法,并拿到返回值
     8             }
     9             update(){  // 还记得Dep.notify方法里循环的update么?
    10               this.run()
    11             }
    12             run(){//这个方法并不是实例化Watcher的时候执行的,而是监听的变量变化的时候才执行的
    13               const  value = this.get()
    14               if(value !==this.value){
    15                 this.value = value
    16                 this.cb.call(this.vm)//触发你传进来的回调函数,call的作用,我就不说了
    17               }
    18             }
    19 get(){ 20 Dep.target = this //将Dep身上的target 赋值为Watcher对象 21 const value = this.vm._data[this.expOrFn];//这里拿到你要监听的值,在变化之前的数值 22 // 声明value,使用this.vm._data进行赋值,并且触发_data[a]的get事件 23 Dep.target = null 24 return value 25 } 26 }

          class Watcher在实例化的时候,重点在于get方法,我们来分析一下,get方法首先把Watcher对象赋值给Dep.target,随后又有一个赋值,

      const value = this.vm._data[this.exOrFn], 这个赋值的过程,是一个关键点,要知道,我们之前所做的都是什么,不就是修改了Vue对象的data(_data)的所有属性的get和set事件么? 而Vue对象也作为第一个参数,传给了Watcher对象,此时此刻,这个this.vm._data里的所有属性,在取值的时候,都会触发之前增加的get方法,此时,我们再来看一下get方法是什么?

     1       get: ()=>{
     2                 if(Dep.target){ //触发这个get事件之前,我们刚刚对Dep.target赋值为Watcher对象
     3                   dep.addSub(Dep.target)//这里会把我们刚赋值的Dep.target(也就是Watcher对象)添加到监听队列里
     4                 }
     5                 return val
     6               } 7           }

          在吧Watcher对象放再Dep.subs数组中之后,new Watcher对象所执行的任务就告一段落,此时我们有:

          Dep.subs数组中,已经添加了一个Watcher对象,

          Dep对象身上有notify方法,来触发subs队列中的Watcher的update方法,

          Watcher对象身上有update方法可以调用run方法触发最终我们传进去的回调,

         可是你会觉得,虽然方法都这么齐全,所谓万事具备只欠东风,那么如何触发Dep.notify方法,来层层回调,找到Watcher的run呢?

         答案就在set方法中的最后一行  

     1               set:newVal=> {                 
    2          var value = val 3 if (newVal === value) { 4 return 5 } 6 val = newVal 7 childOb = observe(newVal) 8 dep.notify()//触发Dep.subs中所有Watcher.update方法
    9          
    }

          别问我set方法怎么触发,当然是你修改了你所监听的那个值的时候啦,

         到这里,就是我对简易版的$watch方法的理解,不过终归是简易版,除了回味代码的思路,更感慨尤大的水平之高,在写博客的时候,也没有太好的思路,不知道怎么写更容易懂(反正也没人看),可能你看了这篇博客之后,仍然对$watch的实现还有问题,当然欢迎指正和提问,虽然是Vue1的实现方法,但是思路是不会过时的,另外推荐,杨川宝大大的文章和GitHub

       文章地址 https://segmentfault.com/a/1190000004384515

       GitHub  https://github.com/georgebbbb...

         

        以上

      2017-04-23    击鼓卖糖

  • 相关阅读:
    HTML5之画布的拖拽/拖放
    HDU 4028 The time of a day STL 模拟题
    java 使用htmlunit模拟登录爬取新浪微博页面
    【java.lang.UnsupportedClassVersionError】版本不一致出错
    Unsupported major.minor version 52.0
    java.lang.NoClassDefFoundError: org/w3c/dom/ElementTraversal问题解决
    htmlunit抓取js执行后的网页源码
    Maven添加本地依赖
    htmlunit爬取js异步加载后的页面
    HtmlUnit爬取Ajax动态生成的网页以及自动调用页面javascript函数
  • 原文地址:https://www.cnblogs.com/dongwy/p/6749984.html
Copyright © 2011-2022 走看看