vue原理
引用
众所周知vue
是一个MVVM 渐进式框架,MVVM是vue的设计模式,在vue框架中数据会自动驱动视图。
1、MVVM设计模式
解释
View是视图,就是DOM;对应视图也就是HTML部分--代表UI组件,它负责将数据模型转化成UI展现出来。 Model是模型,就是vue组件里的data,或者说是vuex里的数据;--代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。 ViewModel--监听模型数据也就是data的的改变和控制视图行为、处理用户交互,简单理解就是一个同步View和Model的对象,连接Model和View。
总结
在MVVM架构下,View
和Model
之间并没有直接的联系,而是通过ViewMode
进行交互,Model和ViewModel之间的交互是双向的,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反应到View上。
ViewModel
通过双向数据绑定把View层和Model层连接了起来,而View和Model之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由MVVM来统一管理。
由此,我们可以引出vue是响应式的
2、响应式
说明
Vue 的响应式原理是核心是通过 ES5 的保护对象的 Object.defindeProperty
中的访问器属性中的 get
和 set
方法,data 中声明的属性都被添加了访问器属性,当读取 data 中的数据时自动调用 get 方法,当修改 data 中的数据时,自动调用 set 方法,检测到数据的变化,会通知观察者 Wacher,观察者 Wacher自动触发重新render 当前组件(子组件不会重新渲染),生成新的虚拟 DOM 树,Vue 框架会遍历并对比新虚拟 DOM 树和旧虚拟 DOM 树中每个节点的差别,并记录下来,最后,加载操作,将所有记录的不同点,局部修改到真实 DOM 树上。
2.1 双向数据绑定原理
检测data变化的核心APIObject.defindeProperty
基本使用
const data = {};
let name = "张三";
Object.defineProperty(data,'name',{
get:function(){
console.log('触发get')
return name
},
set:function(newVal){
console.log('触发set')
name=newVal
}
})
//测试
console.log(data.name) // 触发get 张三
data.name = '李四' // 触发set
这样就是可以实现数据的获取和赋值的监听
2.2 接下来看看vue是如何监听data变化的
//触发更新视图
function updateView() {
console.log('视图更新')
}
//重新定义数组原型
const oldArrayProperty = Array.prototypo
//创建新对象,原型指向oldArrayProperty,在拓展新的方法(这样不会影响原型)
let arrayProto = Array.prototype
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
methods.forEach(methodName => {
arrayProto[methodName] = function () {
updateView ()
oldArrayProperty[methodName].call(this,...arguments)
}
})
//监听对象属性
function observer(target){
if(typeof target !=='object' || target === null) {
//不是对象或者数组
return target
}
//重新定义数组原型
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
//重新定义各个属性(for in 对象/数组都可以遍历)
for(let key in target) {
defineReactive(target,key,target[key])
}
}
//重新定义属性,监听起来
function defineReactive (target, key, value){
//递归深度监听
observer(value)
//核心API
Object.defineProperty(target,key,{
get(){
return value
},
set(newValue){
if(newValue !== value) {
// 深度监听
observer(newValue)
//设置新值
value = newvalue
//触发更新视图
updateView()
}
}
})
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
data.name = 'lisi' //视图更新
data.age = 21 //视图更新
console.log('age', data.age) //age 21
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所以有 Vue.dete
data.info.address = '上海' // 深度监听
data.nums.push(4) // 视图更新
缺点
- 深度监听
obj
,需要递归到底,一次性计算量大,如果数据过大页面,页面可能会卡死 - 无法监听新增属性/删除属性(所以vue提供了
Vue.set
Vue.delete
) - 无法原生监听数组,需要做特殊处理
3、vdom和diff
背景
DOM操作是非常耗时的,Vue 和React 是数据驱动视图,就是 通过 虚拟DOM(vdom)来解决的这个问题
3.1 vdom
vdom
就是一段js形式的html
代码
用 js
模拟 DOM
结构
<div id ='app' class='box'>
<p>p标签的文本</p>
<ul style='font-size:20px'>
<li>li标签文本</li>
</ul>
</div>
{
tag: 'div',
props: {
id: 'app',
className: 'box'
},
children: [
{
tag: 'p',
children: 'p标签的文本'
},
{
tag: 'ul',
props: {
style: 'font-size:20px'
},
children: [
{
tag: 'li',
children:'li标签文本'
}
]
}
]
}
- tag 标签
- props 属性(包括 id、className 、 style、事件等)
- children 子元素,数组或者字符串
拓展
可以通过学习snabbdom 进一步了解
var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I'll take you places!')
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I'll take you places!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
解析
- h 是一个函数,接收三个参数(标签或选择器,属性,子节点数组),返回一个vnode结构;
- patch 补丁的意思;
- patch(containerro/容器, vnode/虚拟dom) ,表示把vnode渲染到DOM结构中
- patch(vnode, newVnode); 表示更新已有的内容(具体怎么计算更新对应的内容就是使用
diff
算法) - patch(containerro, null); 清空DOM结构
3.2 diff算法,新旧vnode对比,计算出最小的更新范围
核心
- 同层级比较(只比较同一层级,不跨级比较)
- tag 不相同,则直接删除重建,不在深度比较
- tag 和 key,两个都相同,则认为是相同节点,不在深度比较
再此推荐两篇文章
- 解析vue2.0的diff算法
- VirtualDOM与diff(Vue实现)
4、渲染过程
vue组件渲染/更新过程(异步渲染)
- 初次渲染过程
- 更新过程
4.1初次渲染过程
- 解析模板为
render
函数(一般在开发环境已经完成,vue-loader)4 - 触发响应式,监听data属性
getter
setter
(模板中使用到的变量会触发getter
) - 执行
render
函数(触发getter
),生成vnode,patch(elem,vnode)
渲染到页面上
注意
如果模板中没有用的data数据就不会触发getter
,因为和视图没关系(vue里面的优化)
4.2 更新过程
- 修改data的数据,触发
setter
(此前data数据在getter
中已被监听) - 重新执行
render
函数,生成newVnode(新的虚拟dom)
- 使用
patch(vnode,newVnode)
更新到页面上
- 1、编译模板生成
render
函数,生成vdom
- 2、执行
render
函数,触发data中的getter
- 3、
getter
方法收集依赖(通俗点就是,在模板里面触发了哪个变量的getter
,就把哪个变量观察起来) - 4、在依赖中
setter
修改data中的数据的时候,Notify
看下修改的那个数据是不是之前被观察起来的 - 5、如果是之前观察起来的,就重新渲染(
re-render
),重新生成render
函数,生成newVdom
形成一个闭环
5、路由
vue分为hash
(默认)以及 history
两个路由模式
解析
- protocol - 协议
- hostname - 主机名
- port - 端口
- pathname - url 路径
- search - ?号之后的参数
- hash - #号之后的部分
5.1 hash
特点
- hash 变化会触发页面跳转,即浏览器的前进,后退
- hash 变化不会刷新页面,SPA(单页面)必须的特点
- hash 永远不会提交到server 端
vue中就是通过hash 的变化触发路由的变化,来触发视图的渲染
js 实现hash
<body>
<p>
hash路由
</p>
<button id='btn'>
修改 hash
</button>
</body>
<script>
//hash 变化 包括;
//a. js 修改URL
//b. 手动修改url的hash
//c.浏览器的前进、后退
//页面初次加载获取hash
window.addEventListener ('DOMCintentLoaded',() =>{
console.log('hash',location.hash)
})
//hash变化触发
window.onhashchange = (event) =>{
console.log('hash',location.hash)
}
//js 修改 url
document.getElementById('btn').addEventListener('click',()=>{
location.href = '#/user'
})
</script>
5.2 history
h5 history 主要是通过 history.pushState
跳转 和 window.onpopstate
监听页面的前进和后退
<body>
<p>
historyh路由
</p>
<button id='btn'>
修改 url
</button>
</body>
<script>
//页面初次加载获取hash
window.addEventListener ('DOMCintentLoaded',() =>{
console.log('load',location.pathname)
})
//js 修改 url
document.getElementById('btn').addEventListener('click',()=>{
//pushState 有三个参数
//第一个参数是个js对象,可以放任何的内容,可以在onpostate事件中(后面介绍)获取到便于做相应的、处理。
//第二个参数是页面标题:目前所有浏览器都不支持,填空字符串即可
//第三个参数是个字符串,就是保存到history中的url。
let state= {
title:'新页面'
}
history.pushState(state,'','user')
})
//监听浏览器的前进、后退
window.onpostate = (event) => {
console.log(event.state) // {title:'新页面'}
console.log(location.pathname)
}
</script>
上面的代码如果放在本地html 文件中运行 js代码会报错,需要放在web服务器
注意
history 模式需要后端配合,就是无论用户访问什么路由,所有路由的切换都由前端来做,后端只需要返回index.html
的文件,如果后面没有配置兼容,当访问user这个路由,点击刷新,就会报user页面找不到
以上内容纯属个人理解,若有不对,请留言纠正!
关注【前端知识小册】,第一时间获取前端优质文章!