本文讲解vue1.0的核心原理,以及我们自己手写1.0版本的Vue。
Tips:后面陆续会出vue2.0、vue3.0的核心原理以及代码实现
课前准备
Object.defineProperty(obj,key,attributes:PropertyDescriptor)- obj:想要设置的对象
- key:想要添加的key
- attributes:属性描述符
注意:vue1.0和vue2.0的核心都是通过
Object.defineProperty实现的,只有在vue3.0的时候代理换成了Proxy后面我们讲到3.0时会用Proxy,以及他们的不同点。
小实验
我们做这个小实验的目的就是,问了让大家看清楚vue的雏形是什么样子的,以及他的核心设计理念。很简单,没有什么特殊的语法,大家仔细看!!!
页面
假设,我们的页面是这样的:
顺便贴一下代码:
<!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">
<style>
.container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
margin: 0 auto;
}
button {
font-size: 22px;
}
.box {
font-size: 30px;
}
</style>
<title>Vue源码实现</title>
</head>
<body>
<div class="container">
<p class="box">0</p>
<div>
<button class="add">加</button> <button class="subtract">减</button>
</div>
</div>
<script>
var add = document.querySelector(".add"); // 加
var subtract = document.querySelector(".subtract"); // 减
var content = document.querySelector(".box"); // 显式内容
var data = {
count: 1
}
function update() {
content.innerHTML = data.count;
}
update();
add.onclick = function () {
data.count++;
}
subtract.onclick = function () {
data.count--;
}
</script>
</body>
</html>
看到这里,使用过vue的同学都知道,是不是只要我们点击加减按钮(也就是改变count的值),页面的值随之改变,我们就实现了vue,真正的声明式渲染。
主角登场
-
说一下思路
思考一下,这时候是不是只要我们修改count的时候拦截到这一操作时再使用update函数,是不是就可以实现。这时候就用到了我们的主角
Object.defineProperty() -
增加代码
<!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>Vue源码实现</title> </head> <body> <div class="container"> <p class="box">0</p> <div> <button class="add">加</button> <button class="subtract">减</button> </div> </div> <script> var add = document.querySelector(".add"); // 加 var subtract = document.querySelector(".subtract"); // 减 var content = document.querySelector(".box"); // 显式内容 var data = { count: 1 } function update() { content.innerHTML = data.count; } update(); add.onclick = function () { data.count++; } subtract.onclick = function () { data.count--; } // 核心代码 function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { get() { return value; }, set(newVal) { if (newVal !== value) { value = newVal update() } } }) } Object.keys(data).forEach(key => { defineReactive(data, key, data[key]); }) </script> </body> </html> -
代码解析
这段代码中,我们定义了
defineReactive方法,然后遍历data中的属性设置到data上面,再利用js语言中闭包的特性,将value保存下来,修改和访问的时候,直接修改的是我们内存当中的值,从而实现了我们的需求
环境搭建
环境很简单,一个html文件,一个js文件足矣。
- index.html中
<!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"> <script src="./vue.js"></script> <title>Vue源码实现</title> </head> <body> <div id="app">{{count}}</div> <script> const app = new Vue({ el: "#app", data: { count: 1 } }) setInterval(() => { console.log(app.count); app.count++ }, 2000) </script> </body> </html>
是不是只要我们可以让html中的例子跑起来,也就是页面中的count每2s加1那么我们就成功实现了Vue!!!!想想是不是很激动,到这里是不是已经有点思路了呢?别急我们一步步来。
vue1.0实现
通过上面的小实验我们可以得出结论,当我们this.count = xxx(也就是set)的时候,就是调用页面中元素相关的update方法,从而触发页面更新,这也就是vue的核心机制。这句话不理解没关系,只需要记住就行。
我们Vue的实现分如下几步:
- 接收用户参数
- 响应式处理
- 代理
- 模板编译
- 依赖收集
其实在源码中,初始化的时候核心就是这几步。 基础代码结构:
class Vue {
constructor(options) {
// 1.接收用户参数
// 2.响应式处理
// 3.代理
// 4.模板编译
}
}
1.接收用户参数
- options就是用户传入的参数,也就是
new Vue({})时传入构造函数的参数
class Vue {
constructor(options) {
// 1.接收用户参数
this.$options = options;
this.$data = options.data;
// 2.响应式处理
// 3.代理
// 4.模板编译
}
}
2.响应式处理
-
什么是响应式处理
vue是典型的MVVM框架,当我们
this.count = xxx时,页面会重新render,而想要实现这一功能,响应式处理是必不可少的一步。 -
代码实现
class Vue { constructor(options) { // 1.接收用户参数 this.$options = options; this.$data = options.data; // 2.响应式处理 observe(this.$data); // 3.代理 // 4.模板编译 } } function observe(obj) { if (Array.isArray(obj)) { // TODO 数组的响应式处理 } else if (typeof obj === "object") { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } } function defineReactive(obj, key, value) { Object.defineProperty(obj, key, { get() { console.log("get", key);// 如果访问这个属性控制到会输出 return value; }, set(newVal) { if (newVal !== value) { console.log("set", key);// 如果设置这个属性控制台会输出 value = newVal; } }, }); } -
成果测试
此时我们的
setInterVal函数一直在访问app.count。按照我们代码中的逻辑控制台会输出:-
期望
-
结果
想一下我们为什么会出现这样的结果?
因为我们还没做代理,
app实例上面是没有count这个属性的,所以你是访问不到。我们可以修改一下代码,此时你就可以看到控制台正确输出了:
//index.html文件中 setInterval(() => { console.log(app.count) app.count++ }, 1000) // 修改为 setInterval(() => { console.log(app.$data.count) app.$data.count++ }, 1000) -
3.代理
-
什么是代理
我们可以通过
app.xxx访问到data中的某个属性,这就是代理的作用成果const app = new Vue({ count:1 }) console.log(app.count)//1 -
代码实现
class Vue { constructor(options) { // 1.接收用户参数 this.$options = options; this.$data = options.data; // 2.响应式处理 observe(this.$data); // 3.代理 proxy(this); // 4.模板编译 } } function observe(obj) {... } function defineReactive(obj, key, value) {... } function proxy(instance) { Object.keys(instance.$data).forEach((key) => { defineReactive(instance, key, instance.$data[key]); }); }
4.模板编译
-
什么是模板编译
{{}}我们称之为插值语法<div>{{count}}</div>编译为<div>2</div>,这就是模板编译
-
实现思路
模板编译的核心就是,将
template里面的{{}}(也就是插值),替换成data中的数据。就这么简单!!! -
代码实现
class Vue { constructor(options) { // 1.接收用户参数 this.$options = options; this.$data = options.data; // 2.响应式处理 observe(this.$data); // 3.代理 proxy(this); // 4.模板编译 new Compile(this, this.$el); } } function observe(obj) {... } function defineReactive(obj, key, value) {... } function proxy(instance) {... } class Compile { constructor(vm, el) { this.$vm = vm; this.$el = document.querySelector(el); this.compile(this.$el); } compile(el) { el.childNodes.forEach((node) => { // 判断是元素 || 插值语法 1元素 2方法 if (node.nodeType === 1) { // TODO 处理元素上面的指令 if (node.childNodes.length > 0) { this.compile(node); } console.log("处理元素"); } else if (this.isInter(node)) { this.compileText(node); } }); } compileText(node) { node.textContent = this.$vm[RegExp.$1]; } // 判断是否是插值语法 isInter(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); } } -
成果演示
-
代码讲解:
Compile这个类的构造函数接收俩个参数,vm(实例)、el(类名)compile这个实例方法,主要是遍历元素节点,区分节点是插值还是元素,分别执行对应的编译方法。同时我们处理元素的时候,因为可能会有子元素,所以要递归处理
5.依赖收集
初始化的过程在我们的前四步已经完成,但是我们的count不能修改,接下来最后一步也是最重要的一步依赖收集,
结合小实验,也就是收集我们的
update函数,利用闭包的特性从而完成更新。
-
代码实现步骤
- 实现
Dep、Watcher这俩个类(一个key对应一个Dep,一个Dep里面可能有多个watcher) - 在响应式处理的时候,实例化
Dep - 模板编译的时候实例化
Watcher Dep、Watcher之间是发布订阅关系,利用这层关系,实现依赖收集。
- 实现
-
依赖收集初始化
响应式处理时,实例化dep
=>get时,调用dep.addDep()=>模板编译实例化Watcher=>Watcher实例化时通过访问vm[key]触发get函数完成收集 -
依赖收集运行时
当app.count++时
=>调用当前key对应Dep.notify()方法=>notify遍历内部的Watcher=>Watcher调用内部的update方法 -
代码实现
class Vue { constructor(options) { // 1.接收用户参数 this.$options = options; this.$data = options.data; this.$el = options.el; // 2.响应式处理 observe(this.$data); // 3.代理 proxy(this); // 4.模板编译 new Compile(this, this.$el); } } /** * 响应式处理 */ function observe(obj) { if (Array.isArray(obj)) { // TODO 数组的响应式处理 } else if (typeof obj === "object") { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } } function defineReactive(obj, key, value) { const dep = new Dep(); Object.defineProperty(obj, key, { get() { Dep.target && dep.addDep(Dep.target); return value; }, set(newVal) { if (newVal !== value) { value = newVal; dep.notify(); } }, }); } /** * 代理 */ function proxy(instance) {... } /** * 模板编译 */ class Compile { constructor(vm, el) { this.$vm = vm; this.$el = document.querySelector(el); this.compile(this.$el); } compile(el) { el.childNodes.forEach((node) => { // 判断是元素 || 插值语法 1元素 2方法 if (node.nodeType === 1) { // TODO 处理元素 if (node.childNodes.length > 0) { this.compile(node); } console.log("处理元素"); } else if (this.isInter(node)) { // TODO 处理模板插值 this.compileText(node); } }); } compileText(node) { const fn = this.textUpdater; new Watcher(this.$vm, RegExp.$1, function () { return fn(node, this.$vm[RegExp.$1]); }).update(); } textUpdater = (node, val) => { console.log(this); node.textContent = val; }; // 判断是否是插值语法 isInter(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent); } } /** * 依赖收集 */ class Dep { constructor() { this.deps = []; } addDep(watcher) { this.deps.push(watcher); } notify() { this.deps.forEach((fn) => { fn && fn.update(); }); } } class Watcher { constructor(vm, key, fn) { this.$vm = vm; this.$fn = fn; Dep.target = this; this.$vm[key]; Dep.target = null; } update() { this.$fn && this.$fn(); } } -
成果展示:每2秒自动加1
完整代码
class Vue {
constructor(options) {
// 1.接收用户参数
this.$options = options;
this.$data = options.data;
this.$el = options.el;
// 2.响应式处理
observe(this.$data);
// 3.代理
proxy(this);
// 4.模板编译
new Compile(this, this.$el);
}
}
/**
* 响应式处理
*/
function observe(obj) {
if (Array.isArray(obj)) {
// TODO 数组的响应式处理
} else if (typeof obj === "object") {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
function defineReactive(obj, key, value) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.notify();
}
},
});
}
/**
* 代理
*/
function proxy(instance) {
Object.keys(instance.$data).forEach((key) => {
defineReactive(instance, key, instance.$data[key]);
});
}
/**
* 模板编译
*/
class Compile {
constructor(vm, el) {
this.$vm = vm;
this.$el = document.querySelector(el);
this.compile(this.$el);
}
compile(el) {
el.childNodes.forEach((node) => {
// 判断是元素 || 插值语法 1元素 2方法
if (node.nodeType === 1) {
// TODO 处理元素
if (node.childNodes.length > 0) {
this.compile(node);
}
console.log("处理元素");
} else if (this.isInter(node)) {
// TODO 处理模板插值
this.compileText(node);
}
});
}
compileText(node) {
const fn = this.textUpdater;
new Watcher(this.$vm, RegExp.$1, function () {
return fn(node, this.$vm[RegExp.$1]);
}).update();
}
textUpdater = (node, val) => {
console.log(this);
node.textContent = val;
};
// 判断是否是插值语法
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
/**
* 依赖收集
*/
class Dep {
constructor() {
this.deps = [];
}
addDep(watcher) {
this.deps.push(watcher);
}
notify() {
this.deps.forEach((fn) => {
fn && fn.update();
});
}
}
class Watcher {
constructor(vm, key, fn) {
this.$vm = vm;
this.$fn = fn;
Dep.target = this;
this.$vm[key];
Dep.target = null;
}
update() {
this.$fn && this.$fn();
}
}
总结
文章可能总结的不是很好,如果有问题的话可以在评论区留言,我看到就会回复,也可以加我微信15735090985一起讨论。后续会出vue2.0以及vue3.0。