Vue的工作过程解析
对于 Vue 的工作过程,我们可以从下面这张图中得到一点思路。
我们可以从两个方面来解析 Vue 的工作过程:初始化阶段、数据修改阶段。
在 Vue 初始化阶段,我们创建了一个 Vue 实例并将其挂载在了页面上:
- 在创建实例的过程中,我们调用了一个
init()方法。它做了什么事情呢?它将传入的props、事件、data等都做了初始化。 - 我们通过调用
$mount()方法,实现了 Vue 实例的挂载。这个$mount()方法,最主要做的事情是什么呢?它通过调用render()函数生成了 virtual DOM,即虚拟DOM树。render()函数在执行的时候,会touch一下 对应属性的getter,这一步即为触发getter进行依赖收集的过程。 - 最后,调用
patch()方法生成真实DOM,挂载在页面上。
在数据修改阶段:
- 数据修改会触发对应属性的
setter。 - 由于数据响应式,对应的监听器 Watcher 会执行更新 (update) 操作。
- 通过调用
patch()方法,对比新旧 virtual DOM,得到页面的最小修改,执行页面刷新。
手写Vue包含的功能
我想要试试自己实现一个简单的 Vue。它将会是怎样的呢:
- 包含功能:它会包含数据响应式、依赖收集、数据更新这些核心过程。
- 解析阶段:只解析最简单的文本自定义变量
{{}}。 - 不包含功能:没有虚拟 DOM模块,也没有patch算法。一个变量对应一个Watcher的方式(Vue 1 阶段)。
文件会有五个:
- 测试文件 index.html
- 核心的 fVue.js
- 监视器 watcher.js
- 调度模块 dep.js
- 编译器 compier.js
首先,给出作为测试用的 index.html:
<!DOCTYPE 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>
<div id="app">
{{test}}
<p k-text="test"></p>
<p k-html="html"></p>
<p>
<input type="text" k-model="test">
</p>
<p>
<button @click="onClick">按钮</button>
</p>
</div>
<script src="fvue.js"></script>
<script src="fcompile.js"></script>
<script src="watcher.js"></script>
<script src="dep.js"></script>
<script>
const fVue = new FVue({
el: "#app",
data: {
test: "hello, frank",
foo: { bar: "bar" },
html: '<button>adfadsf</button>'
},
methods: {
onClick() {
alert('blabla')
}
},
});
//模拟数据修改
setTimeout(function(){
fVue.$data.test = "hello,fVue!";
console.log("setTimeout : ",fVue.$data.test);
}, 2000);
</script>
</body>
</html>
代码实现
为了验证想法,写了这四个文件。代码尽量简单。
//fvue.js
class FVue {
constructor(options){
this.$data = options.data;
this.$options = options;
//数据响应化
this.observe(this.$data);
//解析页面模板
new Compile(options.el, this);
}
observe(value){
if(!value || typeof value !== 'object'){
return;
}
Object.keys(value).forEach(key =>{
this.defineReactive(value, key, value[key]);
// 为vue的data做属性代理:this.xxx = this.$data.xxx
this.proxyData(key);
})
}
defineReactive(obj, key, val){
//递归
this.observe(val);
//每一个 key 都有一个的Dep与之对应
const dep = new Dep();
Object.defineProperty(obj, key, {
get(){
//依赖收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal){
if(newVal === val) return;
val = newVal;
//执行更新操作
dep.notify();
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
get(){
return this.$data[key];
},
set(newVal){
this.$data[key] = newVal;
},
});
}
}
fvue.js 核心文件实现了 observe 逻辑:即在初始化过程中,将传入的data属性做了初始化处理,通过 defineReactive()方法将data中每个属性都做了数据拦截,重新定义了每个属性的getter与setter。更详细的:
-
每一个属性都有自己专有的调度模块 Dep。
-
在
getter中,定义了依赖收集的方式(只要有对应的 Watcher 触发了 getter 方法,那么将其放入到 Dep 的数组里)。 -
在
setter中,定义了响应数据变化的方法(只要对应的setter方法被触发,那么该 Dep 就会执行通知操作,让对应的 Watcher 执行更新)。
再来看 dep.js 与 watcher.js。
//dep.js
class Dep {
constructor(){
this.deps = []
}
addDep(dep){
this.deps.push(dep)
}
notify(){
this.deps.forEach(dep => dep.update())
}
}
//watcher.js
class Watcher{
constructor(vm, key, cb){
this.vm = vm;
this.key = key;
this.cb = cb;
Dep.target = this; //将当前Watcher实例附加到Dep的静态属性上
this.vm[this.key]; //主动触发 getter 属性,触发依赖收集
Dep.target = null; //解除 Dep.target 这个静态变量的锁定
}
update(){
this.cb.call(this.vm, this.vm[this.key]);
}
}
我们将 Dep 看成是一个调度模块,它只负责管理更新。而 Watcher 相当于是一个执行人,它负责执行具体的更新过程。
我们看到,在 Watcher 初始化的过程中,我们主动触发了 getter 属性,触发了依赖收集的过程。但是,还没有看到 Watcher 在哪里被初始化的。其实,在 解析 HTML 模板的过程中,当我们发现了自定义变量时,就会触发 Watcher 的初始化。
为了简化,验证可行性。此时我们的 fcompile.js 会写得非常简单,只处理文本自定义变量的情况(在例子中是{{test}})。
class Compile {
//el是宿主元素或者选择器
//vm 是vue实例
constructor(el, vm){
this.$vm = vm;
this.$el = document.querySelector(el); // 简化:通过选择器来获取到文档元素
this.compile(this.$el);
}
compile(el){
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if(this.isTextParam(node)){
this.compileText(node);
}
//递归
this.compile(node);
})
}
isTextParam(node){
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileText(node){
let key = RegExp.$1;
let currentValue = this.$vm[key];
//解析后,需要将真实值挂载到真实页面上
this.textUpdate(node, currentValue)
//创建新的 Watcher 实例
new Watcher(this.$vm, key, (newValue)=>{
this.textUpdate(node, newValue)
})
}
textUpdate(node, value){
node.textContent = value;
}
}
Compile 是在 FVue 中调用的。它的工作是最为繁重的:
- 解析 HTML 模板,找出各式各样的自定义变量、事件等,将自定义变量对应的真实值展示在网页上。
- 最为关键的是:创建新的 Watcher 实例,触发依赖收集。同时实时响应 Watcher 的 update 情况,将最新的数据响应式结果,展示在页面对应的位置上。
当然,为了简单起见,此处的 Compile 只处理了一个最简单的情况:文本自定义变量 ({{test}}) 的情况。一个完善的 compile 函数会非常周密且复杂,可查看 Vue 源码。
总结
将代码放在一起,它们是可以运转的。页面上的展示变量在定时器时间过后,会发生改变。
在文章最后,让我们来捋一捋整个 Vue 工作的过程:
- 初始化阶段,observe 对传入 data 的每个属性都做了数据拦截,设置了数据响应化逻辑。
- 模板解析阶段,compile 通过查找自定义变量、事件等,并为此创建新的 Watcher 实例,触发依赖收集。
- 当数据发生变动的时候,属性上的 setter 触发 对应 Dep 的通知操作,让对应的 Watcher 实例执行更新。
- Watcher 执行更新的时候, HTML 模板上的自定义变量也会随之发生改变。由此触发页面的刷新。
整个过程可以看做是 Vue 1.x 的工作方式极端简易版本,虽然与 Vue 2.x 不同,但希望不会影响各位读者对 Vue 的理解。