深入理解 Vue.js 原理
文章目录
- 前言
- zvue.js 架构图
- Object.defineProperty()
- 读取数据:vm[key]
- 更新数据
- 完整 zvue.js 代码
前言
前一篇 简单实现vue.js 一文中,我们实现一个简化版的 Vue.js(zvue.js),本文来深入研究 vue 结构及实现原理
zvue.js 架构图
先上一张 zvue.js 架构图,本文我们就对此详细展开研究:
Object.defineProperty()
在上面代码中,有两处使用到 Object.defineProperty()
方法,一个是 Vue 类中的 proxy()
方法,一个是 Observer 类中的 doDefProp()
,其本质上是一个装饰器,给一个数据包装成一个有 get/set
方法的对象,并在 get/set
方法中加入装饰逻辑
,一旦有 读取或更新
数据时触发执行到装饰逻辑。
Vue 类中的 proxy()
方法比较简单,没有什么装饰逻辑,其实就是想要 无中生有
,把 Vue.$data
下的属性当作是 Vue 的自己属性
,即实现 vm.xx==vm.$data.xx
,这里起到的就是代理作用,简化省掉$data
而已。
类似 java 中的 getXXX/setXXX() 方法,可以没有 XXX 字段
// data 来源于vue.$dataproxy(target, data) {Object.keys(data).forEach((key) => {Object.defineProperty(target, key, {enumerable: true,configurable: true,//目的:this.xx==this.$data.xxget() {console.log("proxy -1- get data[key]:", data[key]);return data[key];},set(newVal) {console.log("proxy -2- set data[key]=", newVal);data[key] = newVal;},});});}
Observer 类中的 doDefProp()
,是给 data 每个 key 对应的数据 value
进行封装成一个对象,有 get/set 逻辑
例如 value 本身是一个姓名字符串
“张三”,经过 Object.defineProperty
摇身一变成了一个 Person 对象
,取值设置要调用里面的 get/set 方法
:
并且还可以夹带私货
(装饰逻辑),例如代码中的 Dep.target && dep.addSub(Dep.target);
,这个可以说是 Vue 的灵魂所在,后面我们会看到这里 target
其实就是 Watcher
对象,data 的每个 key 值都有一个 Watcher 监视器
,监听 value 的变化。
// 会放到dep里面相关联,dep是传参进来的,与Observer 对应绑定doDefProp(data, key, val, dep) {Object.defineProperty(data, key, {enumerable: true,configurable: true,get() {// Dep.target 是公用的console.log("doDefProp -1- get data[key] --> dep.addSub", Dep.target);// 加入列表Dep.target && dep.addSub(Dep.target);return val;},set(newVal) {console.log("doDefProp -2- set data[key]=", newVal);if (val === newVal) return;val = newVal;if (typeof val === "object" && val !== null) {new Observer(val);}// 通知监视器更新 token 流,实现页面渲染更新dep.notify();},});}
注意
这里要划重点了,在 读取 key(get)
时添加 监视器 Watcher
到观察者列表
// 加入列表Dep.target && dep.addSub(Dep.target);
在更新 key(set)
时候通知 监视器 Watcher
进行 token 流替换,从而实现页面渲染更新。
// 通知监视器更新 token 流,实现页面渲染更新dep.notify();
在浏览器中可以查看到 $data 的信息,一点开里面的 message
,立刻执行了装饰逻辑,打印出日志 doDefProp -1- get data[key] --> dep.addSub null
读取数据:vm[key]
在 Watcher
构造函数中 有一句 this.oldVal = vm[key];
,
class Watcher {constructor(vm, key, callback) {this.vm = vm;this.key = key;this.callback = callback;console.log("Watcher.constructor -1- Dep.target = this", this);Dep.target = this;console.log("Watcher.constructor -2- this.oldVal = vm[key]");this.oldVal = vm[key];console.log("Watcher.constructor -3- Dep.target = null");Dep.target = null;}...
}
通过日志可以看出其调用的路径,vm[key] -> get -> dep.addSub
zvue.js:119 Watcher.constructor -2- this.oldVal = vm[key]
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- : Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:36 proxy -1- get data[key]: Hello world!
zvue.js:72 doDefProp -1- get data[key] --> dep.addSub Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:96 Dep.addsub -2- : Watcher {vm: Vue, key: 'message', callback: ƒ}
zvue.js:121 Watcher.constructor -3- Dep.target = null
更新数据
在浏览器中手动执行 app.message=1234
,页面渲染马上变成 1234--1234
,执行路径为 set -> Dep.notify -> Watcher.update
,到此就完成页面的更新了
完整 zvue.js 代码
zvue.js
class Vue {constructor(options) {// $xx 表示对象this.$options = options || {};this.$data = options.data || {};// el 可能为字符串或对象this.el = options.el;// el 对象this.$el =typeof this.el == "string" ? document.querySelector(this.el) : this.el;//属性注入vue实例(setX),即data属性变成Vue的对象console.log("Vue.constructor -1- proxy(this, this.$data)", this);this.proxy(this, this.$data);//observer 观察dataconsole.log("Vue.constructor -2- new Observer(this.$data)", this);new Observer(this.$data);//dom视图解析console.log("Vue.constructor -3- new Compiler(this, this.el)", this);new Compiler(this, this.$el);}// // data 来源于vue.$dataproxy(target, data) {Object.keys(data).forEach((key) => {Object.defineProperty(target, key, {enumerable: true,configurable: true,//目的:this.xx==this.$data.xxget() {console.log("proxy -1- get data[key]:", data[key]);return data[key];},set(newVal) {console.log("proxy -2- set data[key]=", newVal);data[key] = newVal;},});});}
}class Observer {constructor(data) {console.log("Observer.constructor -1- data", data);this.data = data;// 注意: Dep new 出来了this.dep = new Dep();// 重点this.walk(data, this.dep);}walk(data, dep) {console.log("Observer.walk -2- this:", this);//data中属性包装成对象Object.keys(data).forEach((key) =>this.doDefProp(data, key, data[key], dep));}// 会放到dep里面相关联,dep是传参进来的,与Observer 对应绑定doDefProp(data, key, val, dep) {Object.defineProperty(data, key, {enumerable: true,configurable: true,get() {// Dep.target 是公用的console.log("doDefProp -1- get data[key] --> dep.addSub", Dep.target);Dep.target && dep.addSub(Dep.target);return val;},set(newVal) {console.log("doDefProp -2- set data[key]=", newVal);if (val === newVal) return;val = newVal;if (typeof val === "object" && val !== null) {new Observer(val);}dep.notify();},});}
}// 依赖收集器
class Dep {constructor() {console.log("Dep.constructor -1-");this.subs = [];}addSub(sub) {console.log("Dep.addsub -2- : ", sub);this.subs.push(sub);}notify() {console.log("Dep.notify -3- : ", this.subs);Array.from(this.subs).forEach((sub) => {sub.update();});}
}Dep.target = null;// 观察者
class Watcher {constructor(vm, key, callback) {this.vm = vm;this.key = key;this.callback = callback;console.log("Watcher.constructor -1- Dep.target = this", this);Dep.target = this;console.log("Watcher.constructor -2- this.oldVal = vm[key]: ", vm[key]);this.oldVal = vm[key];console.log("Watcher.constructor -3- Dep.target = null");Dep.target = null;}update() {const newVal = this.vm[this.key];console.log("Watcher.update -4- newVal:", newVal);if (newVal === this.oldVal) return;this.callback(newVal);this.oldVal = newVal;}
}// 编译模板
class Compiler {constructor(vm, el) {this.vm = vm;this.el = el;if (this.el) {this.compile(this.el);}}compile(el) {const childNodes = el.childNodes;Array.from(childNodes).forEach((node) => {if (node.nodeType === 3) {// 插值形式,进行替换this.compileText(node);} else if (node.nodeType === 1) {// 元素类型,解析属性}// 递归解析if (node.childNodes && node.childNodes.length) {this.compile(node);}});}// 替换 {{key}} -> valuecompileText(node) {const reg = /\{\{(.+?)\}\}/g;const value = node.textContent.replace(/\s/g, "");const tokens = [];let result,index,lastIndex = 0;while ((result = reg.exec(value))) {console.log("Compiler.compileText -1- result:", result);index = result.index;if (index > lastIndex) {tokens.push(value.slice(lastIndex, index));}const key = result[1].trim();tokens.push(this.vm[key]);lastIndex = index + result[0].length;const pos = tokens.length - 1;console.log("Compiler.compileText -2- new Watcher");new Watcher(this.vm, key, (newVal) => {console.log("Compiler.compileText -3- tokens[pos] = newVal",tokens,pos,newVal);tokens[pos] = newVal;node.textContent = tokens.join("");});}if (lastIndex < value.lenth) {tokens.push(value.slice(lastIndex));}if (tokens.length) {node.textContent = tokens.join("");}}
}