本文适用于想探究vue工作原理的同学,大家不妨将其作为一个入门vue原理的文章,跟着一步一步实现,到最后看到结果,内心幸福感满满。这仅仅是我一些感悟,如果有不对的地方,希望各位大佬能指正。
1.vue的工作机制
我们在初始化vue实例之后,执行$mount,vue就会开始编译模板语法。
因为浏览器并不认识我们写的双大括号,v-xxx,自定义组件一类的指令,编译的过程就是将这些指令转化为符合浏览器标准的语法。这就是下图中所示的compile过程。由此可以vue在背后默默做了很多工作,才让我们工作起来上手这么容易。
在编译这个阶段我们会生成渲染函数,或者说是更新函数,我们会产生一个虚拟的dom树,之后我们更新的操作首先都是作用于这个虚拟的树上的。当更新之前我们会做一个diff算法的比较,从而计算出我们需要进行最小的更新,到patch()去打补丁,渲染出真实的页面。
浏览器的瓶颈出现在dom操作,页面渲染这个阶段。vue的核心目的就是用js计算时间去换dom操作的时间,减少页面渲染的次数。
compile()除了生成渲染函数外还有一个最重要的功能,就是依赖收集。页面中元素是怎么和我们的数据模型发生关系的呢,当数据发生变化之后页面又是怎么响应式的更新的呢?这一部分是我们接下来要实现的重点,也是vue的核心部分。

好了,废话不多说,代码永远是别人的,只有自己撸一遍才是自己的。
talk is cheap,show me the code.
新建一个index.html,我们先来认识一下vue2.x中进行数据侦测的一个方法:Object.defineProperty()。
<html lang="en">
<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>Document</title>
</head>
<body>
<div id="app">
<p id="name"></p>
</div>
<script>
var obj = {};
Object.defineProperty(obj,"name",{
get(){
return document.getElementById("name").innerHTML
},
set(newVal){
document.getElementById("name").innerHTML = newVal
}
})
obj.name = "Link"
</script>
</body>
</html>
输出:

class LinkVue {
constructor(option) {
this.$el = option.el;
this.$data = option.data
this.observe(this.$data)
}
}
oberserve()接收的是一个对象,当然也可以是一个函数的返回对象,我这里没有做处理,简单的当作用户传入了一个对象处理。在对data进行遍历之后,我们写一个defineReactive(),这个函数用来定义响应式。
observe(data){
//判断传入数据的合法性
if(data&&typeof(data)=="object"){
//对data遍历
Object.keys(data).forEach((key)=>{
this.defineReactive(data,key,data[key])
})
}
}
在defineReactive()这个函数里面我们就用到了一开始讲到的Object.defineProperty()。这个方法对data的每一个属性都设置了getter和setter.当读取data中的属性时会触发get方法,当数据发生改变时会触发set()。另外为了能够深度监听data中的属性变化,我们要在defineReactive()里递归一下observe函数。
defineReactive(data, key, value) {
//循环遍历data,直至不是一个对象
this.observe(value)
Object.defineProperty(data, key, {
get() {
return value
},
set(newVal) {
if (value === newVal) {
return
}
value = newVal
console.log(key + "更新成为了" + newVal)
}
})
}
我在set的时候加了一个log,这可以让我们阶段性的看到我们的结果。修改index.html如下:
<html lang="en">
<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>Document</title>
</head>
<body>
<script src="LinkVue.js"></script>
<div id="app"> </div>
<script>
const app = new LinkVue({
el: "#app",
data: {
firstdata: "have a try",
fullname: {
fisrname: "Link"
}
}
})
app.$data.firstdata = "changeFirstData"
app.$data.fullname.fisrname = "Lin"
</script>
</body>
</html>
输出结果:

接下来我们实现依赖收集的这部分工作。
我们需要新写一个类,用来保存收集的观察者(watcher)。可以新写一个js,也可以直接写在这个LinkVue.js里面。我们为了更加清晰,还是写在这个js里面。在构造函数里面我们初始化一个数组,这个数组用来存放watcher。这里的对应关系时data中的一个属性对应一个deps,template解析后出现几个这个data中的属性,那这个deps里面就有几个watcher。
Dep这个类里面有两个成员方法,addDep()就是添加操作,而notify()是通知操作,通知deps[]里面的watcher进行更新。
这里面可能有点绕,我一开始也不是很明白Dep是怎么和Watcher发生联系的,没事,我们先向下写,这部分我们稍后会说明。
class Dep {
constructor() {
//数组里面存放的时watcher
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach((dep) => dep.update())
}
}
我们还需要一个watcher类,我们在这个类里面进行update操作。其中将Dep与Watcher链接起来的是构造函数里面的Dep.target = this;将watcher实例指向Dep的静态属性target。成员方法update()我们暂时不需要它做什么,可以打印一下,验证一下我们到现在做的是否正确。
class Watcher {
constructor() {
//将当前watcher的实例指定到Dep的静态属性target下
Dep.target = this;
}
update() {
console.log("执行了update方法");
}
}
我们修改一下我们的 class LinkVue,在定义响应化的defineReactive()函数里面添加初始化依赖收集的方法。在执行get函数的时候我们将Dep.target通过addDep()添加到这个属性对应的deps(依赖收集者)中。在数据发生变化,执行set函数的时候,我们用notify()通知更新。
修改后的defineReactive()方法如下
defineReactive(data, key, value) {
//循环遍历data,直至不是一个对象
this.observe(value)
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return value
},
set(newVal) {
if (value === newVal) {
return
}
value = newVal
dep.notify()
// console.log(key+"更新成为了"+newVal)
}
})
}
好了,到现在我们的数据处理部分大体上已经完成了,我们可以修改一下index.html,看看码到现在的结果。修改index.html。在初始化LinkVue之后,我们初始化Watcher,而且想要触发get(),我们要在初始化Watcher之后读一下这个属性。虽然现在看起来有点笨,但是我们还没写编译函数,未来Watcher的初始化我们会放在编译这个class下。修改如下:
<html lang="en">
<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>Document</title>
</head>
<body>
<script src="LinkVue.js"></script>
<div id="app"> </div>
<script>
const app = new LinkVue({
el: "#app",
data: {
firstdata: "have a try",
fullname: {
fisrname: "Link"
}
}
})
new Watcher()
app.$data.firstdata
new Watcher()
app.$data.fullname.fisrname
app.$data.firstdata =
"changeFirstData"
app.$data.fullname.fisrname = "Lin"
</script>
</body>
</html>
输出结果如下:
