组件化
- 传统组件,只是静态渲染,更新还要依赖于操作DOM
- 数据驱动视图-Vue MVVM
- 数据驱动视图-React setState
响应式
- 组件data的数据一旦变化,立刻触发视图的更新
- 核心API-Object.defineProperty(Vue3.0启用Proxy,Proxy兼容性不好,且无法polyfill)
const data={}
const name="wilber"
Object.defineProperty(data,"name",{
get:function(){
console.log("get")
return name
},
set:function(newVal){
console.log("set")
name=newVal
}
})
监听对象,监听数组
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
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: 'wilber',
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) // 监听数组
几个缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性(Vue.set、Vue.delete)
- 无法原生监听数组,需要特殊处理
vdom和diff
用JS模拟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"
},
context:"",
children:[
{
tag:"p",
context:"vdom",
children:[]
},
{
tag:"ul",
props:{
style:"font-size:20px"
},
context:"",
children:[
{
tag:"li",
context:"a",
children:[]
}
//...
]
}
]
}
虚拟DOM实现
function createComponent(tag) {
let child = [];
for(let i = 0; i < tag.children.length; i++) {
if(tag.children[i].children.length <= 0) {
child[i] = {
tag: tag.children[i].nodeName.toLowerCase(),
props: tag.children[i].attributes,
context: tag.children[i].innerText,
children: []
};
} else {
child[i] = createComponent(tag.children[i]);
}
}
let vNode = {
tag: tag.nodeName.toLowerCase(),
props: tag.attributes,
children: child,
context: tag.childNodes[0].nodeValue
}
return vNode;
}
通过snabbdom学习vdom
- 简洁强大的vdom库,易学易用
- Vue参考它实现的vdom和diff
- https://github.com/snabbdom/snabbdom
树diff的时间复杂度O(n^3)
- 遍历tree1,遍历tree2,排序
- 1000个节点要计算1亿次,算法不可用
优化时间复杂度到O(n)
- 只比较同一层级,不跨级比较
- tag不相同,则直接删掉重建,不再深度比较
- tag和key,两者都相同,则认为是相同节点,不再深度比较
- oldS和newS相同,不发生变化,oldS++,newS++
oldS = a,oldE = d
newS = a,newE = c
- newS与OldVnode不匹配,oldS前面插入f,newS++
oldS = b,oldE = d
newS = f,newE = c
- newS与oldE相同,oldE移动到oldS前面,newS++,oldE–
oldS = b,oldE = d
newS = d,newE = c
- newE与oldE相同,不发生变化,newE–,oldE–
oldS = b,oldE = c
newS = e,newE = c
- 都不相同,oldS前插入newE,删除oldS,oldS++,newS++,newE–,oldE–
oldS = b,oldE = b
newS = e,newE = e
patch
- 找到对应的真实dom,称为el
- 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点
- 如果oldVnode有子节点而Vnode没有,则删除el的子节点
- 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
- 如果两者都有子节点,则执行updateChildren函数比较子节点
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
function patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
// oldVnode起始位置小于结束位置并且newVnode起始位置小于结束位置
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// isUndef 用来判断对象是否等于undefined或者为空,是的话返回true
if (isUndef(oldStartVnode)) {
// oldVnode 起始位置oldS++
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// oldVnode 结束位置oldE--
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// oldS和newS相同,不变化,进行patch,oldS++,newS++
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// oldE和newE相同,不变化,进行patch,oldE--,newE--
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// oldS和newE相同,oldS移动到oldE之后,进行patch,oldS++,newE--
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// oldE和newS相同,oldE移动到oldS之前,进行patch,oldE--,newS++
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 全都不相同情况下
// 获取oldVnode->index的key
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
// oldVnode->index为undefined或null,说明没有该元素,创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
// 获取oldVnode
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
// 创建的Vnode和newS相同,插入到oldS之前,进行patch
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// 相同的key但是不一样的element. 被视为新的element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 当oldS>oldE时,将newS至newE间的全部插入
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// 当newS>newE,将oldS至oldE间的全部删除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
模版编译
- JS的with语法
const obj = {a:100, b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c)//undefined
//使用with,能改变{}内自由变量的查找方式
//将{}内自由变量,当作obj的属性来查找
//with要慎用,它打破了作用域规则,易读性
with(obj){
console.log(a)
console.log(b)
console.log(c)//报错
}
- vue template complier将模版编译为render函数,执行render函数生成vnode
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;
// target._c = createElement;
// }
-
基于vnode再执行patch和diff
-
使用webpack vue-loader,会在开发环境下编译模版
-
vue组件中可以使用render代替template
Vue.component("heading",{
//template:"xxx"
render:function(createElement){
return createElement(
"h"+this.level,
[
createElement("a",{
attrs:{
name:"headerId",
href:"#"+"headerId"
}
},"this is a tag")
]
)
}
})
渲染/更新过程
初次渲染过程
- 解析模版为render函数(或在开发环境已完成,vue-loader)
- 触发响应式,监听data属性getter、setter
- 执行render函数,生成vnode,patch(elem,vnode)
更新过程
- 修改data,触发setter(此前在getter中已被监听)
- 重新执行render函数,生成newVnode
- patch(node,newVnode)
异步渲染
$nextTick
汇总data的修改,一次性更新视图
减少DOM操作次数,提高性能
前端路由
hash
//http://127.0.0.1:8881/01-hash.html?a=100&b=20#/aaa/bbb
location.protocol //"http:"
location.hostname //"127.0.0.1"
location.host //"127.0.0.1:8881"
location.port //"8881"
location.pathname //"/01-hash.html"
location.search //"?a=100&b=20"
location.hash //"#/aaa/bbb"
- 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
- window.onpopstate
<!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>