Kalec的网络日志

漂浮的灵魂

HOME 首页 Vue的整个实现流程

Vue的整个实现流程

基础总结
2018-04-10 17:30:00

Vue的整个实现流程

经过几番考虑,还是把diff算法探究放到后面吧,毕竟有点难度,看得也不是特别懂!那咱今天就来先简述一下Vue的整个实现流程吧!

首先呢,就画了一张这样的图!

屏幕快照 20180409 下午10.51.37.png
大致可以分为4个阶段

  1. 解析模板成render函数
  2. 响应式开始监听
  3. 首次渲染,显示页面,并且绑定相关的依赖
  4. 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.definePropertyset()方法侦听设置属性以及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实现响应式的那部分,可以看到控制台的输出,以及结果如下图

未命名.gif

当属性发生改变触发set(newVal)时候,会马上再次执行updateComponent()来重新执行render()本次vnode与上次保存的vnode做对比,通过diff算法发现差异后,对不同的地方进行真实DOM的渲染,这部分源码同上(初次渲染)走else分支。

最后说两句

这次写的感觉还是很乱,不过各个重点部分中核心的内容凭着自己对vue的部分源码一点点理解,都有一点点说明,可能有误,欢迎指出!

留言

  • 暂时没有留言,来留下你的留言吧!

评论

看不清?换一个