对于 Vue 的工作过程,我们可以从下面这张图中得到一点思路。
我们可以从两个方面来解析 Vue 的工作过程:初始化阶段、数据修改阶段。
在 Vue 初始化阶段,我们创建了一个 Vue 实例并将其挂载在了页面上:
init()
方法。它做了什么事情呢?它将传入的props、事件、data等都做了初始化。$mount()
方法,实现了 Vue 实例的挂载。这个$mount()
方法,最主要做的事情是什么呢?它通过调用 render()
函数生成了 virtual DOM,即虚拟DOM树。 render()
函数在执行的时候,会touch
一下 对应属性的getter
,这一步即为触发getter
进行依赖收集的过程。patch()
方法生成真实DOM,挂载在页面上。在数据修改阶段:
setter
。patch()
方法,对比新旧 virtual DOM,得到页面的最小修改,执行页面刷新。我想要试试自己实现一个简单的 Vue。它将会是怎样的呢:
{{}}
。文件会有五个:
首先,给出作为测试用的 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 中调用的。它的工作是最为繁重的:
当然,为了简单起见,此处的 Compile 只处理了一个最简单的情况:文本自定义变量 ({{test}}) 的情况。一个完善的 compile 函数会非常周密且复杂,可查看 Vue 源码。
将代码放在一起,它们是可以运转的。页面上的展示变量在定时器时间过后,会发生改变。
在文章最后,让我们来捋一捋整个 Vue 工作的过程:
整个过程可以看做是 Vue 1.x 的工作方式极端简易版本,虽然与 Vue 2.x 不同,但希望不会影响各位读者对 Vue 的理解。