五、Vue.js 响应式实现
这里大家可以再回顾下下面这张官网经典的图,思考下前面讲的示例。
(图片来自:cn.vuejs.org/v2/guide/re…)
上一节实现了简单的数据响应式,接下来继续通过完善该示例,实现一个简单的 Vue.js 响应式,测试代码如下:
// index.js const vm = new Vue({ el: '#app', data(){ return { text: '你好,前端自习课', desc: '每日清晨,享受一篇前端优秀文章。' } } });
是不是很有内味了,下面是我们最终实现后项目目录:
- mini-reactive / index.html // 入口 HTML 文件 / index.js // 入口 JS 文件 / observer.js // 实现响应式,将数据转换为响应式对象 / watcher.js // 实现观察者和被观察者(依赖收集者) / vue.js // 实现 Vue 类作为主入口类 / compile.js // 实现编译模版功能
知道每一个文件功能以后,接下来将每一步串联起来。
1. 实现入口文件
我们首先实现入口文件,包括 index.html
/ index.js
2 个简单文件,用来方便接下来的测试。
1.1 index.html
<!DOCTYPE html> <html lang="en"> <head> <script src="./vue.js"></script> <script src="./observer.js"></script> <script src="./compile.js"></script> <script src="./watcher.js"></script> </head> <body> <div id="app">{{text}}</div> <button id="update">更新数据</button> <script src="./index.js"></script> </body> </html>
1.2 index.js
"use strict"; const vm = new Vue({ el: '#app', data(){ return { text: '你好,前端自习课', desc: '每日清晨,享受一篇前端优秀文章。' } } }); console.log(vm.$data.text) vm.$data.text = '页面数据更新成功!'; // 模拟数据变化 console.log(vm.$data.text)
2. 实现核心入口 vue.js
vue.js
文件是我们实现的整个响应式的入口文件,暴露一个 Vue
类,并挂载全局。
class Vue { constructor (options = {}) { this.$el = options.el; this.$data = options.data(); this.$methods = options.methods; // [核心流程]将普通 data 对象转换为响应式对象 new Observer(this.$data); if (this.$el) { // [核心流程]将解析模板的内容 new Compile(this.$el, this) } } } window.Vue = Vue;
Vue
类入参为一个配置项 option
,使用起来跟 Vue.js 一样,包括 $el
挂载点、 $data
数据对象和 $methods
方法列表(本文不详细介绍)。
通过实例化 Oberser
类,将普通 data 对象转换为响应式对象,然后判断是否传入 el
参数,存在时,则实例化 Compile
类,解析模版内容。
总结下 Vue
这个类工作流程 :
3. 实现 observer.js
observer.js 文件实现了 Observer
类,用来将普通对象转换为响应式对象:
class Observer { constructor (data) { this.data = data; this.walk(data); } // [核心方法]将 data 对象转换为响应式对象,为每个 data 属性设置 getter 和 setter 方法 walk (data) { if (typeof data !== 'object') return data; Object.keys(data).forEach( key => { this.defineReactive(data, key, data[key]) }) } // [核心方法]实现数据劫持 defineReactive (obj, key, value) { this.walk(value); // [核心过程]遍历 walk 方法,处理深层对象。 const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get () { console.log('[getter]方法执行') Dep.target && dep.addSub(Dep.target); return value }, set (newValue) { console.log('[setter]方法执行') if (value === newValue) return; // [核心过程]当设置的新值 newValue 为对象,则继续通过 walk 方法将其转换为响应式对象 if (typeof newValue === 'object') this.walk(newValue); value = newValue; dep.notify(); // [核心过程]执行被观察者通知方法,通知所有观察者执行 update 更新 } }) } }
相比较第四节实现的 Observer
类,这里做了调整:
- 增加
walk
核心方法,用来遍历对象每个属性,分别调用数据劫持方法(defineReactive()
); - 在
defineReactive()
的 getter 中,判断Dep.target
存在才添加观察者,下一节会详细介绍Dep.target
; - 在
defineReactive()
的 setter 中,判断当前新值(newValue
)是否为对象,如果是,则直接调用this.walk()
方法将当前对象再次转为响应式对象,处理深层对象。
通过改善后的 Observer
类,我们就可以实现将单层或深层嵌套的普通对象转换为响应式对象。
4. 实现 watcher.js
这里实现了 Dep
被观察者类(依赖收集者)和 Watcher
观察者类。
class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify(data) { this.subs.forEach(sub => sub.update(data)); } } class Watcher { constructor (vm, key, cb) { this.vm = vm; // vm:表示当前实例 this.key = key; // key:表示当前操作的数据名称 this.cb = cb; // cb:表示数据发生改变之后的回调 Dep.target = this; // 全局唯一 // 此处通过 this.vm.$data[key] 读取属性值,触发 getter this.oldValue = this.vm.$data[key]; // 保存变化的数据作为旧值,后续作判断是否更新 // 前面 getter 执行完后,执行下面清空 Dep.target = null; } update () { console.log(`数据发生变化!`); let oldValue = this.oldValue; let newValue = this.vm.$data[this.key]; if (oldValue != newValue) { // 比较新旧值,发生变化才执行回调 this.cb(newValue, oldValue); }; } }
相比较第四节实现的 Watcher
类,这里做了调整:
- 在构造函数中,增加
Dep.target
值操作; - 在构造函数中,增加
oldValue
变量,保存变化的数据作为旧值,后续作为判断是否更新的依据; - 在
update()
方法中,增加当前操作对象key
对应值的新旧值比较,如果不同,才执行回调。
Dep.target
是当前全局唯一的订阅者,因为同一时间只允许一个订阅者被处理。target
指当前正在处理的目标订阅者,当前订阅者处理完就赋值为 null
。这里 Dep.target
会在 defineReactive()
的 getter 中使用到。
通过改善后的 Watcher
类,我们操作当前操作对象 key
对应值的时候,可以在数据有变化的情况才执行回调,减少资源浪费。
4. 实现 compile.js
compile.js 实现了 Vue.js 的模版编译,如将 HTML 中的 {{text}}
模版转换为具体变量的值。
compile.js 介绍内容较多,考虑到篇幅问题,并且本文核心介绍响应式原理,所以这里就暂时不介绍 compile.js 的实现,在学习的朋友可以到我 Github 上下载该文件直接下载使用即可,地址: github.com/pingan8787/…
5. 测试代码
到这里,我们已经将第四节的 demo 改造成简易版 Vue.js 响应式,接下来打开 index.html 看看效果:
当 index.js 中执行到:
vm.$data.text = '我们必须经常保持旧的记忆和新的希望。';
页面便发生更新,页面显示的文本内容从“你好,前端自习课”更新成“我们必须经常保持旧的记忆和新的希望。”。
到这里,我们的简易版 Vue.js 响应式原理实现好了,能跟着文章看到这里的朋友,给你点个大大的赞👍
六、总结
本文首先通过回顾观察者模式和 Object.defineProperty()
方法,介绍 Vue.js 响应式原理的核心知识点,然后带大家通过一个简单示例实现简单响应式,最后通过改造这个简单响应式的示例,实现一个简单 Vue.js 响应式原理的示例。
相信看完本文的朋友,对 Vue.js 的响应式原理的理解会更深刻,希望大家理清思路,再好好回味下~