阅读 116

Vue -- vue响应式原理

image.png 「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」。

前言

今天看了一篇关于vue响应式原理的文章,知识比较深入,虽然花了比较长的时间,但还是稍微的理解了一些其中的原理,赶紧写一篇文章分享一下我的理解,可能比较白话一点,比较适合新手观看,

什么是响应式原理

数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这是官方文档的解释,那官方文档确实太官方了,咱们就简单的理解一下,就是当一个数据发生改变时,与他相关的数据也会即刻做出变化,那这章就来讲讲响应式原理的底层逻辑,也就是具体是怎么实现的,

响应式的设计模式

响应式的设计模式其实就是基于观察者模式,观察者模式简单来说就是,你想去一个商店买一个东西,但是你不知道商店什么时候会有,正常情况你需要一遍遍去问,但是你想到一个好主意,你把电话给了老板,等到有货的时候,打电话给你,来通知你,这样你就不需要一遍遍问了,节省了你很多的时间,这里节省了你的时间就相当于节省了性能,

image.png 如果想具体了解的话可以转站 设计模式--观察者模式

vue包含的文件

image.png
一个基础vue需要包含这些文件,接下来来一一介绍他们的作用

1. vue.js

很明显vue.js一看就知道是vue的入口,每个程序都需要有一个入口,而vue的入口当然是vue.js,vue.js中有个_proxyData()方法,作用是劫持data中的所有数据,

2. compiler.js

一个处理文件,专门用来处理解析指令,差值表达式,等等的集合

3.dep.js

前面说vue响应式结合了观察者模式,这里就开始体现了,dep中存在一个存储容器,添加观察者的方法,和通知观察者的方法,也就是观察者模式中的被观察者,我前面举例中的商店
作用: 收集依赖,也就是watcher (dep中讲解)

4.observer.js

不要被他的名字误导了,认为他是观察者,其实他不是,他内部有个walk方法,遍历data给data中的每个数据添加get和set现在说感觉有点听不懂,没有关系,先大概有个认识,然后继续看

5.watcher.js

这个才是观察者模式中的观察者,包含接收更新通知的方法,调用了data数据的地方

具体代码

建议全部看一遍之后再来逐个文件理解,跟着文章中的指引观看

vue.js

class Vue {
    constructor (options) {
        this.$options = options || {} // save options
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el)
        :options.el // get dom
        this.$data = options.data // get data
        this.$methods = options.methods
        // 1.data 所有数据进行劫持代理
        this._proxyData(this.$data)
        // 2.调用observe对象,监听数据变化
        new Observer(this.$data)
        // 3.调用compiler对象,解析指令和差值表达式
        new Compiler(this)
    }
    _proxyData (data) {
        // 遍历所有data
        Object.keys(data).forEach(key => {
            // 将每一个data通过defineProperty进行劫持
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get () {
                    return data[key]
                },
                set (newValue) {
                    if (data[key] === newValue) {
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}
复制代码

进入第一个文件vue.js,我当时就遇到了问题,我废了好大劲,查了mdn把里面的方法看懂,比较陌生的在_proxyData方法中,接下来就来解释一下这两个函数Object.keysObject.defineProperty,我们先看看observer文件再来讲解

observer.js

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk (data) { // 循环执行data
        if (!data || typeof data !== 'object') {
            return
        }
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive (obj, key, val) { 
        let that = this
        this.walk(val) // 如果val是对象,则给他绑定get和set时触发的方法
        let dep = new Dep() // 负责收集依赖,并发送通知
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get() {
                Dep.target && dep.addSub(Dep.target) // 收集依赖
                return val // 如果使用obj[key],会变成死循环
            },
            set(newValue) {
                if (newValue === val) {
                    return
                }
                val = newValue
                that.walk(newValue) // 修改后可能是对象,set函数内部调用,修改了this指向
                dep.notify() // 发送通知
            }
        })
    }
}
复制代码

!!!必看
observer中有一个walk方法使用递归的方式,将data中的每个数据添加get和set,具体等看完就能豁然开朗,walk中调用了Object.keys()方法,先来看看这个方法的作用:看右边目录中的插曲下面有讲解两个方法是作用和用法,如果不懂的话先看完插曲再继续看代码

分析代码

  1. observer.js 首先使用递归将data中所有的存在对象的key全部拆解开,
  2. 使用Object.keys方法强制转换为对象,并存储key值,forEach遍历
  3. defineReactive中调用Object.defineProperty给每个数据添加get和set,用于监听数据变化,当调用了该数据就会执行get方法,赋值则执行set方法,
  4. let dep = new Dep() 为每个数据创建dep实例(dep就是收集watcer的) (watcher就是调用数据的,可以理解为视图调用了数据显示)当watcher调用数据,就会执行get方法Dep.target && dep.addSub(Dep.target)就会执行这个数据的dep实例中的addSub方法,将调用了该数据的watcher存起来,
  5. 当某个数据发生变化时,就会调用set方法,去调用这个数据的dep实例中的notify方法(也就是通知watcher更新),因为与这个数据有关的watcher已经再调用的时候被存放到dep中sub数组中了

为什么每次遍历都要生成一个dep实例?

这个我当时没仔细看不理解,理解的就不用看了
首先要知道dep中存放的是什么,他存放的是依赖对象,不是data这点要清楚,
每个data都会有自己各自的依赖对象,也就是watcher,所以每个data都需要一个dep实例来存放他们各自的依赖对象,所以要每个数据创建一个实例

dep.js

// 订阅者Dep,存放观察者对象
class Dep {
    constructor() {
      this.subs = []  //存放观察者
    }
    /*添加观察者对象*/
    addSub (sub) {
      this.subs.push(sub)
    }
    // 通知所有watcher对象更新视图
    notify () {
      this.subs.forEach((sub) => {
        sub.update()  // watcher的更行方法
      })
    }
}
复制代码

代码分析

  1. 这就是观察者模式中的被观察者
  2. 包含存放观察者的数组,添加观察者的方法,和通知观察者的方法
  3. dep在observer中被创建实例,并且是每个数据一个dep实例,为什么每个实例都需要一个dep实例呢? 每个数据都会有依赖对象,也就是需要使用这个数据的watcher,这就是依赖对象,dep中存放的也就是这些依赖对象

什么是收集依赖?

假如我是data,那我怎么收集依赖,谁需要使用我,那就是对我有依赖,那我就把他收集起来,那谁会依赖我呢,那就是下面的watcher,这么解释应该很清晰了,所以dep中数组存放的是watcher,而不是data

watcher.js

class Watcher {
  constructor (vm, key, cb) {
      this.vm = vm
      // data中的属性名称
      this.key = key
      // 回调函数负责更新视图
      this.cb = cb
      // 把watcher对象记录到Dep类的静态属性target
      Dep.target = this
      // 触发get方法,在get方法中会调用addSub
      this.oldValue = vm[key]
      Dep.target = null
  }
  // 当数据发生变化的时候通知视图更新
  update () {
      let newValue = this.vm[this.key]
      if (this.oldValue === newValue) {
          return
      }
      this.cb(newValue)
  }
}
复制代码

代码分析

  1. watcher对象在构造函数,构造时,将this也就是自己本身赋值给dep.target,
  2. 然后在observer中判断dep.target是否存在,存在就添加到响应的数据dep实例中
  3. watcher拥有updata方法,用来更新视图

compiler.js

// 解析 v-model
  modelUpdater (node, value, key) {
      node.value = value
      new Watcher(this.vm, key, (newValue) => { // 创建watcher对象,当数据改变更新视图
          node.value = newValue
      })
      // 双向绑定
      node.addEventListener('input', () => {
          this.vm[key] = node.value
      })
  } 
// 编译模板
  compile (el) {
      let childNodes = el.childNodes
      Array.from(childNodes).forEach(node => {
          if (this.isTextNode(node)) { // 处理文本节点
              this.compileText(node)
          }   else if(this.isElementNode(node)) { // 处理元素节点
              this.compileElement(node)
          }
          // 如果还有子节点,递归调用
          if (node.childNodes && node.childNodes.length > 0) {
              this.compile(node)
          }
      })
  }
  // 编译元素节点,处理指令
  compileElement (node) {
      // console.log(node.attributes)
      if (node.attributes.length) {
          Array.from(node.attributes).forEach(attr => { // 遍历所有元素节点
              let attrName = attr.name
              if (this.isDirective(attrName)) { // 判断是否是指令
                  attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) // 获取 v- 后面的值
                  let key = attr.value // 获取data名称
                  this.update(node, key, attrName)
              }
          })
      }
  }
// 编译文本节点,处理差值表达式
  compileText (node) {
      // 获取 {{  }} 中的值
      // console.dir(node) // console.dir => 转成对象形式
      let reg = /\{\{(.+?)\}\}/
      let value = node.textContent
      if (reg.test(value)) {
          let key = RegExp.$1.trim() // 返回匹配到的第一个字符串,去掉空格
          node.textContent = value.replace(reg, this.vm[key])
          new Watcher(this.vm, key, (newValue) => { // 创建watcher对象,当数据改变更新视图
              node.textContent = newValue
          })
      }
  }

复制代码

image.png

小结:

compile 把元素转换为数据模型,他是普通的 JavaScript 对象,我们这叫做 vnode 对象, >然后遍历 vnode 对象,根据标识分为元素节点,文本节点,数据三个分类,分别进入不同的处理函数,并且创建一个 Watcher 对象,然后在 Watcher 对象中触发 get 实现响应式,同步会进行 updata 更新数据,转换成真实 dom ,完成页面渲染,更新就是如此反复。



插曲

Object.keys

作用:将参数强制转化为对象,并将对象中的key抽出放入一个数组

举个栗子:

let obj = {name : "orange" , age : 20}  //一个对象    取出key值存放起来
Object.keys(obj)  // ["name","age"]
复制代码
let arr = [ "1", "2"]  //一个数组 ---> {0: "1", 1: "2"}  //强制转化为对象取出key值
Object.keys(obj)  // ["0","1"]
复制代码

这就是Object.keys的作用,相关文档

将data中的key存放到数组然后forEach遍历,所以为什么forEach中写的是key而不是item就是这个原因了 遍历的使用调用了defineReactive()函数,接下来,我们来看看这个函数,

又一次调用walk()方法,这就是递归,为什么要调用呢,因为如果data的key还有对象,就需要再次拆分,(这个我不确定我理解的对不对)直到if (!data || typeof data !== 'object') { return }这个时候结束递归,接着往下看,let dep = new Dep()这里new了一个dep的实例,然后调用了一个陌生的函数Object.defineProperty,我们来了解一下这个函数的作用

Object.defineProperty

作用:在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty的参数

Object.defineProperty(obj,prop,descriptor)
obj ==> 一个对象
prop ==> 需要添加或者修改的名称
descriptor ==> 要定义或修改的属性描述符

来看看代码,这是我自己写的一个例子,去验证一下Object.defineProperty中属性描述符的含义

function Person(){
  let name = 'a'
  Object.defineProperty(this,"name",{
    get(){
      return name + 1 //为了区别这里加个1
    },
    set(value){
      name = value 
    }
  })
}
const person = new Person()
console.log(person.name); //a1
person.name = "orange"
console.log(person.name); //orange1
复制代码

这里来讲一讲我对get和set的理解:

这里的this是指向Person这个对象,也就是function本身,因为第一个参数传入的要求也是个对象,所以满足,
第二个对象是"name",Person中有name属性,所以是修改name,如果没有name属性,则是添加,这个一会再说,
第三个参数是个对象,里面有get和set方法,但是这是Person实例调用的方法吗,其实不是,在Object.defineProperty中的get和set分别代码取值赋值,在你取值的时候就会调用get方法,赋值的时候会调用set方法,前提是取值和赋值的是第二个参数也就是"name"属性,,
当我第一次console.log也就是第一次取值,调用了get方法所以打印的是a1而不是a,在我赋值的时候调用了set方法,所以第二次取值的时候name变成了orange,所以打印的是orange1,这样就明白Object.defineProperty中set和get的具体作用了,这里就不展开讲数据描述符和存取描述符了,想了解更多可以看我另一篇方法介绍 或者 去官网查看相关用法

总结: 循环遍历data是为了给data中每个数据添加get和set,以至于监听每个数据的变化

本章完

文章分类
前端
文章标签