阅读 727

【Vue源码学习】深入理解watch的实现原理 —— Watcher的实现

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

秋招来袭,笔者最近正在学习Vue的源码,通过对源码的解读,结合常考的面试题,深入理解Vue的源码原理,并动手实现Vue的一些方法。该系列文章即为这项学习计划的总结和输出,后续将持续更新。。。

学习目标

  • 搞清楚watch的实现原理
  • 自己动手实现一个Watcher

知识储备

在理解实现原理之前,如果是初学者,可以先初步了解一下响应式原理

把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。 data.png 初步熟悉响应式原理后,下面开始Watcher的实现

Watcher 开造

step1:先用代码来看看我们想要达到的效果 —— 需求分析

开造之前,先来看看我们想要达到的效果是什么样子的:

let vm = new Watcher({
    data: {
      a: 0,
      b: 'hello'
    },
    watch: {
      a(newValue, oldValue) {
        console.log(newValue, oldValue);
      }
    }
  })
复制代码

如上述代码所示,在此次封装中,我们希望在创建一个Watcher实例后,通过对Watcher的data对象中属性进行修改,Watcher中的watch能够检测到数据的修改,并且对应属性名的函数会触发执行。这既是响应式原理的最简化理解。

step2:废话不说,直接动手

搞清楚了需求后,直接开始动手:

  • 1、我们先把架子搭好,创建一个Watcher类
  • 2、如需求分析中的代码,既然想要检测到数据的改变,必须得有自己的数据源 和 放方法声明的对象,所以在构造器中,需要放上一个data对象 和 watch对象
  • 3、根据前面说的响应式原理,接下来我们就需要对传入的data对象进行遍历,对该对象的所有property使用Object.defineProperty方法进行数据劫持,将这些property全部转为getter/setter,并经此实现追踪。
class Watcher{
    constructor(opts){
        //这里我们封装了一个函数getBaseType用于传入的参数是否是'Object'类型
        this.$data = this.getBaseType(opts.data) === 'Object' ? opts.data : {}
        this.$watch = this.getBaseType(opts.watch) === 'Object' ? opts.watch : {}
        for(let key in opts.data){
            //这里通过for...in..拿到data对象的每一个key,并将对每一项要进行的操作统一封装到setData函数中
            this.setData(key)
        }
        
    }
}
复制代码

接下来我们来分别打造getBaseType方法 和 setData方法:

  • getBaseType方法:主要用来做类型判断
getBaseType(target){
         调用Object原型上的toString方法可以用来判断类型,再用call将this指向target,从而实现类型的判断
         const typeStr = Object.prototype.toString.call(target)
         return typeStr.slice(8,-1)
     }
复制代码

注:关于Object.prototype.toString.call(obj)方法判断数据类型,为了避免造成逻辑混乱,这里只是简单阐述,若不理解可以看这篇文章: blog.csdn.net/hanyanshuo/…

  • setData方法
setData(_key){
    //使用Object.defineProperty对data对象的每一项进行数据捆绑,通过getter/setter来追踪数据的改变
    Object.defineProperty(this,_key,{
        get:function(){
            return this.$data[_key]
        },
        set:function(val){
            const oldVal = this.$data[_key]
            if(oldVal === val) return val
            this.$data[_key] = val
            this.$watch[_key] && this.getBaseType(this.$watch[_key]) === 'Function' && (
            this.$watch[_key].call(this , val , oldVal)
            )
        }
    })
}
复制代码

这一段的逻辑,简单的来说其实就是Object.defineProperty的语法糖,实现的核心还是依赖该方法。(建议深入了解该方法)

Object.defineProperty接收三个参数:

 1. 要定义属性的对象
 2. 要定义或修改的属性
 3. 要定义或修改的属性描述符
复制代码

在上述代码中,按道理来讲,Object.defineProperty的第一个参数应该接收的参数为:this.$data,而这里接收的是this,那么这个this指向谁呢?

其实这个this指向的就是this.$data,这是因为该方法第一个参数遇到this 就会将上下文指向当前的对象。而在上述代码中,this指向的就是Watcher,而当前对象是this.$data。所以这样写相当于绑定了this.$data对象。

当对data的每一个属性调用setData方法后,只要对任意属性进行读取操作,就会走get回调,读取到data中对应属性的值;如果对任意属性进行修改操作,就会触发set回调,set回调中的操作如下:

  1. 先保存data对象中的oldValue
  2. 优化:如果传入回调的newValue 等于 oldValue,则不进行任何操作
  3. 再保存传入回调的newValue
  4. 进行判断:如果watch有被改变属性名的同名属性 -> 如果该同名属性是一个方法时 -> 将oldValue,newValue传入该方法并调用(这里使用了一个call将该同名方法指向watch)。

完整代码

class Watcher {
    constructor(opts) {
      this.$data = this.getBaseType(opts.data) === 'Object' ? opts.data : {}
      this.$watch = this.getBaseType(opts.watch) === 'Object' ? opts.watch : {}
      for(let key in opts.data) { // 拿到data对象里面的每一个key   
         // Object.keys(opts.data).forEach()
          this.setData(key)
      }
    }
    getBaseType(target) {
      const typeStr = Object.prototype.toString.call(target) // "[Object string]"
      return typeStr.slice(8, -1)
    }
  
    setData(_key) {
      // this.$data = this
      Object.defineProperty(this, _key, { // Object.defineProperty(this)会把上下文指向当前的对象
        get: function() {
          return this.$data[_key]
        },
        set: function(val) {
          const oldVal = this.$data[_key]
          if (oldVal === val) return val
          this.$data[_key] = val
          this.$watch[_key] && this.getBaseType(this.$watch[_key]) === 'Function' && (
          this.$watch[_key].call(this, val, oldVal)
          )
        }
      }) 
    }
  }
  
复制代码

总结

在本文中Watcher的实现思路主要是以下两步:

  1. 初始化一个data对象,一个watch对象。并使用getBaseType对传入的实参进行类型判断
  2. 通过for...in对data的每一个属性key调用setData(也就是通过Object.defineProperty进行数据绑定)

难点:在Object.defineProperty中的操作可能对于初学的朋友不太友好,这里总结一下:

data被绑定后

  • 读取属性值:通过get回调返回data对象中的属性值
  • 修改属性值,会触发set回调,set中的逻辑总结如下:
  1. 如果newValue 和 oldVaue 完全相等,不进行任何操作
  2. 保存oldValue,newValue
  3. 在watch对象中存在对应属性名的同名方法的前提下,将第二步的两个参数传入 -> 将该方法的this指向Watcher类 -> 调用该方法

如果对此文的部分方法不熟悉,温馨链接如下:

结语

出于以文章作为平时学习的输出和技术分享的初衷,力求以简短的篇幅,输出平日所学,分享给同样即将面对面试官拷问的朋友们。这篇文章只是Vue源码系列的开始,后续会持续更新该系列,还会输出一些必问面试题的总结,欢迎大家持续关注,和笔者共同进步!!!

这里就是本文的结尾了,希望笔者的分享能够帮你解决一些困惑,如果有不清楚的地方或者文章的错误也欢迎各位在评论区讨论和指出。。。

文章分类
前端
文章标签