【前端词典】Vue 响应式原理其实很好懂
前言
这是十篇 Vue 系列文章的第三篇,这篇文章我们讲讲 Vue 最核心的功能之一:响应式原理。
如何理解响应式
可以这样理解:当一个状态改变之后,与这个状态相关的事务也立即随之改变,从前端来看就是数据状态改变后相关 DOM 也随之改变。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。
抛个问题
我们先看看我们在 Vue 中常见的写法:
<div id="app" @click="changeNum"> {{ num }}</div>
var app = new Vue({ el: '#app', data: { num: 1 }, methods: { changeNum() { this.num = 2 } }})
这种写法很常见,不过你考虑过当为什么执行
this.num=2
后视图为什么会更新呢?通过这篇文章我力争把这个点讲清楚。
如果不使用 Vue,我们应该怎么实现?
我的第一想法是像下面这样实现:
let data = { num: 1};Object.defineProperty(data, 'num',{ value: value, set: function( newVal ){ document.getElementById('app').value = newVal; }});input.addEventListener('input', function(){ let data.num = 2;});
这样可以粗略的实现点击元素,自动更新视图。
这里我们需要通过 Object.defineProperty 来操作对象的访问器属性。监听到数据变化的时候,操作相关 DOM。
而这里用到了一个常见模式 —— 发布/订阅模式。
我画了一个大概的流程图,用来说明观察者模式和发布/订阅模式。如下:
仔细的同学会发现,我这个粗略的过程和使用 Vue 的不同的地方就是需要我自己操作 DOM 重新渲染。
如果我们使用 Vue 的话,这一步就是 Vue 内部的代码来处理的。这也是我们为什么在使用 Vue 的时候无需手动操作 DOM 的原因。
关于 Object.defineProperty
我在上一篇文章已经提及,这里就不再复述。
Vue 是如何实现响应式的
我们知道对象可以通过 Object.defineProperty
操作其访问器属性,即对象拥有了 getter
和 setter
方法。这就是实现响应式的基石。
先看一张很直观的流程图:
initData 方法
在 Vue 的初始化的时候,其 _init()
方法会调用执行 initState(vm)
方法。 initState
方法主要是对 props
、 methods
、 data
、 computed
和 wathcer
等属性做了初始化操作。
这里我们就对 data
初始化的过程做一个比较详细的分析。
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { ...... } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] ...... // 省略部分兼容代码,但不影响理解 if (props && hasOwn(props, key)) { ...... } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */)}
initData
初始化 data 的主要过程也是做两件事:
- 通过
proxy
把每一个值vm._data.[key]
都代理到vm.[key]
上; - 调用
observe
方法观测整个 data 的变化,把 data 也变成响应式(可观察),可以通过vm._data.[key]
访问到定义 data 返回函数中对应的属性。
数据劫持 — Observe
通过这个方法将 data 下面的所有属性变成响应式(可观察)。
// 给对象的属性添加 getter 和 setter,用于依赖收集和发布更新export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value // 实例化 Dep 对象 this.dep = new Dep() this.vmCount = 0 // 把自身实例添加到数据对象 value 的 __ob__ 属性上 def(value, '__ob__', this) // value 是否为数组的不同调用 if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } }
// 取出所有属性遍历 walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } }
observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }}
def
函数内封装了 Object.defineProperty
,所以你 console.log(data) ,会发现多了一个 __ob__
的属性。
defineReactive 方法遍历所有属性
// 定义一个响应式对象的具体实现export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) { const dep = new Dep() ..... // 省略部分兼容代码,但不影响理解 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 进行依赖收集 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val ..... // 省略部分兼容代码,但不影响理解 if (setter) { setter.call(obj, newVal) } else { val = newVal } // 对新的值进行监听 childOb = !shallow && observe(newVal) // 通知所有订阅者,内部调用 watcher 的 update 方法 dep.notify() } })}
defineReactive
方法最开始初始化 Dep 对象的实例,然后通过对子对象递归调用 observe
方法,使所有子属性也能变成响应式的对象。并且在 Object.defineProperty
的 getter
和 setter
方法中调用 dep
的相关方法。
即:
-
getter
方法完成的工作就是依赖收集 ——dep.depend()
-
setter
方法完成的工作就是发布更新 ——dep.notify()
我们发现这里都和 Dep 对象有着不可忽略的关系。接下来我们就看看 Dep 对象。这个 Dep
调度中心作用的 Dep
前文中我们提到发布/订阅模式,在发布者和订阅者之前有一个调度中心。这里的 Dep 扮演的角色就是调度中心,主要的作用就是:
- 收集订阅者 Watcher 并添加到观察者列表 subs
- 接收发布者的事件
- 通知订阅者目标更新,让订阅者执行自己的 update 方法
详细代码如下:
// Dep 构造函数export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>;
constructor () { this.id = uid++ this.subs = [] } // 向 dep 的观察者列表 subs 添加 Watcher addSub (sub: Watcher) { this.subs.push(sub) } // 从 dep 的观察者列表 subs 移除 Watcher removeSub (sub: Watcher) { remove(this.subs, sub) } // 进行依赖收集 depend () { if (Dep.target) { Dep.target.addDep(this) } } // 通知所有订阅者,内部调用 watcher 的 update 方法 notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }}// Dep.target 是全局唯一的观察者,因为在任何时候只有一个观察者被处理。Dep.target = null// 待处理的观察者队列const targetStack = []
export function pushTarget (_target: ?Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target}
export function popTarget () { Dep.target = targetStack.pop()}
Dep 可以理解成是对 Watcher
的一种管理,Dep 和 Watcher
是紧密相关的。所以我们必须看一看 Watcher
的实现。
订阅者 —— Watcher
Watcher
中定义了许多原型方法,这里我只粗略的讲 update
和 get
这三个方法。
// 为了方便理解,部分兼容代码已被我省去 get () { // 设置需要处理的观察者 pushTarget(this) const vm = this.vm let value = this.getter.call(vm, vm) // deep 是否为 true 的处理逻辑 if (this.deep) { traverse(value) } // 将 Dep.target 指向栈顶的观察者,并将他从待处理的观察者队列中移除 popTarget() // 执行依赖清空动作 this.cleanupDeps() return value }
update () { if (this.computed) { ... } else if (this.sync) { // 标记为同步 this.run() } else { // 一般都是走这里,即异步批量更新:nextTick queueWatcher(this) } }
Vue 的响应式过程大概就是这样了。感兴趣的可以看看源码。
最后我们在通过这个流程图来复习一遍:
Vue 相关文章输出计划
最近总有朋友问我 Vue 相关的问题,因此接下来我会输出 9 篇 Vue 相关的文章,希望对大家有一定的帮助。我会保持在 7 到 10 天更新一篇。
- 【前端词典】Vuex 注入 Vue 生命周期的过程(完成)
- 【前端词典】学习 Vue 源码的必要知识储备(完成)
- 【前端词典】 Vue 响应式原理其实很好懂(完成)
- 【前端词典】新老 VNode 进行 patch 的过程
- 【前端词典】如何开发功能组件并上传 npm
- 【前端词典】从这几个方面优化你的 Vue 项目
- 【前端词典】从 Vue-Router 设计讲前端路由发展
- 【前端词典】在项目中如何正确的使用 Webpack
- 【前端词典】Vue 服务端渲染
- 【前端词典】Axios 与 Fetch 该如何选择
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 【答疑解惑】为什么你的 Charles 会抓包失败?
- Mybatis-generator 逆向工程 自定义PO,xml,mapper,example
- 高速上云/网络穿透/视频上云网关EasyNTS组网服务登录状态检测优化记录
- 树莓派基础实验38:逻辑分析仪分析PWM、UART信号
- 【终端设备】视频上云/网络穿透EasyNTS云组网硬件终端无法单独修改账号的优化方式
- 测试环境问题排查的那些事儿
- RTSP流媒体协议视频平台EasyNVR和EasyNTS智能云组网同一浏览器运行为什么会导致EasyNTS无法登陆?
- Java:手写线程安全LRU缓存X探究影响命中率的因素
- 视频上云/网络穿透/网络映射服务EasyNTS设备管理为什么会出现无法搜索到设备的情况?
- 快速打造属于你的接口自动化测试框架
- 大数据下的质量体系建设
- PostgreSQL 日志系统 及 设置错误导致磁盘塞满案例
- 六、乘胜追击,将剩下的Git知识点搞定
- 树莓派基础实验39:解析无线电接收机PWM、SBUS信号
- nodejs源码分析第十九章 -- udp模块