Vuejs技术内幕:数据响应式之2.x版

每个系列一本前端好书,帮你轻松学重点。

本系列来自ZOOM前端架构师,前百度、滴滴资深技术专家黄轶 所编写的 《Vue.js技术内幕》

上一篇我们聊了组件渲染,你好奇的第二个问题,一定是响应。即"数据驱动"。

数据驱动的本质是数据变化引起页面变化,让开发者只关注数据操作。

响应式的处理,需要应对多种场景,而且其中带有不易察觉的"巧思",所以,本文只聊一个话题,就是Vue 2.x的响应式实现。

核心与框架

大家对于Vue,js 2.x使用的API和流程应该耳熟能详。API 就是 Object.defineProperty,流程则是:在数据被访问时收集依赖,数据被修改时更新依赖

图示如下:

具体过程,以一个简单的示例来看:

xml 复制代码
<template>
  <p>{{ msg }}</p>
  <button @click="random">Random msg</button>
</template>
<<script>
export default {
  data(){
    return {
      msg:"msg reactive"
    }
  },
  methods: {
    random(){
      this.msg = Math.random()
    }
  },
}
</script>

不得不说,Vue 2.x的流行是有原因的,笔者已经写了几年的Vue3 和 React,敲这段代码依然非常自然,符合直觉。

开发者只需要把变量定义在data中,改变变量的方法定义在methods中,然后在模板中绑定这个变量,页面就会渲染定义好的变量,点击按钮,执行函数,就能改变页面的渲染。

代码和表现都很简单,但不论是简单还是复杂,都会经过同一流程。

处理流程

流程分为三步:数据劫持、依赖收集、派发更新

说法比较抽象,直接看代码:

scss 复制代码
// 核心:定义对象的响应式属性
function defineReactive(obj: any, key: string) {
  let value = obj[key];
  let dep = new Dep();

  // 深度代理
  let childOb = observe(value);
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集 dep.depend()
      if (Dep.target) {
        dep.depend();
        if (childOb) {
    // value 是对象或者数组,childOb 才会有值
    // 这里是针对数组的依赖收集
          childOb.dep.depend();
        }
      }
      return value;
    },
    set(newVal) {
  if (!hasChanged(value, newVal)) {
         return;
       }
       // 新值进行响应式
       observe(newVal);
       value = newVal;
       // 通知更新
       dep.notify();
    },
  });
}

这个方法里,我们见到了两个关键角色:dep、observe

还看到 observe(value)、Dep.target,dep.depend(),dep.notify(),你可能已经懵了,这些代码在干什么?

底层机制

搞懂这些操作,需要先做两件事:

1、理解底层机制

2、理顺机制与代码结合的过程

响应式机制使用了"观察者"模式,它是设计模式的一种。

设计模式,是由前人所总结的,用来解决某类问题的方案。可以理解为数学或者物理中的公式,同类问题均能套用,此谓"模式"。

在这里,它解决的问题是哪些数据要响应,数据变化之后哪些地方要跟着变

所以,需要有一个角色对数据做观察(Watcher) ,还需要有一个角色来收集对数据有依赖的集合(subs) ,然后在变化发生的时候通知更新(notify), 这些事情都是在dep中完成的。

说到这,就明了。

在拿到数据之后,首先创建了一个Dep实例dep。

scss 复制代码
let dep = new Dep();

export default class Dep {
  // 静态变量,保存 Watcher 类型对象
  static target: ?Watcher;
  subs: Array<Watcher>;     // 订阅者数组 元素即 Watcher对象
  constructor () {
    this.subs = []
  }
  // 添加订阅者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // 依赖收集
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  // 通知订阅者 更新事件
  notify () {
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

数据就是data,Dep.target是Watcher,dep.depend()就是在收集依赖,我们结合三段代码来看。

它们分别来自:defineReactive、Dep、Watcher。

kotlin 复制代码
// defineReactive中
if (Dep.target) {
  dep.depend();
}

// Dep中
// 添加订阅者
addSub (sub: Watcher) {
  this.subs.push(sub)
}
// 依赖收集
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
// 通知订阅者 更新事件
notify () {
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

// Watcher中
class Watcher {
  addDep(dep) {
    dep.addSub(this);
  }
  update() {
    const oldValue = this.value;
    this.value = this.get(); // 重新获取
    this.callback.call(this.vm, this.value, oldValue); // 触发回调
  }
}

从上到下,分别调用了:dep.depend()、Watcher.addDep(this)、dep.addSub(this);

玄机就在理解这两个this,第一个是dep,第二个是watcher

这个流程下来做了三件事:

1、创建dep实例

2、Watcher掉用自身的addDep方法,把dep实例加进去

3、dep实例调用自身的addSub方法,把watcher作为订阅者加入到订阅者列表中

总结:把跟当前数据相关的观察者存储到了所创建的dep实例的subs数组当中,这就是收集依赖。

这个流程清楚了,更新就好说,需要更新的时候,调用观察者的update()方法,想想观察者放哪儿去了?dep的subs里,那么执行下面的代码就可以了。

kotlin 复制代码
// defineReactive中
// 通知更新 dep.notify()
dep.notify();

// Dep中
// 通知订阅者 更新事件
notify () {
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

// Watcher中
update() {
  const oldValue = this.value;
  this.value = this.get(); // 重新获取
  this.callback.call(this.vm, this.value, oldValue); // 触发回调
}

一句话总结,找到跟当前数据相关的watcher,执行update。

observe

到这里我们刻意忽略了一个角色------observe,我们在defineReactive方法中见过它。

ini 复制代码
function defineReactive(obj: any, key: string) {
  let value = obj[key];

  // 深度代理
  let childOb = observe(value);
}

它起到什么作用呢?可以看出,从data中取值之后,它可以对值进行更深层次的代理,所以其实它才是响应式数据开始处理的入口,而不是defineReactive。数据先经过observe,做完了对数据类型的判断,才进入defineReactive,

看下面两段代码,就都串起来了。

scss 复制代码
// 数据响应式的入口函数
export function observe(value: any) {
  if (isPlainObject(value) || isArray(value)) {
    // 当值为对象或数组时,进行响应式处理
    return new Observer(value);
  }
}

class Observer {
  constructor(value: any) {
    if (isArray(value)) {
      // 数组,需要特殊处理,进行劫持数组方法
      (value as any).__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      // 对象
      const keys = Object.keys(value);
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        defineReactive(value, key);
      }
    }
  }

  /**
   * 将数组的每一项进行响应式处理
   * @param value
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i]);
    }
  }
}

至此,Vue.js 2.x的响应式机制梳理完成。

小结

响应式的过程,说复杂也复杂,需要一步步精妙的设计及考虑不同细节,才能囊括日常开发的不同情况,但说简单也简单。

我们从头再做一次梳理:

1、observe接收data,判断data中的变量是一般的值,还是普通对象,还是数组;

2、如果是普通对象,对其进行劫持,如果是数组,则进入数组的处理流程;

3、响应式机制应用了"观察者模式",需要观察者角色,负责收集观察者的角色,及发生变化时通知观察者更新的动作;

4、收集依赖和执行更新的过程使用了Object.defineProperty API中的getter和setter特性;

5、收集依赖在getter中进行,Dep负责收集Watcher;

6、更新动作在setter中进行,Dep负责通知Watcher更新。

OK,下篇文,我们聊Vue.js 3.x的实现过程。

更多好文第一时间接收,可关注公众号:前端说书匠

相关推荐
辻戋16 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保16 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun17 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp17 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.18 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl20 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫1 天前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友1 天前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理1 天前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻1 天前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js