组件化基础
‘很久以前’就有组件化
- asp jsp php 已经有组件化了
- nodejs中也有类似的组件化
数据驱动视图(MVVM,setState)
- 传统组件,只是静态渲染,更新还要依赖于操作DOM
- 数据驱动视图 - Vue MvvM
- 数据驱动视图 - React setState
在很早以前我们就有了“组件化”的思想,也就是同一个模板,根据后台的数据渲染不同的内容,但这个过程是通过dom去静态渲染的,所以当时jquery很流行。后来,在这个基础上,逐渐发展成“数据驱动视图”,也就是我们说的MVVM模型。
MVVM是Model - View - ViewModel的简写,数据和视图之间变更是通过ViewModel去实现的,数据更新时,通过ViewModel去通知视图更新;当视图更新时,也是通过ViewModel去修改数据,开发者不再将重心放在dom的操作上,而是让开发者更关注于数据和业务,dom操作则由ViewModel去实现,也就是Vue和React框架帮我们做的事。
但是Vue和React又不是完全遵循MVVM模型的,因为它们允许用户通过ref去获取dom,并进行对应的dom操作,这又与MVVM是相悖的,所以说它们不是完全遵循的。
Vue响应式
- 组件data 的数据一旦变化,立刻触发视图更新
- 实现数据驱动视图的第一步
- 核心API -
Object.defineProperty Object.defineProperty的一些缺点(Vue3.0启用Proxy)Proxy兼容性不好,且无法使用polyfill
Object.defineProperty 基本用法
/**
* Object.defineProperty 基本用法
*/
const data = {}
const name = 'zhangsan'
Object.defineProperty(data,"name",{
get:function(){
console.log('get')
return name
}
set:function(newValue){
console.log('set')
name = newValue
}
})
//测试
console.log(data.name) //get zhangsan
data.name = 'lisi' //set
深度监听,对象、数组
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型 用于将数组进行监听
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
updateView() // 触发视图更新
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
data.name = 'lisi'
data.age = 21
console.log('age', data.age)
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
Object.defineProperty缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性、删除属性(Vue.set Vue.delete)
- 无法原生监听数组,需要特殊处理
虚拟DOM(Virtual DOM) 和 diff
- vdom 是实现vue和React的重要基石
- diff算法是vdom中最核心、最关键的部分
- vdom- 是用JS模拟DOM结构,计算出最小的变更,操作DOM
- 数据驱动视图的模式下,有效控制DOM操作
用JS(vnode)模拟DOM结构
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size:20px">
<li>a</li>
</ul>
</div>
{
tag:"div",
props:{
className:"container",
id:"div1"
},
children: [
{
tag:"p",
children: "vdom"
},{
tag: 'ul',
props:{style:'font-size:20px'}
children:[
{
tag:li,
children: a
}
// ...
]
}
]
}
通过 snabbdom 学习vdom
- 简单强大的
vdom库,易学易用 - Vue参考它实现的
vdom和diff - Vue3.0重写了vdom的代码,优化了性能
- React vdom 具体实现和Vue也不同,但不妨碍统一学习
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h' // helper function for creating vnodes
const patch = init([ // Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
])
const container = document.getElementById('container')
const 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)
const 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
diff算法
- diff算法vdom中最核心,最关键的部分
- diff算法能在日常中使用vue React 中体现出来(如Key)
- diff算法是前端热门话题,面试'宠儿
- diff即对比,是一个广泛的概念,如linux diff命令。
gitdiff等 - 两个js对象也可以做diff,如 github.com/cujojs/jiff
- 两棵树做diff,如这里的vdom diff
diff算法概述
树diff的时间复杂度O(n^3)
- 第一,遍历tree1;第二,遍历tree2
- 第三,排序
- 1000个节点,要计算1亿次。算法不可用
优化时间复杂度到O(n)
- 只比较同一层级,不跨级比较
- tag不相同,则直接删掉重建,不再深度比较
- tag和key,两者都相同,则认为是相同节点,不再深度比较
模板编译
- Vue模板不是html,有指令、有插值、JS表达式
- 是“组件渲染和更新过程”中的一部分
- 前置知识:JS的with语法
- Vue template complier 将模板编译为render 函数
- 执行render函数生成vnode
with语法
const obj = {a:100,b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) //undefined
// 使用 with , 能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当做 obj 的属性来查找
with(obj) {
console.log(a)
console.log(b)
console.log(c) // 会报错!!
}
- 改变{}内自由变量的查找规则,当做obj属性来查找
- 如果找不到匹配的obj属性,就会报错
- with要慎用,它打破了作用域的规则,易读性变差
编译模板
模板不是html,有指令、插值、js表达式,能实现判断、循环 html是标签语言,只有JS才能实现判断、循环(图灵完备)
const compiler = require('vue-template-compiler')
// 插值
const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
h -> vnode
createElement -> vnode
// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// // 属性和动态属性
const template = `
<div id="div1" class="container">
<img :src="imgUrl"/>
</div>
`
// with(this){return _c('div',
// {staticClass:"container",attrs:{"id":"div1"}},
// [
// _c('img',{attrs:{"src":imgUrl}})])}
// 条件
const template = `
<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>
`
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
// 循环
const template = `
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
`
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
const template = `
<button @click="clickHandler">submit</button>
`
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
/* with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:
(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:
{"input":function($event{if($event.target.composing)return;name=$event.target.value}}})}*/
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._o = markOnce;
// target._n = toNumber;
// target._s = toString;
// target._l = renderList;
// target._t = renderSlot;
// target._q = looseEqual;
// target._i = looseIndexOf;
// target._m = renderStatic;
// target._f = resolveFilter;
// target._k = checkKeyCodes;
// target._b = bindObjectProps;
// target._v = createTextVNode;
// target._e = createEmptyVNode;
// target._u = resolveScopedSlots;
// target._g = bindObjectListeners;
// target._d = bindDynamicKeys;
// target._p = prependModifier;
// }
- 模板编译为render函数,执行render函数返回vnode
- 基于vnode再执行patch和diff
- 使用webpack vue-loader,会在开发环境下编译模板(重要)
扩展Vue组件中使用render代替template
Vue.component('heading',{
//template: `xxx`,
render:function (createElement){
return creatElement(
'h' + this.level,
[
createElement('a', {
attrs:{
name: 'headerId',
href: '#' + 'headerId'
}
},'this is a tag')
]
)
}
- })
- 但容易出现语意不明。
- 在有些复杂的情况中,不能用template,可以考虑用render
- React一直都用render(没有模板),和这里一样
小结
- 模板到render函数,再到vnode,再到渲染和更新
- vue组件可以用render 替代 template
组件渲染/更新的过程
- 一个组件渲染到页面,修改data触发更新(数据驱动视图)
- 响应式:监听 data 属性 getter setter (包括数组)
- 模板编译:模板到render函数,再到vnode
- vdom:patch(elem,vnode)和patch(vnode,newVnode)
更新过程
初次渲染过程
- 解析模板为render函数(或在开发环境已完成,vue-loader
- 触发响应式,监听data属性getter setter
- 执行render函数,生成vnode,patch(elem,vnode)
更新过程
- 修改data,触发stter(此前在getter中已被监听)
- 重新执行render函数,生成newVnode
- patch(vnode,newVnode)
异步渲染
- 回顾$nextTick
- 汇总data的更改,一次性更新视图
- 减少DOM操作次数,提高性能
前端路由的原理
- 稍微复杂一点的SPA,都需要路由
- vue-router也是vue全家桶的标配之一
Vue-router路由模式
- hash
- H5 history
网页url组成部分
hash的特点
- hash变化会触发网页跳转,即浏览器的前进、后退
- hash变化不会刷新页面,SPA必需的特点
- hash永远不会提交到server端(自生自灭)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hash test</title>
</head>
<body>
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
</script>
</body>
</html>
H5 history
- 用 url 规范的路由,但跳转时不刷新页面
- history.pushState
正常页面浏览 如图:
改造成H5 history模式 如图:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>history API test</title>
</head>
<body>
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
</script>
</body>
</html>
小结:
- hash -
window.onhashchange - h5 history -
history.pushState和window.onpopstate - history 需要后端支持 两者如何选择
- to B 的系统推荐使用hash,简单易用,对url规范不敏感
- to C 的系统,可以考虑选择H5 history, 但需要服务端支持
- 能选择简单的,就别用复杂的,要考虑成本和收益