Vue.js响应式原理(四)——模拟vue响应式原理

tech2025-07-27  2

分析

整体分析模拟最小版本的vue步骤:

观察Vue基本使用结构

打印Vue实例观察即将要模拟的成员:

以_开头的是私有成员(例如_data),以 开 头 的 是 公 共 成 员 ( 例 如 ‘ 开头的是公共成员(例如` (data`),我们只模拟公共成员即可。$el可以是选择器,也可以是dom对象,如果是选择器需要我们转换成dom对象进行存储。最小版本准备模拟$data、$el、$options

最小版本的vue的整体结构如下,即需要实现的几种类型。 不同类分别的作用如下:

Vue:把data中的成员注入到Vue实例,并且把data中的成员转成getter/setter;Vue内部会调用Observer和Compiler。Observer(数据劫持):能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep(发布者)Compiler(解析指令):解析每个元素中的指令及插值表达式,并替换成对应的数据Dep(发布者):添加观察者,当数据发生变化时通知所有观察者Watcher(观察者):有一个update方法,当数据变化负责更新视图。

实现

准备一个html,里面具有需要解析的结构,如下:

<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Mini Vue</title> </head> <body> <!-- 需要被解析的html start --> <div id="app"> <h1>差值表达式</h1> <h3>{{ msg }}</h3> <h3>{{ count }}</h3> <h1>v-text</h1> <div v-text="msg"></div> <h1>v-model</h1> <input type="text" v-model="msg" /> <input type="text" v-model="count" /> </div> <!-- 需要被解析的html end --> <!-- 自定义的类依次引入,需要被依赖的类优先引入 --> <!-- <script src="./js/dep.js"></script> --> <!-- <script src="./js/watcher.js"></script> --> <!--<script src="./js/compiler.js"></script> --> <!--<script src="./js/observer.js"></script> --> <!--<script src="./js/vue.js"></script> --> <script> let vm = new Vue({ el: "#app", data: { msg: "Hello Vue", count: 100, person: { name: "zs" }, }, }); console.log(vm.msg); // vm.msg = { test: "Hello" }; vm.test = "abc"; </script> </body> </html>

Vue类

功能:

负责接收初始化的参数(选项)负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter负责调用 observer 监听 data 中所有属性的变化负责调用 compiler 解析指令/插值表达式

类图结构: 解释说明:

约定以 下划线 开头的成员都是私有成员。 _proxyData():将data的属性转换成getter和setter,注入到vue实例中。

代码: class Vue { constructor(options) { // 1.保存选项的数据 this.$options = options || {}; this.$data = options.data || {}; const el = options.el; this.$el = typeof el === "string" ? document.querySelector(el) : el; // 2.把data中的属性注入到vue实例中,转换成getter和setter this._proxyData(this.$data); // 3.调用Observer实现数据劫持 new Observer(this.$data); // 4.调用Compiler解析指令/插值表达式等 new Compiler(this); } _proxyData(data) { Object.keys(data).forEach((key) => { // 这里的this是指vue实例,所以直接向其注入转换成getter和setter的属性 Object.defineProperty(this, key, { configurable: true, enumerable: true, get() { return data[key]; }, set(newVal) { if (newVal === data[key]) return; data[key] = newVal; }, }); }); } }

Observer (数据劫持)类

功能

负责把 data 选项中的属性转换成响应式数据data 中的某个属性也是对象,把该属性转换成响应式数据数据变化发送通知 ----------------?

类图 解释说明:

walk(data):遍历data的所有属性,会调用defineReactive方法进行数据转换。defineReactive(data,key,value):将数据定义为响应式数据,即getter和setter。 代码: class Observer { constructor(data) { this.walk(data); } // 1.遍历data所有属性 walk(data) { // 加强程序健壮性,做对象判断 if (!data || typeof data !== "object") return; Object.keys(data).forEach((key) => { this.defineReactive(data, key, data[key]); }); } // 2.将data转换成响应式数据,即getter和setter // 特别说明:该方法中需要传入value而不直接使用data[key]的原因是 会造成循环引用的问题。 defineReactive(data, key, value) { const that = this; // ______特别加入:为每一个属性增加Dep对象,负责收集依赖,发送通知。 const dep = new Dep(); // 特别情况1:当data的某一属性值也是对象时,也需要将其转换为getter和setter this.walk(value); Object.defineProperty(data, key, { configurable: true, enumerable: true, get() { // ______特别加入:在getter中收集观察者: // 判断是否有观察者的方式是在watcher类实例化时,向Dep类上挂载一个target属性,用于存储当前的watcher对象,在这里进行target判断,如果有,就使用dep.addSub()添加观察者。 Dep.target && dep.addSub(Dep.target); return value; }, set(newVal) { if (newVal === value) return; // 特别情况2:当设置data的某一个新属性值为对象时,也需要将其转换为getter和setter。 // 由于这是在set方法中,this指向并非vue实例,故需要使用外面的that指向vue实例的对象。 that.walk(newVal); value = newVal; // ____特别加入:在setter时,需要通知所有的观察者变化。 dep.notify(); }, }); } }

Compiler(编译器)类

功能:(操作dom) 负责编译模板,解析指令/插值表达式负责页面的首次渲染当数据变化后重新渲染视图

注意:这里没有使用虚拟dom,直接操作的dom。

类图:

解释说明:

el:传入的dom对象,后面需要渲染dom使用vm: vue实例,后面需要vm中的数据compiler(el):遍历el这个dom对象的所有节点,不同的节点调用解析文本节点或者元素节点的方法compileElement(node):解析元素节点,即解析元素中的指令compileText(node):解析文本节点,即解析差值表达式isDirective(attrName):判断元素节点上的属性 是否是 vue指令isTextNode(node):判断是否是文本节点isElementNode(node):判断是否是元素节点 代码: class Compiler { constructor(vm) { // 方便后面使用,将vm与el存储 this.vm = vm; this.el = vm.$el; // 编译模板 this.compiler(this.el); } // 1.遍历所有el中的节点 compiler(el) { // 注意:这里使用childNodes可以获取所有子节点,而children只能获取所有子元素。这里只获取一层的子节点。 const nodes = el.childNodes; // nodes是伪数组,需要转换成数组 Array.from(nodes).forEach((node) => { if (this.isElementNode(node)) { this.compileElement(node); } else if (this.isTextNode(node)) { this.compileText(node); } // 当子节点中还有其子节点时,需要递归调用 if (node.childNodes && node.childNodes.length) { this.compiler(node); } }); } // 1-1 判断是否文本节点 isTextNode(node) { return node.nodeType === 3; } // 1-2 判断是否元素节点 isElementNode(node) { return node.nodeType === 1; } // 1-3 解析差值表达式 compileText(node) { // 正则匹配 差值表达式 const reg = /\{\{(.+?)\}\}/; // node.textContent和node.nodeValue都是当前节点的内容 const content = node.textContent; if (reg.test(content)) { // 使用$1可获取正则表达式中小括号中的值 const value = RegExp.$1.trim(); // 差值表达式去掉双括号后,就是变量的名字,由于变量已经都在observer.js中挂载到了vm实例中,故这里取值 node.textContent = content.replace(reg, this.vm[value]); // ____数据响应添加:实例化Watcher对象 new Watcher(this.vm, value, (newValue) => { node.textContent = newValue; }); } } // 1-4 解析指令 // 首次渲染——只渲染v-text和v-model compileElement(node) { const attributes = node.attributes; Array.from(attributes).forEach((attr) => { let attrName = attr.name; if (this.isDirective(attrName)) { // 根据不同的指令进行不同的渲染 // 不采取if语句,使用字符串拼接方法名的方式,便于扩展更多的指令处理方法 attrName = attrName.substr(2); //从索引2开始截取到最后,获取到v-text指令名去除v-后的字符串。 // 获取该属性的属性值 const key = attr.value; // 解析不同的指令 this.update(node, key, attrName); } }); } // 1-1-1 判断是否vue指令 isDirective(attrName) { return attrName.startsWith("v-"); } // 负责更新Dom,update相当于一个watcher,统一执行不同指令解析的位置 update(node, key, attrName) { const updaterFn = this[attrName + "Updater"]; updaterFn && updaterFn.call(this, node, this.vm[key], key); } // v-text解析方法 textUpdater(node, value, key) { node.textContent = value; // ____数据响应添加:实例化Watcher对象 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue; }); } // v-model解析方法 modelUpdater(node, value, key) { node.value = value; // ____数据响应添加:实例化Watcher对象 new Watcher(this.vm, key, (newValue) => { node.value = newValue; }); // ____为了实现双向绑定,页面输入的变化,同步到数据,需要定义input方法 node.addEventListener("input", () => { this.vm[key] = node.value; }); } }

Dep(发布者)类

功能:

收集依赖,添加观察者(watcher)通知所有观察者

类图:

解释说明:

subs:用来存储所有观察者的数组属性

addSub(sub):添加观察者的方法

notify():通知所有观察者的方法

代码:

class Dep { constructor() { // 1.初始化subs存储所有观察者的数组 this.subs = []; } // 2.添加观察者 addSub(sub) { //判断是否有传递观察者,且约定观察者必须有update方法,方便在被通知后,调用update方法 if (sub && sub.update) { this.subs.push(sub); } } // 3.发送通知给所有观察者 notify() { this.subs.forEach((sub) => { sub.update(); }); } }

Watcher(观察者)类

功能:

当数据变化就触发依赖,dep通知所有的watcher实时更新视图自身实例化的时候往dep对象中添加自己。

类图:

解释说明:

update():方法,update中进行更新视图 cb:属性,不同的watcher更新的操作不一样。通过cb交代 vm:属性,通过vue实例获取属性对应的值 key:属性,当前观察的属性名称 oldValue:属性,存储变化之前的值

代码: class Watcher { constructor(vm, key, cb) { this.vm = vm; this.key = key; // 回调函数负责更新视图 this.cb = cb; // 1.实例化时,要把自己加入Dep类中, // 1.1 先将自己存储在target静态属性中 Dep.target = this; // 1.2 再通过取值操作,触发执行属性的getter,在observer.js文件中,属性的getter方法中会通过判断Dep.target属性来进行存储当前的watcher实例 this.oldValue = this.vm[key]; // 1.3 为了避免重复添加,每一次判断后,需要将Dep的target属性置空。 Dep.target = null; } // 2.更新视图 update() { //此时,vue实例对象中data中的key属性值已经变为最新的了 let newValue = this.vm[this.key]; if (newValue === this.oldValue) return; this.cb(newValue); } }

Watcher类在Compiler中的实例化

作用:为了实现数据改变=》发送通知=》改变视图,更新视图的时候需要调用update,但在update方法中是通过cb回调函数实现的,所以我们需要实例化Watcher类后,将cb回调函数定义,从而实现该功能。

Watcher类实例化的位置:compiler代码中,所有把数据渲染成dom的位置都需要创建一个watcher对象。

代码:(compiler文件中新增的代码)

class Compiler { constructor(vm) { this.vm = vm; this.el = vm.$el; this.compiler(this.el); } compiler(el) { const nodes = el.childNodes; Array.from(nodes).forEach((node) => { if (this.isElementNode(node)) { this.compileElement(node); } else if (this.isTextNode(node)) { this.compileText(node); } if (node.childNodes && node.childNodes.length) { this.compiler(node); } }); } isTextNode(node) { return node.nodeType === 3; } isElementNode(node) { return node.nodeType === 1; } compileText(node) { const reg = /\{\{(.+?)\}\}/; const content = node.textContent; if (reg.test(content)) { const value = RegExp.$1.trim(); node.textContent = content.replace(reg, this.vm[value]); new Watcher(this.vm, value, (newValue) => { node.textContent = newValue; }); } } compileElement(node) { const attributes = node.attributes; Array.from(attributes).forEach((attr) => { let attrName = attr.name; if (this.isDirective(attrName)) { attrName = attrName.substr(2); const key = attr.value; this.update(node, key, attrName); } }); } isDirective(attrName) { return attrName.startsWith("v-"); } update(node, key, attrName) { const updaterFn = this[attrName + "Updater"]; updaterFn && updaterFn.call(this, node, this.vm[key], key); } textUpdater(node, value, key) { node.textContent = value; // ____数据响应添加:实例化Watcher对象 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue; }); } modelUpdater(node, value, key) { node.value = value; // ____数据响应添加:实例化Watcher对象 new Watcher(this.vm, key, (newValue) => { node.value = newValue; }); // ____为了实现双向绑定,页面输入的变化,同步到数据,需要定义input方法 node.addEventListener("input", () => { this.vm[key] = node.value; }); } }

特别说明:

在写Dep类和Watcher类的时候,需要随之修改两个文件中的类 一是observer.js中的Observer类(因为在这个类中为data中的每一个属性转换为getter和setter形式)

原因是:考虑到需要为每一个data对象中的属性添加一个Dep实例,在getter方法中添加观察者(这里也会涉及辅助Watcher类中实例的存储),在setter方法中通知所有观察者做修改。

二是compiler.js中的Compiler类 (因为在这个类中主要做dom操作,数据的双向绑定都需要在这里进行修改)

原因是:Watcher类的实例化位置需要在Compiler类中,因为需要实现数据的变化。

整体流程

当页面首次加载的时候,是通过Compiler更新视图; 当页面数据变化的时候,是通过Watcher更新视图。

最新回复(0)