话不多说,直接上代码:
1 | <div id="app"> |
为了实现这些功能,我们分几步走:
- 首次渲染
- 响应式系统
- 事件绑定
- 双向绑定
首次渲染
我们先来实现一下首次渲染,首次渲染我们需要做的事情比较简单:解析模板,获取数据,渲染。
Vue
1 | class Vue { |
Vue 类的构造函数中会将外部传进来的参数进行保存,然后初始化一个编译器对模板进行编译。
这里还实现了方法 getVal 用来从 obj.age.a.b 这样的表达式中“递归地”进行取值。
注:Vue 中的做法是把模板编译成了渲染函数,就像这样:
1 | function render() { |
其中,_c 就是 $createElement,位于 core/instance/render.js,其他函数 _v, _s 见 core/instance/render-helpers/index.js。
Compiler
1 | class Compiler { |
Compiler 构造函数中会调用 compile 方法来编译我们的模板。这里分元素类型和文本类型。
遇到元素类型就解析上面的指定,如果命中了就执行相关的指定方法(这里暂时只实现了 test 和 html 指令),同时解析出指定的表达式,通过 getVal 得到值,根据不同的指令进行相关的渲染。
文本类型则需要解析出双大括号中的表达式,最后调用 text 指令的方法。
响应式数据系统
先不急着写代码,我们先来画个流程图来梳理下我们的思路:

我们需要在首次渲染获取值的时候通过拦截 get 方法来收集每个 key 所对应的依赖,即 watcher,并给每个 key 分配一个 dep 来负责管理。注意到一个 key 可能在页面中多次被使用,所以这里我们一个 key 可能对应着多个 watcher,这里 dep 和 watcher 的关系是一对多的。当给 key 赋值的时候,我们需要去通知对应的 watcher 进行更新,watcher 则会对视图进行重新渲染。
注:Vue 中为了避免一个组件中存在太多的 watcher 影响性能,实际上是一个组件只有一个 watcher(不包括 computed 属性产生的)
流程图出来了,我们来实现一下:
1 | class Vue { |
首先看一下 proxy 函数:
1 | function proxy(vm) { |
该函数只是做了一下代理,这样就可以通过实例直接访问 $data 中的属性了。
再来看一下 observe 这个函数:
1 | function observe(value) { |
该函数类似一个工厂函数,当传入的值为对象时,返回一个 Observer 实例,即 value 被观察后的一个对象。
1 | function def(obj, key, val, enumerable) { |
Observer 中做了几件事:
- 定义了
__ob__属性,该属性通过Object.defineProperty来定义,主要是为了让其无法被遍历。 - 将所观察的值挂载在
value属性上。 - 因为
value是一个对象,所以遍历value的 key 来defineReactive。
在看 defineReactive 前,我们先快速的看一下 Dep 和 Watcher:
1 | // 管理一个依赖,未来执行更新 |
这两个比较好懂,就不赘述了。我们看一下 defineReactive:
1 | function defineReactive(obj, key, val) { |
这里要注意的有几点:
- 需要对
val递归地进行观察。 val是函数的参数,相当于是函数的内部变量,因为它是可以被外部访问到的,所以这里实际上形成了闭包。这样我们在set函数里面对val进行赋值是有用的。set中传入的新值也需要进行观察。
最后,别忘了我们的 watcher,它应该在初始渲染的时候被实例化:
1 | update(node, exp, dir) { |
这样,我们的响应式系统的雏形就写好了。
不过,我们现在的响应式系统是无法处理新增属性这样的需求的,需要我们进行一些优化。
我们先来分析一下目前的问题:一个 dep 是服务于某一个 key 的,所以当 key 对应的值中新增了属性时是无法触发 key 的 set 方法的。所以新增 key 就不能用 js 原生的写法了,只能通过调用 $set 来进行,这样,我们才有可能在 $set 函数里面手动的去通知 watcher 进行更新。
1 | class Vue { |
然后,在收集依赖的地方,依赖某个 key 的 watcher 也必须同时依赖 key 所对应的值:
1 | function defineReactive(obj, key, val) { |
事件绑定
这里暂时只实现了 @click 事件,我们需要再编译器中增加对事件的解析:
1 | ... |
同时,需要对 $methods 也进行代理:
1 | function proxy(vm) { |
双向绑定
我们要实现类似 v-model 的双向绑定效果,首先我们需要添加指令对应的函数:
1 | // v-model |
这样双向绑定的 value 这一向就完成了,接下来要添加 @input 那一向:
1 | class Vue { |
这一向其实也比较简单,就是监听 input 事件,将事件返回的值赋值给 $data 对应的 key。
至此,一个简单的 vue 就实现了。