# Vue源码理解
# MVVM的三要素
响应式:vue如何监听到
data的每个属性变化?模板引擎:vue的
模板如何被解析,指令如何处理?渲染:vue的
模板如何被渲染成html?以及渲染过程
# vue中如何实现响应式?
什么是响应式
修改
data属性后,vue立刻监听到data属性被代理到vm上
Object.defineProperty
var obj = {}
var _name = 'evan'
Object.defineProperty(obj, 'name', {
get: function() {
console.log('get', _name)
return _name
},
set: function (newVal) {
console.log('set', newVal)
_name = newVal
}
})
console.log(obj._name) // 可以监听到
obj._name = 'wang' // 可以监听到
所以,vue中数据改变,视图就改变,其核心就是通过
Object.defineProperty实时监听到数据的变化
- 模拟
var vm = {}
var data = {
name: 'evan',
age: 25
}
var key,value
for (key in data) {
(function (key) {
Object.defineProperty(vm, key, {
get: function () {
console.log('get', data[key]) // 监听
return data[key]
},
set: function (newVal) {
console.log('set', newVal) // 监听
data[key] = newVal
}
})
})(key)
}
# Object.defineProperty API 缺点
- 不能监听对象属性新增和删除
- 初始化阶段递归执行
Object.defineProperty带来的性能负担
# Reactive API
- 响应式的实现方式就是
劫持数据,Vue.js 3.0 的 Reactive API 就是通过Proxy劫持数据,由于 Proxy 劫持的是整个对象,所以可以检测到任何对对象的修改,弥补了Object.defineProperty API的不足。
# vue中如何解析模板
- 模板是什么?
本质:字符串
有逻辑,如
v-if``v-for等与
html格式很像,但有很大区别最终还是要转换为
html来显示模板最终必须转成JS代码,因为:
a. 有逻辑(v-if v-for),必须用JS才能实现(图灵完备)
b. 转换为html渲染页面,必须用JS才能实现
c. 因此,模板最重要转换成一个JS函数(
render函数)
- render函数
<div id="app">
<p>{{price}}</p>
</div>
with(this){
return _c(
'div',
{
attrs: {"id":"app"}
},
[
_c('p', [_v(_s(price))])
]
)
}
模板中所有信息都包含在了
render函数中this即vmprice即this.price即vm.price,即data中的price_c即this._c即vm._c
- render函数执行时返回vnode
a. updateComponentt中实现了vdom的path
b. 页面首次渲染执行updateComponent
c. data中每次修改属性,执行updateComponent
# vue的整个实现流程
解析模板成
render函数a. 模板中所有信息都被
render函数所包含b. 模板中用到的
data中的属性,都变成了JS变量c. 模板中的
v-model v-for都变成了JS逻辑d.
render函数返回vnode响应式开始监听
a. vue中数据驱动视图改变,其核心就是通过
Object.defineProperty实时监听到数据的变化b. 同样,通过
Object.defineProperty,将data的属性代理到vm上首次渲染,显示页面,且绑定依赖
a. 初次渲染,执行
updateComponent,执行vm._render()b. 执行
render函数,会访问到上一步代理到vm上的data的属性c. 会被响应式的
get方法监听到因为,data中有很多属性,有些被用到,有些可能不被用到,被用到的走get,不被用到的不会走到get,未走到get中的属性,set的时候我们也无需关心,避免不必要的重复渲染
d. 执行
updateComponent,会走到vdom的patch方法e.
patch将vnode渲染成DOM,初次渲染完成data属性变化,触发
rerendera. 修改属性,被响应式的
set监听到b.
set中执行updateComponentc.
updateComponent重新执行vm._render()d. 生成的
vnode和preVnode,通过patch进行对比e. 渲染到
html
# 入口
vue首先是一个Function,es5中通过方法声明一个类,需要new这个方法,也就是new Vue(),生成一个实例对象,这个实例对象上有那么多方法哪来的呢?是通过Vue的prototype挂载到原型上。
这里没有通过ES6的class来定义,原因就是js是动态类型语言,通过方法,我们可以随意挂载方法到它的原型上,这样一类方法用一个文件来编写,就把代码解耦了,做到了模块化拆分。
# vue的数据驱动
vue是怎么通过data的赋值就实现了DOM的渲染呢?并没有像jq那样直接操作dom啊?下面就是答案:
在Vue中,我们通过$mount实例方法来挂载vm。
首先,对el做了限制,Vue不能挂载到body、html上这样的根节点上,然后会进行判断,如果没有定义render方法,则会把el或者template字符串转换成render方法。
$mount方法传入两个参数,第一个是el,表示挂载的元素,可以是字符串,也可以是DOM对象,如果是字符串在浏览器环境下会调用query方法来转成DOM对象,第二个参数是和服务端渲染(ssr)有关。
然后就是调用vm._render方法了生成虚拟的Node了,同时呢,在实例化一个Watcher,也就是说在页面初始化的时候通过一大堆的数据计算后,渲染DOM,然后呢,中间数据变化了,Watcher监听到了,再次执行。
第一步,把一堆template转成render函数;
第二步,通过render中的createElement创建组件的VNode(虚拟DOM)
第三步,走vm._update调用__patch__,把虚拟DOM转换成真正的DOM节点。
update的调用时机有两个,一个是首次渲染,一个是数据更新的时候,通过VNODE Tree调用js原生的appendChild方法,调用insert方法把DOM插入到父节点,进行递归调用,把虚拟dom和真实dom映射到一起,从而实现DOM的真正渲染。
vdom的真正意义是为了实现跨平台,服务端渲染,以及提供一个性能还算不错 Dom 更新策略
vdom 让整个 mvvm 框架灵活了起来,性能不是最重要的
好的 diff 算法只是 vdom 实现层面的优化,而并不代表 vdom 本身是为了性能优化
# runtime-only和compiler and runtime
vue实例挂载,最终都要通过render函数来执行,如果采用runtime-only模式,要直接写render函数,而采用compiler组合的模式,则可以书写template。通过wm._update方法传入render来渲染Watcher。
# component生成
组件的生成的流程,是之前通过render方法,来createElement,如果tag是传入的最上层的app组件,那么首先Vue会构造子类的构造函数,把自身的一堆options传递给子类,组件集成过程其实用的就是
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Super完全继承了Vue的各种属性,所以Vue.extend也可以用组件componentName.extent来表示。
这里面有一个缓存方法,把构造函数进行了缓存,对于拥有相同父类的子组件,就不重复构造了。完了之后,通过new VNode实例化一个vnode并返回。
# 派发更新和nextTick
派发更新就是当数据发生改变后,通知所有订阅了这个数据变化的watcher执行update派发更新的过程中会把所有要执行
update的watcher推入队列中,在nextTick后执行flush。把所有的数据变化都收集到一次,做一次
nextTick数据的变化到dom的变化是一个异步的过程
本次
nextTick如果没有执行到,会被收集到callbacks数组中,下次nextTick执行nextTick返回一个promise对象nextTick是把要执行的任务推入到一个队列中,在下一个tick同步执行
