Vue-初始化和渲染过程
因为最近在用Vue
,所以想把代码看一下,我觉得,越多的了解运行机制,越能更好的解决问题和……避坑。
本文必须提前要了解的两个概念是:defineProperty
特性,观察者模式。
我们以一个简单的结构来开始
1 2 3 4 5 6 |
new Vue({ el: '#app', data: { foo: '123' } }) |
new Vue
实际会进入Vue._init
的初始化函数,这也是一切的开始。
初始化数据
这一步完成的工作是:为data
创建Observer
对象,使其产生响应式的效果。操作(get/set
)data
时,实际会操作挂载为_data
的data
,然后触发defineProperty设置的响应操作。
1 2 |
-initState -initData |
从config.data
到vm.data
用一个proxy(vm, keys[i])
完成对data
内容的挂载。实现用代理(proxy
)的方式访问
1 2 3 4 5 6 7 8 |
Object.defineProperty(vm, keys[i], { get: function proxyGetter () { return vm._data[key] } set: function proxy (val) { vm._data[key] = val } }) |
为data
创建对应的Observer
这一步操作之后,创建出的结构如下
vmCount
以该对象作为root $data
的vm
计数器;dep
实例包含一个自增Id,和一个Watcher
列表以存储该依赖的改变会触发哪些watcher
更新。
代码流程
1 2 3 4 5 6 |
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?)
1 2 3 4 5 6 7 8 |
-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
如何执行的。
1 2 3 4 5 6 |
{ 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
节点的渲染过程。
1 2 3 4 5 |
-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
,所以入门开发人员很容易聚焦到必要的代码结构和声明周期上来。
前面的唠叨和本文主旨没太大关系,因为完整对比是要放在对二者充分了解之后,这里只是从入门简单说两句。
期待你出一个基于vue框架的wordpress主题,练练手,哈哈,另外,这vue确实在响应式和商品管理等操作上也更加高效灵活便捷,不错哈。
@郑永 主题倒是没问题。数据比较麻烦,需要使用rest-api插件,然后调用一堆不知道会不会有问题的接口。比较蛋疼
所以wordpress团队用VueJS取代了ReactJS
@郑永 wp那个替换是技术无关的,因为react夹带问题专利条款