Vue的整个实现流程
经过几番考虑,还是把diff算法探究放到后面吧,毕竟有点难度,看得也不是特别懂!那咱今天就来先简述一下Vue的整个实现流程吧!
首先呢,就画了一张这样的图!
大致可以分为4个阶段
- 解析模板成render函数
- 响应式开始监听
- 首次渲染,显示页面,并且绑定相关的依赖
- data属性变化时触发rerender
解析模板成render函数
从Vue2开始,这个阶段在编译打包的时候就已经完成了,从模板变成render函数,交由编译的工具完成,其原理还是大量运用正则解析字符串模板,得到指令,class等,形成AST。这部分涉及编译原理等一系列相关知识,有能力的小伙伴请参考(在此我就不做讨论啦)(ps:其实我也不是非常明白)
《template 模板是怎样通过 Compile 编译的》
render函数
在这里,我想单独说明一下render函数构成,其中with()
用得非常巧妙,他把变量直接就全部挂载到了render函数上面。
关说说,不是太明白,我们就去修改一下vue的源码,把编译的render函数弄出来看看吧!直接搜索code.render
,然后console.log(code.render)
打印出来,大概在1w多行吧(版本不同可能位置不同)
<div id="app">
<input type="text" v-model="sth">
<button @click="add">确认添加</button>
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
data: {
sth:'',
list:[]
},
methods: {
add(){
this.list.push(this.sth)
this.sth = ''
}
}
})
</script>
模板编译后的render函数如下
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (title),
expression: "title"
}],
attrs: {
"type": "text"
},
domProps: {
"value": (title)
},
on: {
"input": function ($event) {
if ($event.target.composing) return;
title = $event.target.value
}
}
}), _v(" "), _c('button', {
on: {
"click": add
}
}, [_v("确认")]), _v(" "), _c('ul', _l((list),
function (item) {
return _c('li', [_v(_s(item))])
}))])
}
从这里我们就可以看到,通过with(this)
就把里面的_c()
,_v()
,_l()
,_s()
等方法以及上面的属性全部挂载到了render函数上面,指向同一个this
,这样做的目的是待后面的响应式开始监听后,全部挂载到new Vue的实例对象上。vue里面的指令也都变为了JS的逻辑。render函数中_c()
返回返回每一个节点的虚拟节点,render最终目的返回整个虚拟DOM(Virtual DOM Tree)
响应式
响应式就是要让我们从对象获得到数据的内容,以及我们对其修改增加属性进行侦听。能够让计算机知道,你获得了什么,你改变了什么!
这一部分的核心是用到了ES5的Object.defineProperty
,也正是Vue兼容性为IE9+的主要原因。Object.defineProperty
的具体用法可以参考MDN。
这里也就简单模拟Vue实现一个响应式的过程
<section>
<input type="text" id="ipt">
<p id="p"></p>
</section>
class Vue {
constructor(options) {
this._data = options
observer(this._data)
}
}
let ipt = document.getElementById('ipt')
let p = document.getElementById('p')
function cb(val){
p.innerHTML = val
}
function defineReactive(obj, key, val){
let prevVal
Object.defineProperty(obj, key, {
get() {
console.log('得到', val)
return val
},
set(newVal) {
if(newVal === prevVal)return;
prevVal = newVal
console.log('设置', key + '为' + newVal)
cb(newVal)
}
})
}
function observer(value) {
if (!value || (typeof value !== 'object')) {
return
}
for(let key in value){
defineReactive(value, key, value[key])
}
}
let data = {
name: '哈哈',
age: 24
}
let o = new Vue(data)
p.innerHTML = ipt.value = o._data.name
ipt.addEventListener('input', function(){
o._data.name = ipt.value
})
然后结果见下面data属性变化部分
重点在于Object.defineProperty
的set()
方法侦听设置属性以及get()
方法中侦听获得的属性。
通过外部传递来的属性的变化,侦听得到并且触发cb()
执行内容修改。在这里有人可能会想说那直接侦听set()
方法不就可以了嘛?
实际上在Vue实际运行的过程中,需要渲染到页面的属性往往不会是全部的属性。如果直接用set的话,就把所有属性都挂载过来了,造成了一定的资源浪费。而通过get()
就可以提前知道页面中需要获取哪些属性,再把需要的属性进行挂载,这样做能节约不少资源。
初次渲染页面
第一次渲染的时候会直接执行updateComponent
,执行_render
会访问到vm上面,也就是Vue实例出来的属性,同时会被响应式的get()
方法监听到。这时候会到vdom的__patch__
方法,patch
会将其渲染为真实的DOM,并且保存当前的vdom。初次渲染完成。
我们对源码做一点疯狂简化,便可以清晰的看明白这个过程啦
Vue.prototype._update = function (vnode, hydrating) {
var prevVnode = vm._vnode;
vm._vnode = vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode);
} else {
vm.$el = vm.__patch__(prevVnode, vnode);
}
}
function updateComponent() {
vm._update(vm._render(), hydrating);
}
可以清楚看到,初次渲染,和HTML上的的el
模板做对比,发现不同的地方就进行渲染,这里主要是针对服务端ssr渲染后,模板有内容的情况,当然没有内容就更好办了,那就全部渲染呗!
data属性变化
当然这里还是脱离不了之前响应式的内容,只不过这次侦听的是set(newVal)
方法,上面模仿Vue实现响应式的那部分,可以看到控制台的输出,以及结果如下图
当属性发生改变触发set(newVal)
时候,会马上再次执行updateComponent()
来重新执行render()
本次vnode与上次保存的vnode做对比,通过diff算法发现差异后,对不同的地方进行真实DOM的渲染,这部分源码同上(初次渲染)走else分支。
最后说两句
这次写的感觉还是很乱,不过各个重点部分中核心的内容凭着自己对vue的部分源码一点点理解,都有一点点说明,可能有误,欢迎指出!