知其然知其所以然,只有了解了vue原理才能更好的应用.
响应式就是组件data的数据一旦发生变化, 会立即触发视图的重新渲染,是实现数据驱动的第一步.如下例所示:
<template>
<p>{{name}}</p>
<button @click="changeMes">改名</button>
</template>
<script>
export default {
data() {} {
return {
name: 'aley'
}
},
methods: {
changeMes() {
// name值改变,视图发生更新
this.name = '杨家祈'
}
}
}
</script>
原理: vue.js 通过Object.defineProperty()来劫持各个属性的setter,getter,通过getter读取data中的数据(收集视图依赖的数据);当数据变更时,自动调用setter方法, 发布消息给订阅者,触发相应的监听回调,进行视图更新.
Object.defineProperty
或es6的proxy(vue3.0启用,这里暂且不讲)
let data = {}
let name = 'zs'
Object.defineProperty(data, 'name' , {
get: function() {
console.log('get')
return name
},
set: function(newVal) {
console.log('set')
name = newVal
}
})
console.log(data.name) // get zs
data.name = 'aley'
console.log(data.name) // set get aley
响应式代码演示:注意:
Object.defineProperty
无法监听新增/删除属性, 可通过Vue.set新增响应性属性//Vue.delete删除属性Object.defineProperty
只能监听对象的变化,无法监听数组的变化,需要特殊处理// 复杂对象
let data = {
name: 'aley',
info: {
age: 18,
address: ['昆明', '建水']
},
nums: [1,2,3,4]
}
// 数组特殊处理
// 重新定义数组原型,避免污染全局数组原型
const oldarrPrototype = Array.prototype
// 创建新原型对象,原型指向arrPrototype , 再扩展新的方法,不会影响原型
const newProto = Object.create(oldarrPrototype) ; // Array {}
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach((method) => {
newProto[method] = function() {
// 触发视图更新
updateView()
oldarrPrototype[method].call(this, ...arguments)
// 相当于 Array.prototype.push.call(this, ...arguments)
}
})
// 监听数据
observe(data)
// 监听数据
function observe(obj) {
if (!obj || typeof obj !== 'object') return
// 数组特殊处理
if (Array.isArray(obj)) {
obj.__proro__ = newProto
return
}
// 重新定义各个属性
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
// 定义各个属性
function defineReactive(obj, key, value) {
// 如果属性值为对象,需深听监听,递归子属性
observe(value)
Object.defineProperty(obj, key, {
get: function() {
console.log('get')
return value
},
set: function(newVal) {
if (newVal !== value) {
// 如果新属性值为对象,需深听监听,递归子属性
observe(newVal)
console.log('set')
value = newVal
// 更新视图
updateView()
}
}
})
}
function updateView() {
console.log('更新视图')
}
// data.name= '杨家祈'
// data.info.age = 8 // get -> set -> '更新视图'
data.nums.push(2) // 如果不特殊处理数组的监听, 无法立即更新视图
console.log(data)
注: 为了解决以上问题, vue3.0启用Proxy实现响应式
,但proxy兼容性不好,无法使用polyfill虚拟DOM: js模拟DOM结构,计算出最小的变更,操作DOM
产生背景:
当传统的api或jQuery操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程.轻微的触碰可能就会导致页面重排,频繁操作可能导致页面卡顿,耗费性能.
vue和react都是数据驱动视图,不直接操作DOM,如何有效操作DOM?
相对于DOM对象,原生的 JS 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以用 JS对象表示出来.JS的执行速度很快, 特别v8引擎普及的背景下.
JS模拟DOM结构:
<div id="div" class="container">
<p>vdomp>
<span style="color:#f0f0f0f">
<a>diff算法a>
span>
div>
{
tag: 'div',
props: {
id: 'div',
className: 'container'
},
children: [
{
tag: 'p',
children: 'vdom'
},{
tag: 'span',
props: {style: 'color: #f0f0f0'},
children: [
{
tag: 'a',
children: 'diff算法'
}
]
}
]
}
diff算法是vdom中最核心,最关键的部分。diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
树diff的时间复杂度O(n^3) ,如遍历tree1/遍历tree2 /排序 ,三个步骤下来, 如果是1000个节点就计算1亿次,算法不可用, 基于以下几点,优化时间复杂度为O(n):
以循环渲染v-for
的key
举例说明:
key的作用就是用来跟踪节点身份, 对比组件自身新旧DOM进行更新的, diff算法
会根据key值去对新旧元素进行比对,如果key值对应的dom元素被改动的话,则会进行重新渲染,删除之前的旧元素将新的元素放进去.
注: key不能乱用,尽量使用和业务实体相关联的值,尽量不用index!!
let list = ['a', 'b', 'c']
<!--遍历数组 -->
<ul>
<!--key值为item,如果把a,b的值调换,现key值所对应的dom元素没有变化,此时同样的只需变换元素排列顺序就可以了 -->
<li v-for="(item, index) in list" :key="item"></li>
<!--key值为index,如果把a,b的值调换,通过key值去对比,发现index对应的值不一样,则会删除之前旧的元素,生成新的元素,会增加DOM操作次数,降低性能-->
<li v-for="(item, index) in list" :key="index"></li>
</ul>
模板不是html, 它有插值,指令,js表达式,能实现循环,判断。html是标签语言,只有JS能实现循环判断等,因此把模板转化为某种JS代码(虚拟 DOM 渲染函数),即为模板编译
编译过程:
模板解析为render函数,执行render函数返回vnode;
const template = '{{message}}
'
//message : 11
// 模板编译
with(this) {
// 创建p标签 返回vnode 11
return _c('p', [_v(_s(message))])
}
// _c createElement -> vnode
// _v createTextNode
// _s toString
基于vnode再执行patch和diff
渲染和更新
elem:挂载节点
使用Object.defineProperty将data中的所有属性都转为存取器属性,然后在首次渲染过程中把属性的依赖关系记录下来并为这个Vue实例添加观察者。当数据变化时,setter会调用Dep.notify通知订阅者数据变动,最后订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。
Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少
渲染函数