【JS】如何实现一个极简版Vue (响应篇)

编程入门 行业动态 更新时间:2024-10-09 16:25:10

【JS】<a href=https://www.elefans.com/category/jswz/34/1769177.html style=如何实现一个极简版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,CompileWatcher三者、 从而实现一个简单的MVVM来阐述了双向绑定的原理和实现。

单纯的去分析工业级别的源码实在过于牵强,如果能熟悉其中的原理先简单实现,出现问题带着疑问再去参考源码中的解决方案,不失为一种折中的学习方式~

参考资料

  • Vue高级指南-01 Vue源码解析之手写Vue源码

  • Vue 源码解读 —— Vue 初始化过程

  • 实现最简 vue3 模型

  • Vue.js 技术揭秘

友情链接

【JS】如何实现一个极简版Vue (响应篇) | AsnLi的博客

更多推荐

【JS】如何实现一个极简版Vue (响应篇)

本文发布于:2024-03-12 02:40:27,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1730505.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:如何实现   JS   Vue   极简版

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!