四、模拟 Vue 响应式原理
- vue 基本结构
- vue 实例对象
- 整体结构
- Vue: 把 data 转换成 gette/setter,并把 data 中的成员注入到 Vue 实例上.
- Observer: 能够对数据对象的所有属性进行监听,数据发生变动时会拿到最新值,并通知 Dep , Dep 会通知所有的 Watcher 进行更新.
- Dep & Watcher: 熟悉的观察者模式,Dep 负责把所有的观察者 Watcher 添加进来,Watcher 中的 update 方法负责视图的更新.
要模拟实现 Vue 的功能
- 1. 实现 Vue 类
- 负责接收初始化参数(选项)
- 负责把 data 中的数据注入到 Vue 实例上,并转换成对应的 gette/setter
- 负责调用 Observer 监听 data 中所有属性的变化
- 负责调用 Compile 解析 指令/插值表达式
class Vue { constructor(options) { // 1. 接收初始化参数(选项) options this.$options = options || {}; this.$data = options.data || {}; this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el; // 2. 把 data 中的数据注入到 Vue 实例上,并转换成对应的 gette/setter this._proxyData(this.$data); // 3. 调用 Observer 监听 data 中所有属性的变化 new Observer(this.$data); // 4. 调用 Compile 解析 指令/插值表达式 new Compiler(this); } _proxyData(data) { Object.keys(data).forEach((key) => { // 将 data 中的数据注入到 this 上 Object.defineProperty(this, key, { configurable: true, enumerable: true, get() { return data[key]; }, set(newVal) { if (data[key] === newVal) return; data[key] = newVal; }, }); }); } } 复制代码
- 2. 实现 Observer 类
- 负责把 data 中的属性转换为响应式
- 如果 data 中的属性为对象,也要将这个对象转换为响应式
- 当 data 中数据发生变化时,要发送通知
class Observer { constructor(data) { this.walk(data); } walk(data) { // 1. 判断 data 不为空 或者 不是一个对象 if (!data || typeof data !== "object") return; // 2. 否则遍历 data 中的所有属性 Object.keys(data).forEach((key) => { this.defineReative(data, key, data[key]); }); } // 调用 Object.defineProperty 将属性转换成 getter/setter defineReative(obj, key, val) { const that = this; // 收集依赖,发送通知 const dep = new Dep(); // 如果 val 是对象,那么把 val 内部的属性也转换成响应式数据 this.walk(val); Object.defineProperty(obj, key, { configurable: true, enumerable: true, get() { // 收集依赖 Dep.target && dep.addSub(Dep.target); return val; }, set(newVal) { if (newVal === val) return; val = newVal; // 防止当前属性被重新赋值为一个新对象时,失去响应式 that.walk(val); // 发送通知 dep.notify(); }, }); } } 复制代码
- 3. 实现 Compiler 类
- 负责编译模板,解析指令/插值表达式,实例化 Watcher 实例,触发 get 方法,向 Dep 添加 Watcher 实例
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
class Compiler { constructor(vm) { this.el = vm.$el; this.vm = vm; // 页面的初始化渲染 this.compile(this.el); } // 编译模板,处理文本节点和元素节点 compile(el) { let childNodes = el.childNodes; // childNodes 伪数组 Array.from(childNodes).forEach((node) => { if (this.isTextNode(node)) { // 处理文本节点 this.compileText(node); } else if (this.isElementNode(node)) { // 处理元素节点 this.compileElement(node); } // 判断 node 是否存在子节点,如果存在,要递归遍历子节点 if (node.childNodes && node.childNodes.length) { this.compile(node); } }); } // 编译元素节点,处理指令 compileElement(node) { // console.log(node.attributes); // 伪元素 // 遍历所有的属性节点 Array.from(node.attributes).forEach((attr) => { let attrName = attr.name; // 判断是否是指令 if (this.isDiretive(attrName)) { // 去除 v- 前缀,如:v-text ——> text attrName = attrName.substr(2); let key = attr.value; this.update(node, key, attrName); } }); } // 根据指令调用不同的 updater update(node, key, attrName) { let updateFunc = this[attrName + "Upater"]; updateFunc && updateFunc.call(this, node, this.vm[key], key); } // 处理 v-text 指令 textUpater(node, value, key) { node.textContent = value; // 创建 Watcher 对象,当数据改变更新视图 new Watcher(this.vm, key, (newVlaue) => { node.textContent = newVlaue; }); } // 处理 v-model 指令 modelUpater(node, value, key) { node.value = value; // 创建 Watcher 对象,当数据改变更新视图 new Watcher(this.vm, key, (newVlaue) => { node.value = newVlaue; }); // 注册事件 node.addEventListener("input", () => { this.vm[key] = node.value; }); } // 编译文本节点,处理插值表达式 compileText(node) { // console.dir(node); //以对象形式打印文本节点 // 用于匹配插值表达式,如:{{ msg }} let reg = /\{\{(.+?)\}\}/; let value = node.textContent; if (reg.test(value)) { // 用于获取正则表达式中匹配到的分组,并去除匹配内容前后的空格 let key = RegExp.$1.trim(); node.textContent = value.replace(reg, this.vm[key]); // 创建 Watcher 对象,当数据改变更新视图 new Watcher(this.vm, key, (newVlaue) => { node.textContent = newVlaue; }); } } // 判断元素属性是否是指令,判断属性是否是 v- 开头 isDiretive(attrName) { return attrName.startsWith("v-"); } // 判断节点是否为文本节点 isTextNode(node) { return node.nodeType === 3; } // 判断节点是否为元素节点 isElementNode(node) { return node.nodeType === 1; } } 复制代码
- 4. 实现 Dep(Dependcy) 类
- 收集依赖,添加观察者(Watcher)
- 依赖变化,通知观察者更新
class Dep { constructor() { this.subs = []; // 存储所有的观察者 } // 添加观察者 addSub(sub) { // 约定 sub 必须为 watcher 类 if (sub && sub.update) { this.subs.push(sub); } } // 通知观察者 notify() { this.subs.forEach((subs) => { subs.update(); }); } } 复制代码
- 5. 实现 Watcher 类
- 当数据变化触发依赖,Dep 通知所有的 Watcher 更新视图
- 在实例化自身时,往 Dep 中添加自己的实例
class Watcher { constructor(vm, key, cb) { this.vm = vm; // vue 的实例对象 this.key = key; // data 中属性的名称 this.cb = cb; // 回调函数,负责视图更新 // 1. 把当前的 Whatcher 实例记录在 Dep.target 这个静态属性中 Dep.target = this; // 2. 触发属性的 get 方法,在 get 方法中鬼调用 dep.addSub 方法添加观察者 this.oldVal = vm[key]; // data 中对应 key 上一次的值 // 3. 每次添加完 watcher 实例后,清空 Dep.target Dep.target = null; } // 当数据发生变化,更细视图 update() { let newVal = this.vm[this.key]; if (newVal === this.oldVal) return; this.cb(newVal); } } 复制代码
- 6. 测试功能
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>my-vue</title> </head> <body> <div id="app"> <h1>text</h1> <h2>{{count}}</h2> <h2>{{msg}}</h2> <hr /> <h1>v-text</h1> <h2 v-text="msg"></h2> <hr /> <h1>v-model</h1> <label for="msg"> msg:</label> <input id="msg" type="text" v-model="msg"> <label for="count">count:</label> <input id="count" type="text" v-model="count"> </div> <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 world', count: 1, person: { name: '张三', age: 30 } } }); </script> </body> </html>