数据驱动视图
MVVM:model(数据) + view(视图) + view model(view 和 model 层的连接,比如:事件)
MVVM就是将其中View的状态和行为抽象化,其中ViewModel将试图(即View)和业务逻辑分开,它可以去除Model的数据的同时帮忙处理View中由于需要展示内容而涉及的业务逻辑。
MVVM采用:双向数据绑定。
View中数据变化将自动反映到Model上,反之,Model中数据变化也将会自动展示在页面上。
ViewModel就是View和Model的桥梁。
ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回到Model。
Vue 响应式: Vue通过defineProperty完成了Data中所有数据的代理,当数据触发get查询时,会将当前的Watcher对象加入到依赖收集池Dep中,当数据Data变化时,会触发set通知所有使用到这个Data的Watcher对象去update视图。
Vue 响应式:
**核心API - Object.definProperty
**
Object.definProperty 的一些缺点(vue3.0 启用 Proxy)
Object.definProperty 实现响应式:
监听对象,监听数组
复杂对象,深度监听
缺点
Object.definProperty 缺点:
深度监听,需要递归到底,一次性计算量大
无法监听新增属性/删除属性(使用 Vue.set Vue.delete)
无法原生监听数组,需要特殊处理
// 监听数据变化。步骤:**1. 对象监听;2. 深度监听(递归);3. 监听数组;**
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 3.2 数组方法处理,这里稍微复杂一点
const oldArrayPrototype = Array.prototype
const arrProto = Object.create(oldArrayPrototype); // 创建新对象,原型指向 oldArrayPrototype ,再扩展属性不会影响 oldArrayPrototype
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
// 重新定义这些新方法,监听数组变化
arrProto[method] = function () {
updateView() // 触发更新视图
oldArrayPrototype[method].call(this, ...arguments) // 调用原有方法
}
})
// 2.2 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 2.3 value 如果是对象,需要递归监听,即深度监听 —— 注意这里的递归,而且是在数据监听时,一次性递归完成!!!
// (如果 value 不是对象,observer 中会做判断)
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 2.3 设置的值,也需要监听起来,即深度监听
observer(newValue)
// 设置新值
value = newValue // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值!!!
// 触发更新视图
updateView()
}
}
})
}
// 2.1 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
if (Array.isArray(target)) {
// 重写数组的原型
target.__proto__ = arrProto
}
// 重新定义各个属性
for (let key in target) { // 遍历对象或者数组
defineReactive(target, key, target[key])
}
}
// 1. 准备数据
const data = {
name: 'zhangsan',
info: {
address: '北京'
},
nums: [10, 20, 30]
}
// 2. 监听数据
observer(data)
// 测试
data.name = 'lisi'
data.info.address = '上海' // 深度监听
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
data.nums.push(4) // 监听数组
vdom 是实现 vue 和 React 的重要基石
diff 算法是 vdom 中最核心、最关键的部分
解决方案 - vdom:
有了一定复杂度,想减少计算次数比较难
能不能把计算更多的转移为 JS 计算,因为 JS 执行速度很快
vdom - 用 JS 模拟 DOM 结构,计算出最小的变更,操作 DOM
snabbdom:
简洁强大的 vdom 库,易学易用
Vue 参考它实现的 vdom 和 diff
snabbdom 重点总结:h 函数;vnode 数据结构;patch 函数。
vdom 总结:
用 JS 模拟 DOM 结构(vnode)
新旧 vnode 对比,得出最小的更新范围,最后更新 DOM
数据驱动视图的模式下,有效控制 DOM 操作
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script type="text/javascript">
const snabbdom = window.snabbdom
// 定义关键函数 patch
const patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义关键函数 h
const h = snabbdom.h
// 原始数据
const data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 把表头也放在 data 中
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
const container = document.getElementById('container')
// 渲染函数
let vnode
function render(data) {
const newVnode = h('table', {}, data.map(item => {
const tds = []
for (let i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''))
}
}
return h('tr', {}, tds)
}))
if (vnode) {
// re-render
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}
// 存储当前的 vnode 结果
vnode = newVnode
}
// 初次渲染
render(data)
const btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', () => {
data[1].age = 30
data[2].address = '深圳'
// re-render
render(data)
})
</script>
</body>
</html>
diff 算法:diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom。
diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。
旧children 有,新children无,移出旧children;旧text 有,新text无,移出旧text
key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁得进行重绘和回流。
所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。
patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。
patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点。(addVnodes removeVnodes)
updateChildren
with 语法:
改变 {} 内自由变量的查找规则,当做obj 属性来查找
如果找不到匹配的 obj 属性,就会报错
with 要慎用,它打破了作用域规则,易读性变差
编译模板:(vue-template-compiler)
模板不是 html ,有指令、插值、JS 表达式,能实现判断、循环
html 是标签语言,只有 JS 才能实现判断、循环(图灵完备的)
因此,模板一定是转换为某种 JS 代码,即编译模板
模板编译为 render 函数,执行 render 函数返回 vnode
基于 vnode 再执行 patch 和 diff
使用 webpack vue-loader ,会在开发环境下编译模板(重要)
组件 渲染/更新 过程:
初次渲染过程:
1,解析模板为 render 函数(或在开发环境已完成,vue-loader)
2,触发响应式,监听 data 属性 getter setter
3,执行 render 函数,生成 vnode,patch (elem,vnode)
更新过程:
1,修改 data ,触发 stter (此前在 getter 中已被监听)
2,重新执行 render 函数,生成 newVnode
3,patch (vnode,newVnode)
异步渲染:
1,回顾 $nextTick
2,汇总 data 的修改,一次性更新视图
3,减少 DOM 操作次数,提高性能
hash 的特点:
hash 变化会触发网页跳转,即浏览器的前进、后退
hash 变化不会刷新页面,SPA 必需的特点
hash 永远不会提交 server 端(前端自生自灭)
H5 history :
用 URL 规范的路由,但跳转时不刷新页面
history.pushState
window.onpopstate
两者选择:
to B 的系统推荐用 hash ,简单易用,对 url 规范不敏感
to C 的系统,key考虑选择 H5 history ,但需要服务端支持
能选择简单的,就别用复杂的,要考虑成本和收益