MVVM 原理 :
1、响应式:vue如何监听data的数据变化
2、模板解析:vue模板是如何被解析的
3、渲染:vue模板是如何被渲染成HTML的
响应式
对于MVVM来说,data一般是放在一个对象当中,
var obj ={
name:"yang",
age:"23"
}
console.log(obj.name) //访问
obj.age = 22 //修改
但是这样的操作vue本身是没有办法感知到的,那么应该如何让vue知道我们进行了访问或是修改的操作呢?
那就要使用Object.defineProperty
var vm = {} var data = { name: 'zhangsan', age: 20 } 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将data里的每一个属性的访问与修改都变成了一个函数,在函数get和set中我们即可监听到data的属性发生了改变。
模板解析
模板本质上是一串字符串,它看起来和html的格式很相像,实际上有很大的区别,因为模板本身还带有逻辑运算,比如v-if,v-for等等,但它最后还是要转换为html来显示。
<div id="app"> <div> <input v-model="title"> <button v-on:click="add">submit</button> </div> <div> <ul> <li v-for="item in list">{{item}}</li> </ul> </div> </div>
模板在vue中必须转换为JS代码,原因在于:在前端环境下,只有JS才是一个图灵完备语言,才能实现逻辑运算,以及渲染为html页面。
这里就引出了vue中一个特别重要的函数——render
render函数中的核心就是with函数。
with函数将某个对象添加到作用域链的顶部,如果在 statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。
var obj = { name: 'zhangsan', age: 20, getAddress: function () { alert('beijing') } } function fn1() { with(obj) { alert(age) alert(name) getAddress() } } fn1()
with将obj这个对象放在了自己函数的作用域链的顶部,当执行下列函数时,就会自动到obj这个对象去寻找同名的属性。
而在render函数中,with的用法是这样:
<div id="app"> <div> <input v-model="title"> <button v-on:click="add">submit</button> </div> <div> <ul> <li v-for="item in list">{{item}}</li> </ul> </div> </div> // 对应的js文件 var data = { title: '', list: [] } // 初始化 Vue 实例 var vm = new Vue({ el: '#app', data: data, methods: { add: function () { this.list.push(this.title) this.title = '' } } }) with(this){ // this 就是 vm return _c( 'div', { attrs:{"id":"app"} }, [ _c( 'div', [ _c( 'input', { directives:[ { name:"model", rawName:"v-model", value:(title), expression:"title" } ], domProps:{ "value":(title) }, on:{ "input":function($event){ if($event.target.composing)return; title=$event.target.value } } } ), _v(" "), _c( 'button', { on:{ "click":add } }, [_v("submit")] ) ] ), _v(" "), _c('div', [ _c( 'ul', _l((list),function(item){return _c('li',[_v(_s(item))])}) ) ] ) ] ) }
在一开始,因为new操作符,所以this指向了vm,通过with我们将vm这个对象放在作用域链的顶部,因为在函数内部我们会多次调用vm内部的属性,所以使用with可以缩短变量长度,提供系统运行效率。
其中的_c函数表示的是创建一个新的html元素,其基本用法为:
_c(element,{attrs},[children...])
其中的element表示所要创建的html元素类型,attrs表示所要创建的元素的属性,children表示该html元素的子元素。
_v函数表示创建一个文本节点,_l函数表示创建一个数组。
最终render函数返回的是一个虚拟DOM。
如何将模板渲染为html
模板渲染为html分为两种情况,第一种是初次渲染的时候,第二种是渲染之后数据发生改变的时候,它们都需要调用updateComponent,其形式如下:
vm._update(vnode){ const prevVnode = vm._vnode vm._vnode = vnode if (!prevVnode){ vm.$el = vm.__patch__(vm.$el,vnode) } else { vm.$el = vm.__patch__(prevVnode,vnode) } } function updateComponent(){ vm._update(vm._render()) }
首先读取当前的虚拟DOM——vm._vnode,判断其是否为空,若为空,则为初次渲染,将虚拟DOM全部渲染到所对应的容器当中(vm.$el),若不为空,则是数据发生了修改,通过响应式我们可以监听到这一情况,使用diff算法完成新旧对比并修改。