话不多说,直接上代码:
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
就实现了。