当你把一个一般的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并应用 Object.defineProperty
把这些 property 全副转为 getter/setter。
数据劫持
vue2中数据劫持应用了Object.defineProperty
来实现Object.defineProperty(obj, prop, descriptor)
参数
- obj要定义属性的对象。
- prop要定义或批改的属性的名称或 Symbol 。
- descriptor要定义或批改的属性描述符。
返回值
被传递给函数的对象。
模仿vue中的data选项
// 模仿 Vue 中的 data 选项 let data = { msg: 'hello' } // 模仿 Vue 的实例 let vm = {} // 数据劫持:当拜访或者设置 vm 中的成员的时候,做一些干涉操作 Object.defineProperty(vm, 'msg', { // 可枚举(可遍历) enumerable: true, // 可配置(能够应用 delete 删除,能够通过 defineProperty 从新定义) configurable: true, // 当获取值的时候执行 get () { console.log('get: ', data.msg) return data.msg }, // 当设置值的时候执行 set (newValue) { console.log('set: ', newValue) if (newValue === data.msg) { return } data.msg = newValue // 数据更改,更新 DOM 的值 document.querySelector('#app').textContent = data.msg } }) // 测试 vm.msg = 'Hello World' console.log(vm.msg)
当data对象中有多个属性时,此时须要遍历vm,把每一个属性都转换成vm中的getter和setter
// 模仿 Vue 中的 data 选项 let data = { msg: 'hello', count: 10 } // 模仿 Vue 的实例 let vm = {} proxyData(data) function proxyData(data) { // 遍历 data 对象的所有属性 Object.keys(data).forEach(key => { // 把 data 中的属性,转换成 vm 的 setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // 数据更改,更新 DOM 的值 document.querySelector('#app').textContent = data[key] } }) }) } // 测试 vm.msg = 'Hello World' console.log(vm.msg)
vue3中的数据劫持应用了ES6中的Proxy来实现,他能够代理一整个对象const p = new Proxy(target, handler)
参数
- target要应用 Proxy 包装的指标对象(能够是任何类型的对象,包含原生数组,函数,甚至另一个代理)。
- handler一个通常以函数作为属性的对象,各属性中的函数别离定义了在执行各种操作时代理 p 的行为。即执行代理行为的函数
MDN代码示例
const handler = { get: function(obj, prop) { return prop in obj ? obj[prop] : 37; }};const p = new Proxy({}, handler);p.a = 1;p.b = undefined;console.log(p.a, p.b); // 1, undefinedconsole.log('c' in p, p.c); // false, 37
测试示例
<div id="app"> hello </div> <script> // 模仿 Vue 中的 data 选项 let data = { msg: 'hello', count: 0 } // 模仿 Vue 实例 let vm = new Proxy(data, { // 执行代理行为的函数 // 当拜访 vm 的成员会执行 get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // 当设置 vm 的成员会执行 set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } }) // 测试 vm.msg = 'Hello World' console.log(vm.msg)
模仿VUE中的公布订阅模式
公布/订阅模式
- 订阅者
- 发布者
- 信号核心
咱们假设,存在一个"信号核心",某个工作执行实现,就向信号核心"公布"(publish)一个信
号,其余工作能够向信号核心"订阅"(subscribe)这个信号,从而晓得什么时候本人能够开始执
行。这就叫做"公布/订阅模式"(publish-subscribe pattern)
// 事件触发器 class EventEmitter { constructor () { // { 'click': [fn1, fn2], 'change': [fn] } this.subs = Object.create(null) } // 注册事件 $on (eventType, handler) { this.subs[eventType] = this.subs[eventType] || [] // 初始化为数组,不便前面的push操作 this.subs[eventType].push(handler) } // 触发事件 $emit (eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach(handler => { handler() }) } } } // 测试 let em = new EventEmitter() em.$on('click', () => { console.log('click1') }) em.$on('click', () => { console.log('click2') }) em.$emit('click')
模仿VUE中的观察者模式
观察者(订阅者) -- Watcher
- update():当事件产生时,具体要做的事件
指标(发布者) -- Dep
- subs 数组:存储所有的观察者
- addSub():增加观察者
- notify():当事件产生,调用所有观察者的 update() 办法
没有事件核心
代码模式实现
// 发布者-指标 class Dep { constructor () { // 记录所有的订阅者 this.subs = [] } // 增加订阅者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 公布告诉 notify () { this.subs.forEach(sub => { sub.update() // 调用订阅者办法 }) } } // 订阅者-观察者 class Watcher { update () { console.log('update') } } // 测试 let dep = new Dep() let watcher = new Watcher() dep.addSub(watcher) dep.notify()
- 观察者模式是由具体指标调度,比方当事件触发,Dep 就会去调用观察者的办法,所以观察者模式的订阅者与发布者之间是存在依赖的。
公布/订阅模式由对立调度核心调用,因而发布者和订阅者不须要晓得对方的存在。
Vue 响应式原理模仿
- Vue.把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
- Observer.可能对数据对象的所有属性进行监听,如有变动可拿到最新值并告诉 Dep
- Compiler.解析每个元素中的指令/插值表达式,并替换成相应的数据
- Dep.增加观察者(watcher),当数据变动告诉所有观察者
- Watcher.数据变动更新视图
Vue类
性能
- 负责接管初始化的参数(选项)
- 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
- 负责调用 observer 监听 data 中所有属性的变动
负责调用 compiler 解析指令/插值表达式
class Vue {constructor(options) { // 1,通过属性保留选项的数据 this.$options = options || {}; // 初始化 this.$data = options.data || {}; this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el; // 2,把data中的成员转换成getter和setter,注入到vue实例中 this._proxyData(this.$data); // 3,调用observer对象,监听数据变动 // 4,调用compiler对象,解析指令和差值表达式}_proxyData(data) { // 遍历data中的所有属性 Object.keys(data).forEach((key) => { // 把data中的属性注入到vue实例中 Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { return data[key]; }, set(newVal) { if (newVal === data[key]) { return; } data[key] = newVal; }, }); });}}
测试代码
<!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>myVue</title></head><body> <div class="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> <script src="./myVue.js"></script> <script> let vm = new Vue({ el: "#app", data: { msg: "my vue", count: 1 } }) </script></body></html>
Observer类
性能
- 负责把 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.defineReactive(data, key, data[key]); });}// 调动Object.defineProperty把属性转换为getter和setterdefineReactive(obj, key, val) { let that = this; // Dep类在Observer类做了两件事,先收集依赖,而后告诉。 let dep = new Dep(); // 负责收集依赖,并发送告诉 this.walk(val); // 解决问题一, data属性是对象类型, 如果val是对象,将val外部属性转换成响应式数据, Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依赖 Dep.target && dep.addSub(Dep.target); return val; // 这里如果是return obj[key]会产生死递归,返回的就是obj自身 }, set(newVal) { if (newVal === val) { return; } val = newVal; that.walk(newVal); // 解决问题二, 如果新赋值类型是对象类型,也须要循环解决,转换成响应式数据 // 发送告诉 dep.notify(); }, });}}
在vue.js中引入observer
// 3,调用observer对象,监听数据变动 new Observer(this.$data);
控制台打印vm对象,对应的data对象属性曾经转化为getter和setter
Compiler类
性能(操作DOM)
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变动后从新渲染视图
class Compiler { constructor(vm) { this.el = vm.$el; this.vm = vm; this.compile(this.el); } // 编译模板,解决文本节点和元素节点 compile(el) { let childNodes = el.childNodes; Array.from(childNodes).forEach((node) => { // 对节点类型进行判断 // 解决文本节点 if (this.isTextNode(node)) { this.compileText(node); } else if (this.isElementNode(node)) { // 解决元素节点 this.compileElement(node); } // 判断node节点是否有子节点,有子节点判断递归compiler if (node.childNodes && node.childNodes.length) { this.compile(node); } }); } // 编译元素节点,解决指令,这里处,理v-text,v-model compileElement(node) { console.log(node.attributes); // 遍历所有的属性节点 Array.from(node.attributes).forEach((attr) => { // 判断是否是指令 let attrName = attr.name; if (this.isDirective(attrName)) { // 解决指令 //v-text ==>text attrName = attrName.substr(2); // 属性名 let key = attr.value; // 属性值 this.update(node, key, attrName); } }); } update(node, key, attrName) { let updateFn = this[attrName + "Update"]; // 使this指向compiler对象 updateFn && updateFn.call(this, node, this.vm[key], key); } // 解决 v-text 指令 textUpdate(node, value, key) { node.textContent = value; // 这里如法炮制,在创立指令的时候创立watcher对象 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue; }); } // 解决v-model指令 modelUpdate(node, value, key) { node.value = value; // 这里如法炮制,在创立指令的时候创立watcher对象 new Watcher(this.vm, key, (newValue) => { node.value = newValue; }); // 双向绑定,文本框值批改,视图扭转,批改数据 node.addEventListener("input", () => { this.vm[key] = node.value; }); } // 编译文本节点,解决差值表达式 compileText(node) { // console.dir(node) let reg = /\{\{(.+?)\}\}/; let value = node.textContent; if (reg.test(value)) { let key = RegExp.$1.trim(); // 把msg中的值取出来,替换差值表达式,赋值给 node.textContent node.textContent = value.replace(reg, this.vm[key]); // 在创立实例的时候, 创立watcher对象,当数据扭转更新视图 new Watcher(this.vm, key, (newVal) => { node.textContent = newVal; }); } } // 判断元素节点是否是指令 isDirective(attrName) { return attrName.startsWith("v-"); } // 判断节点是否是文本节点 isTextNode(node) { return node.nodeType === 3; } // 判断节点是否是元素节点 isElementNode(node) { return node.nodeType === 1; }}
Vue中的响应式,Vue负责把data中的属性存储到实例,并且调用Observer和Compiler,Observer负责数据劫持,监听数据变动,把data中的数据转换为getter和setter。Compiler负责解析差值表达式和指令,这外面须要应用观察者模式监听数据的变动
创立Dep和Watcher
Dep类
性能
- 收集依赖,增加观察者(watcher)
告诉所有观察者
class Dep {constructor() { // 存储所有的观察者 this.subs = [];}// 增加观察者addSub(sub) { // 判断是否为观察者对象 if (sub && sub.update) { this.subs.push(sub); }}// 发送告诉notify() { this.subs.forEach((sub) => { sub.update(); });}}
Watcher类
性能
- 当数据变动触发依赖, dep 告诉所有的 Watcher 实例更新视图
本身实例化的时候往 dep 对象中增加本人
class Watcher {constructor(vm, key, cb) { this.vm = vm; // data中的属性名称 this.key = key; // 回调函数负责更新视图 this.cb = cb; // 把watcher对象记录到Dep类的动态属性target Dep.target = this; // 触发get办法,在get办法中会调用addSub this.oldValue = vm[key]; Dep.target = null; // 避免反复增加}// 当数据发生变化时更新视图update() { let newValue = this.vm[this.key]; if (this.oldValue === newValue) { return; } this.cb(newValue);}}
总结