学习Vue响应式原理,实现一个小“框架”(2):实现Vue2.0响应式__Vue.js
发布于 3 年前 作者 banyungong 1037 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

这个同样是写博客的产出,之前在写博客的时候,瞎弄了一个模板引擎,当时就想之后把他优化一下,把 Vue 响应式原理放进去,弄成一个小的可以用来减少我写博客前端代码的组件化的”框架“,于是我就来填坑了。这个只是自己用来学习 Vue2.0响应式原理的一个小项目,代码的实现可能很初级,有些地方写的会比较差,请见谅。这篇文章接着上篇,说下响应式的实现。

项目源码:传送门 实现效果:我的博客就是用这个小框架改造的

什么是MVVM模式[1][2]?

Model-View-ViewModel就是将其中的 View 的状态和行为抽象化,让我们可以将 UI 和业务逻辑分开。当然这些工作 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

MVVM模式是通过以下三个核心组件组成,每个都有它自己独特的角色:

  • Model - 包含了业务和验证逻辑的数据模型
  • View - 定义屏幕中View的结构,布局和外观
  • ViewModel - 扮演“View”和“Model”之间的使者,帮忙处理 View 的全部业务逻辑,通过双向数据绑定把 View 层和 Model 层连接了起来。

Vue2.0的响应式原理

vue的响应式是怎么实现的呢?很多文章也解析过了,vue 是通过数据劫持的形式进行响应式的实现,最最关键的就是Object.defineProperty,利用它可以 把传入 Vue 实例property 全部转为getter/setter。像下面这样:

function defineReactive(obj, key, value) {
  let val = value
  Object.defineProperty(obj, key, {
    enumerable: true, // 可枚举
    configurable: true, // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
    get: function () {
      // val 闭包
      return val
    },
    set: function (newVal) {
      // val 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
      val = newVal
    }
  })
}

obj对用的key的值发生变化了我们就能监听到了。那么又如何实现,数据变化后进行相关的操作的呢?

Vue的响应式主要包括三大部分:ObserverDepWatcher

  • Observer:将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。响应式数据中每一个对象,对应一个Observer的实例
  • DepObserver实例的data在触发 getter 时,Dep 实例就会收集依赖的 Watcher实例 , Dep实例是一个管理员 ,可以管理多个Watcher实例,当data变动时,就会通过 Dep实例 给 Watcher实例 发通知进行更新。响应式数据中每一个对象的属性,对应一个Dep的实例
  • Watcher:是一个观察者对象。依赖收集以后 Watcher 对象会被Dep实例进行管理,数据变动的时候Dep实例会通知Watcher实例,然后由Watcher实例进行视图的更新。

响应式实现的思路

响应式的实现主要实现一个observe函数,传入的参数是响应式数据data,返回值是Observer实例。observe函数的执行流程是:

  1. 判断传入的对象是否是数组或者对象,否则返回`undefined`
    
  2. 如果是,判断是否已经为响应式数据,如果是则返回已经存在的`Observer`实例,否则实例化一个新的`Observer`实例并返回。
    

    /**

    • 监听
    • @param {Object} data */ export default function observe(data) { if (typeof data !== ‘object’ || data === null) { // 不是对象或数组 return } let ob if (data.hasOwnProperty(‘ob’) && data.ob instanceof Observer) { ob = data.ob //__ob__为定义的响应式数据的Observer实例 } else { ob = new Observer(data) } return ob }

Observer类的实现

那么我们再来看看Observer类的定义:

class Observer {
  constructor(data) {
    this.data = data // 初始化属性
    this.dep = new Dep()// 初始化dep实例,在数组的监听中会用到
    def(data, '__ob__', this) // 给对象的本身添加一个属性指向响应式对象,用于判断数据是否已经成为响应式式数据,并可以通过数据对象找到
    if (Array.isArray(data)) {//判断是否为数组,如果是数组即进行数组的监听
      if ('__proto__' in {}) {
        data.__proto__ = arrProto
      } else {
        addSelfMethod(data, arrProto, arrayKeys)
      }
      this.observeArr(data)
    } else {//如果是对象进行对象的监听
      this.observeObj(data)
    }
  }
// 监听对象
  observeObj(obj) { 
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  //监听数组
  observeArr(arr) {
    for (let i = 0; i < arr.length; i++) {
      observe(arr[i])
    }
  }
}

从定义可以看出,在Observer类实例化得过程中有几个关键的地方:实例属性dep,数组的监听和对象的监听,那么我们接下来看一看Dep类的实现。

Dep类的实现

export default class Dep {
  constructor() {
    this.subs = [] // 数组,管理Watcher实例
  }
  addSub(sub) { //添加Watcher实例
    this.subs.push(sub)
  }
  notify(isArrayMethod = false) { // 调用Watcher实例的更新方法
    this.subs.forEach(function (sub) {
      sub.update(isArrayMethod)
    })
  }
}

Dep.target = null //静态属性,用于添加Watcher实例

从上面的代码可以看出,Dep类的实现比较简单,只需要维护一个数组管理Watcher实例,可以添加Watcher实例,通知Watcher实例,并调用其更新方法。关于notify的参数isArrayMethod并不是vue源码之中实现方法,只是为了辨别是否为数组方法所产生数组变化,然后做出响应,后续会解释。

既然Dep类的实现已经清楚,那么接下来回到Observer类的实例化过程,首先看下,如何实现对象的监听。

对象的监听

Observer类的定义中,对对象的监听是对对象的每一个属性调用defineReactive函数,这个函数其实就是实现响应式的关键部分,如下代码。

/**
 * 定义响应式
 * @param {Object} data 数据
 * @param {string} key 属性名
 * @param {*} val 值
 */
function defineReactive(obj, key, value) {
  let val = value
  let childOb = observe(val) // 递归深度遍历,实现深度监听,如果值不是对象或者数组,childOb为undefined
  let dep = childOb ? childOb.dep : new Dep()// 如果属性为对象或者数组,则使用其Observer实例的属性dep来管理,否则就实例化一个Dep实例,利用闭包,实现对原始值的管理
  Object.defineProperty(obj, key, {
    enumerable: true, // 可枚举
    configurable: true, // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
    get: function () {
      // val 闭包
      if (Dep.target) { // 添加`watcher`实例
        dep.addSub(Dep.target)
      }
      return val
    },
    set: function (newVal) {
      // 如果对象的属性为对象或数组,那么由于是引用类型,值更改之后,需要将原来响应式数据dep管理的`watcher`实例继承过来,然后再深度监听
      if (childOb) {
        let temp = dep
        childOb = observe(newVal) // 递归深度遍历,实现深度监听
        childOb.dep.subs.push(...temp.subs)
        dep = childOb.dep
      }
      // val 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
      val = newVal
      dep.notify()
    }
  })
}

在上面的代码中,有三个地方有些特殊:

  1. `let dep = childOb ? childOb.dep : new Dep()`,为什么属性为对象或者数组,使用其`Observer`实例的属性`dep`来管理?在数组的监听的过程中,需要借助这个属性才能实现对数组方法的监听
    
  2. `get`函数中,关于`dep`管理`Watcher`实例那部分代码的`Dep.target`是什么?`Dep.target`其中存储的是`Watcher`实例,因为同一时刻只能存在一个`Watcher`实例被纳入管理。或许不太清楚,具体解释会在后面依赖收集中进行解释
    
  3. `set`函数中关于新的值的递归遍历处理是存在问题的。
    
    • 在代码注释里我的解释其实不完全正确的,如果对象对应的属性是ObjectArray,而且新的值是同类型的且值内部的被依赖的属性是相同的,那么这是没有问题的。
    • 如果对象对应的属性是原始值,新的值是ObjectArray,那么新的值是无法被准换响应式的
    • 如果对象对应的属性是ObjectArray,而且新的值是原始值,这段代码会报错。
    • 有一行代码是的目的是将原来响应式数据dep管理的Watcher实例继承过来,但是我并没有考虑到,原本的Watcher实例如果不需要使用的情况,这可能导致depsubs数组越来越大,然后内存溢出。
    • 但只是解释响应式够了,所以这段代码没有优化修改。

    // 如果对象的属性为对象或数组,那么由于是引用类型,值更改之后,需要将原来响应式数据dep管理的watcher实例继承过来,然后再深度监听 if (childOb) { let temp = dep childOb = observe(newVal) // 递归深度遍历,实现深度监听 childOb.dep.subs.push(…temp.subs)//将原来响应式数据dep管理的watcher实例继承过来 dep = childOb.dep }

数组的监听

Observer类的定义中,Array的监听是这样实现的:

if ('__proto__' in {}) {
  data.__proto__ = arrProto
} else {
  addSelfMethod(data, arrProto, arrayKeys)
}
this.observeArr(data)

在这段代码中,observeArr不用解释的。重点是data.__proto__ = arrProtoaddSelfMethod两个部分的实现。在解释这两个之前,先要弄清楚数组的监听是指对于一些常用的数组的方法的引起的改变的监听。arr[0]=4arr.length=1这种类型的改变是无法监听的。

这里利用了原型链的原理,先来看看arrProtoarrayKeys的实现。

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty)
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(
  (methodName) => {
    arrProto[methodName] = function (...args) {
      const result = oldArrayProperty[methodName].call(this, ...args) //执行原来的数组方法
      const ob = this.__ob__ // 获取对应的`Observer`实例
      let inserted
      switch (methodName) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      if (inserted) ob.observeArr(inserted) // 监听新增的值
      ob.dep.notify(true) //通知更改
      return result
    }
  }
)
export default arrProto

const arrayKeys = Object.getOwnPropertyNames(arrProto)

由于__proto__不是标准的属性,所以有的浏览器可能没实现,如果存在这个属性,那么就让原本的数组的__proto__指向我们更改后的对象,这样可以实现,对数组方法的监听。如果不存在就调用addSelfMethod,在对象身上添加对应的不可枚举的方法,实现拦截。

/**
 *
 * @param {obj} target
 * @param {*} src
 * @param {*} keys
 */
function addSelfMethod(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])//给数组对象的本身添加对应的方法
  }
}

其实到上面已经很清楚,为什么Observer实例上会存在dep实例,为什么对象或者数组要用Observer实例上的dep实例来管理Watcher实例。因为这样可以在非defineReactive函数内部访问和调用。

到这里,除了Watcher,主要的响应式核心已经完成了。那么,我们先来整合一下实现一个简单的响应式类。

基础的MVue类的实现

作为一个框架肯定要有一个入口,那么就是响应式实例,响应式实例主要负责将数据转换成响应式的,还有一些生命周期钩子什么的,实现比较简单。

import observe from './observer/index'
import { proxy } from './utils/index'

export default class MVue {
  constructor(options) {
    const { el, data, methods, created } = options
    if (data) {
      this.data = typeof data === 'function' ? data() : data // 函数则执行获取返回值
      proxy(this.data, this)
      observe(this.data) //转换响应式
    }
    Object.assign(this, methods)
    if (el) {
      this.elSelector = el
    }
    created && created.call(this)
  }
}

这就实现了一个简单的响应式类,调用observe实现响应式数据,其中proxy函数是实现访问实例.a相当于访问实例.data.a,代码如下:

export function proxy(data, mVue) {
  const me = mVue
  Object.keys(data).forEach(function (key) {
    Object.defineProperty(me, key, {
      configurable: false,
      enumerable: true,
      get: function () {
        return me.data[key]
      },
      set: function (newVal) {
        me.data[key] = newVal
      }
    })
  })
}

依赖收集

响应式实例有了,数据变化也能监听到了,那么怎么样执行相关操作呢?这就涉及到依赖收集,关于依赖收集课参考这篇文章的说法。我的理解,依赖收集就是在模板中使用的响应式数据才会根据数据的变化执行渲染。也就是用到了响应式对象的属性就在对应的dep实例添加Watcher实例。

那么我们再回到对象监听的defineReactive函数里面关于get函数的部分。

get: function () {
      // val 闭包
  if (Dep.target) { // 添加`watcher`实例
    dep.addSub(Dep.target)
  }
    return val
}

当我们访问响应数据的时候都会经过get函数,如果这个时候需要进行依赖收集的话,那么Dep.target的值就是待管理的Watcher实例,然后进行依赖收集。如果不需要收集的话就让Dep.target = null

那么我们整理一下依赖收集的思路:假设我们在模板中需要用到响应式数据的属性a的值,在模板编译过程中,实例化一个Watcher实例,并定义相关操作,然后将Dep.target指向该Watcher实例,去响应式实例中访问这个属性,然后就能够添加管理,最后再Dep.target = null,等待下一次收集。

那么如何去访问响应式实例的数据呢?

/**
 * 获取变量
 * @param {String} expPath 变量路径
 */
export function parsePath(expPath) {
  let path = expPath
  //实现对数组的访问类似arr[0]
  if (path.indexOf('[')) {
    path = path.replace(/\[/g, '.')
    path = path.replace(/\]/g, '.')
    if (/\.$/.test(path)) {
      path = path.slice(0, path.length - 1)
    }
    if (/\.\./.test(path)) {
      path = path.replace('..', '.')
    }
  }
  const bailRE = /[^\w.$]/
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (object) {
    let obj = object
    for (let i = 0; i < segments.length; i++) {
      if (typeof obj === 'undefined') return ''
      let exp = segments[i]
      obj = obj[exp]
    }
    if (typeof obj === 'undefined') return ''
    return obj
  }
}

上面的函数就是实现对响应式数据的访问,传入对应的表达式(如a.b.c)会获得对应的函数,传入对应的响应式实例,执行函数即可获得变量的值,即调用get函数。

Watcher类的实现

一切就绪就差一个执行者来负责实施对应的操作了,也就是Watcher,下面就是代码。

import { parsePath } from '../utils/index'
import Dep from '../observer/dep.js'

export default class Watcher {
  constructor(mVue, exp, callback) {
    this.callback = callback //回调函数
    this.mVue = mVue // 响应式实例
    this.exp = exp // 表达式
    Dep.target = this // 开始依赖收集
    this.value = this.get() // 调用get
    Dep.target = null // 添加完毕
    this.update() // 第一次执行
  }
  async update(isArrayMethod = false) {
    const value = this.get()
    if (this.value !== value || isArrayMethod) {
      this.callback(value, this.value) // 调用回调
      this.value = value
    }
  }
  get() {
    const getter = parsePath(this.exp)
    return getter.call(this.mVue, this.mVue)
  }
}

从上面的代码可以知道,实例化时进行了依赖收集。我们在模板编译的时候只定义回调就好了。

总结

写到这里,整个响应式基本就完成了。最后整理一下,整体的思路。

20200726-214504-0093.jpg


  1. MVVM 模式介绍 ↩︎

  2. vue.js和MVVM ↩︎

    版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: Ray-daydayup 原文链接:https://juejin.im/post/6854573219597254670

回到顶部