如何实现一个极简版Vue (响应篇)"/>
【JS】如何实现一个极简版Vue (响应篇)
前言
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。
DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。
往期回顾:【JS】 如何实现一个极简版Vue (初始化)
概述
Vue.js 是通过数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
第一步 - 实现一个订阅器(Dep)
target
指向当前正在评估的目标观察者,一个Dep
通常有一个可观察的对象(subs
),可以有多个指向它的指令
// observe.js
class Dep {target = nullconstructor() {this.subs = []}// 收集观察者addSub(watcher) {this.subs.push(watcher)}// 通知观察者去更新视图notify() {this.subs.forEach(watcher => {watcher.upDate()})}
}
第二步 - 实现一个Observe
Observe 劫持目标对象的 getter / setter 收集依赖关系并调度更新,参考文档: Object.defineProperty() - JavaScript | MDN
export class Observe {constructor (data) {this.observe(data)}observe (data) {if (data && typeof data == 'object') {Object.keys(data).forEach((key) => {this.defineReactive(data, key, data[key])})}}defineReactive (data, key, value) {// 递归遍历this.observe(value) // 为每一个key创建依赖收集器const dep = new Dep()Object.defineProperty(data, key, {enumerable: true,configurable: false,get () {// 当有订阅时才往Dep中添加观察者Dep.target && dep.addSub(Dep.target)return value},// 利用箭头函数保持对当前对象的应用set: (newValue) => {this.observe(newValue)if (newValue !== value) {value = newValue}// 订阅器 => 观察者 => 更新视图dep.notify()}})}
}
第三步 - 实现一个观察者(Watcher)
观察者解析表达式,收集依赖项,并在表达式值更改时触发回调。
export class Watcher {/** vm 当前Vue实例* expr 当前指令的value 例如obj.msg* cb 回调函数*/constructor (vm , expr, cb) {this.vm = vmthis.expr = exprthis.cb = cbthis.oldValue = this.getOldValue() // underfind}getOldValue () {// 标示订阅者,手动触发get读取,将观察者挂载到Dep订阅器上Dep.target = thisconst oldValue = compileUtils.getValue(this.expr, this.vm)// 销毁此次订阅者Dep.target = null}upDate () {const newValue = compileUtils.getValue(this.expr, this.vm)// 更新并回调新值if (newValue !== this.oldValue) {this.cb(newValue)}}
}
添加数据观察者,先劫持对象再解析,最后再代理数据
// vue.js
import { Observe } from './observe.js';export default class Vue {constructor (options) {...new Observe(this.$data)new Compile(options.el, this)this.proxyData(this.$data)}
}
简单测试下,以v-text
为例,后续会补充模版字符串({{}}
)和v-model
的识别,先理解原理比较重要
完善指令绑定逻辑,添加观察者并注册回调函数
// compileUtils.js
import { Watcher } from './observe.js';const compileUtils = {text (node, expr, vm) {let value;if (expr.indexOf('{{') !== -1) {...} else {new Watcher (vm, expr, (newValue) => {this.upDater.textUpDater(node, newValue)})value = this.getValue(expr, vm)}}
}
打开控制台,这个时候我们已经初步实现了单向数据绑定,回到最开始的流程图,那么整个逻辑调用堆栈应该是
注册:Observer => Compile => Updater(text) => Watcher => getter => Dep
更新:Observer => setter => Dep => Watcher => Updater
// <h1 v-text="msg"></h1>
// <button v-on:click="btnClick">Click</button>
new Vue({data: {msg: 'hello world'}methods: {btnClick() {this.msg = 'hello Watcher'}}
})
注册v-model
指令,监听input
方法,手动触发 setter
通知对应视图更新,从而实现双向数据绑定
const compileUtils = {...model (node, expr, vm) {const value = this.getValue(expr, vm)new Watcher (vm, expr, (newValue) => {this.upDater.modelUpDater(node, newValue)})node.addEventListener('input', (e) => {this.setValue(expr, vm, e.target.value)})this.upDater.modelUpDater(node, value)},// input双向数据绑定setValue(expr, vm, inputVal) {return vm.$data[expr] = inputVal},upDater: {...modelUpDater (node value) {node.value = value}}
}
// <input v-model="msg" />
处理模版字符串({{}}
)和嵌套属性obj.msg
const compileUtils = {...text (node, expr, vm) {let value;if (expr.indexOf('{{') !== -1) {value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {new Watcher(vm, args[1], (newValue) => {this.upDater.textUpDater(node, this.getContentVal(expr, vm))})return this.getValue(args[1], vm)})}...},getContentVal(expr, vm) {return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {return this.getValue(args[1], vm)})},// 处理 input 嵌套属性 obg.msgsetValue(expr, vm, inputVal) {const [arr, len = arr.length] = [expr.split('.')]return arr.reduce((data, currentVal, index) => {if (index == len - 1) {data[currentVal] = inputVal} else {return data[currentVal] // 引用传递}}, vm.$data)}
}
第四步 测试验证
实现的功能比较少,但包括基础的双向数据绑定、指令解析器
<body><div id="app"><h1 v-bind:title="obj.msg">{{obj.name}} -- {{obj.msg}}</h1><input v-model="obj.msg" type="text"><button @click="btnClick">Click</button></div><script type="module">import Vue from './vue.js'new Vue({el: '#app',data: {obj: {name: 'AsnLi',msg: 'hello world'}},methods: {btnClick() {this.obj.msg = 'hello world'}}})</script>
</body>
总结
通过整合Observe
,Compile
和Watcher
三者、 从而实现一个简单的MVVM
来阐述了双向绑定的原理和实现。
单纯的去分析工业级别的源码实在过于牵强,如果能熟悉其中的原理先简单实现,出现问题带着疑问再去参考源码中的解决方案,不失为一种折中的学习方式~
参考资料
-
Vue高级指南-01 Vue源码解析之手写Vue源码
-
Vue 源码解读 —— Vue 初始化过程
-
实现最简 vue3 模型
-
Vue.js 技术揭秘
友情链接
【JS】如何实现一个极简版Vue (响应篇) | AsnLi的博客
更多推荐
【JS】如何实现一个极简版Vue (响应篇)
发布评论