因为最近在用
Vue,所以想把代码看一下,我觉得,越多的了解运行机制,越能更好的解决问题和……避坑。本文必须提前要了解的两个概念是:
defineProperty特性,观察者模式。我们以一个简单的结构来开始
new Vue({
el: '#app',
data: {
foo: '123'
}
})
new Vue实际会进入Vue._init的初始化函数,这也是一切的开始。初始化数据
这一步完成的工作是:为
data创建Observer对象,使其产生响应式的效果。操作(get/set)data时,实际会操作挂载为_data的data,然后触发defineProperty设置的响应操作。-initState
-initData
从
config.data到vm.data用一个
proxy(vm, keys[i])完成对data内容的挂载。实现用代理(proxy)的方式访问Object.defineProperty(vm, keys[i], {
get: function proxyGetter () {
return vm._data[key]
}
set: function proxy (val) {
vm._data[key] = val
}
})
为
data创建对应的Observer这一步操作之后,创建出的结构如下
Observer structure
Observer与data的对应结构
其中
vmCount以该对象作为root $data的vm计数器;dep实例包含一个自增Id,和一个Watcher列表以存储该依赖的改变会触发哪些watcher更新。代码流程
observe(data) // data => {foo: '123'},最外层data对象
-new Observer(data)
-Observer.walk
-defineReactive(obj, key, value) // {foo: '123', __ob__: obj}, foo, '123'
-Object.defineProperty(obj, key, {...})
-let childOb = observe(val)
逐层遍历对象的属性,生成与之对应的
Observer,直到目标不为对象为止。childOb和dep(defineReactive时创建)一样,也作为闭包对象被reactiveGetter/reactiveSetter使用。这一步结束后,会使
data的所有属性都产生响应式的效果,即,操作(get/set)data里任意位置的数据,都会触发reactiveGetter/reactiveSetter。初次渲染
整个渲染过程都包在一个
watcher里,并保存在实例对象的_watcher上。和watcher前后相连的,就是beforeMount、mounted两个生命周期函数。(哪些操作或配置会创建Watcher?)-initRender
-Vue._mount
// beforeMount
vm._watcher = new Watcher(vm, function () {
vm._update(vm._render(), hydrating);
}, noop);
// mounted
-this.value = this.get(); // this =>Watcher
注意:
watcher是包含value属性的,因为创建的时候就会执行传入的getter函数并获取返回值。所以声明在watch关键字里的函数要return一个返回值。Watcher structure
需要关注的是这个
watcher如何执行的。{
Dep.pushTarget(this); // Dep.target赋值为当前watcher
value = this.getter.call(this.vm, this.vm); // Watcher的处理函数
Dep.popTarget();
this.cleanupDeps();
}
依赖收集
这部分内容比较碎,涉及到两个类:
Dep、Watcher。需要配合观察者模式来想一下整个流程。Dep就是dependency(依赖),前面说过,一个data属性对应一个Observer,一个Observer里包含一个dep实例。dep.subs中存储了所有观察者(Watcher),属性变更,就会触发这些watcher。而
Watcher就是观察者,这里的watcher即vm._watcher是执行渲染的基本单元。对应dep,watcher中也存储了dep实例和depId,即收集此渲染所需的依赖(dep,一个dep对应一个data属性)。所以,整个过程就是围绕着这两个类来操作的:
dep拿小本本记录所有在观察它的Watcher,当有变动是,让这些Watcher采取行动。Watcher也记录所有dep依赖,必要时(不再需要观察某个dep),通知dep把自己从它的小本本上划掉。1、准备阶段
vm._watcher.get()。保存watcher为Dep的静态变量target,作为是否处于收集依赖阶段的标志位使用;2、渲染阶段
Vue._render,在需要的时候(即调用到某个data属性时)获取data的值(逐层操作,直到取到目标值)2.1 创建依赖列表
把
把
reactiveGetter外层闭包里的dep存入watcher的newDepIds、newDeps;2.2 创建反向记录
把
把
watcher存入闭包的dep.subs,这一步很重要,属性变更时,这里是入口;3、重置阶段
Dep.target重新置为null。newDepIds替换depIds,newDeps替换deps,下次使用,如果检查上次有依赖A,如果此次没有,表示已经不需要(如v-if为false),会从dep.subs中移除。执行渲染
渲染会用到虚拟节点
VNode,限于篇幅(其实还没看),这里先不说。大概看一下渲染流程,render会生成一个奇怪的代码段(template?),来实际执行从虚拟节点到DOM节点的渲染过程。-vm._update(vm._render(), hydrating); // vm._render()返回一个VNode
-vm._render()
-with(this){return _h('ul',{attrs:{"id":"demo"}},[_h('div',{on:{"click":clickHandler}},[_s(foo)])])}
=createElement // 创建不同类型的节点
-vm.__patch__(vm.$el, vnode, hydrating)
更新流程:复制旧的虚拟节点 => 更新内容 => 插入替换后的节点 => 删除之前的节点
后话:框架使用感受
坊间传言
VueJS更易上手,ReactJS开发难度略高,AngularJS技能树陡峭。确实,和React相比,Vue似乎天然不要求组件化,这个感觉使前端开发更趋近于传统页面脚本的逻辑。产生这种感觉,除了先入为主,还有其本身特性比较……零碎,无论是watch、computed或者更常用的directives,都在传达一个“霍,黑科技,这个窍门很NB”的感觉。这种感觉乍一看很吸引人,因为新上手确实酷炫。但是带来的一个问题是,新手很容易忽略框架本身的特性。虽然Vue各个环节是完备的,但是最通常的使用,还是一个UI框架+一个大Vue实例,必要的时候使用几个第三方组件。甚至当感觉代码到一定规模觉得可以拆一下的时候,还是懒得动,或者觉得没必要。相比较来说,
React只有state和props,所以入门开发人员很容易聚焦到必要的代码结构和声明周期上来。前面的唠叨和本文主旨没太大关系,因为完整对比是要放在对二者充分了解之后,这里只是从入门简单说两句。