vue响应式指的是:组件的data发生变化,立刻触发试图的更新
原理:
简单API:Object.defineProperty的使用
作用: 用来定义对象属性
特点:
响应式原理:
如何实现的监听数组
嵌套对象,如何实现深度监听
Object.defineProperty的几个缺点
<div class='vdom' id='first'>
<p>内容p>
<ul>
<li>1li>
<li>2li>
ul>
div>
{
tag: 'div',
data: {
className: 'vdom',
id: 'first'
},
children: [
{
tag: 'p',
children: '内容'
},
{
tag: 'ul',
children: [
{
tag: 'li',
children: '1'
},
{
tag: 'li',
children: '2'
}
]
}
]
}
init
方法会返回一个 patch
函数,这个函数接受两个参数,第一个是旧的 vnode
节点或是 dom
节点,第二个参数是新的 vnode
节点,调用 patch
函数会对 dom 进行更新。第一步:patch函数中对新老节点进行比较
// 用于 比较 新老节点的不同,然后更新的 函数
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 1. 当新节点不存在的时候,销毁旧节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
// 用来存储 insert 钩子函数,在 插入节点之后调用
const insertedVnodeQueue = []
// 2. 如果旧节点 是未定义的,直接创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 当老节点不是真实的 dom 节点, 当两个节点是相同节点的时候,进入 patctVnode 的过程
// 而 patchVnode 也是 传说中 diff updateChildren 的调用者
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 当老节点是真实存在的 dom 节点的时候
if (isRealElement) {
// 当 老节点是 真实节点,而是在 ssr 环境的时候,修改 SSR_ATTR 属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
....
// 设置 oldVnode 为一个包含 oldVnode 的无属性节点
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
// 获取父亲节点,这样方便 删除或者增加节点
const parentElm = nodeOps.parentNode(oldElm)
// 在 dom 中插入新节点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 递归 更新父占位符元素
// 就是执行一遍 父节点的 destory 和 create 、insert 的 钩子函数
// 类似于 style 组件,事件组件,这些 钩子函数
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 销毁老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 触发老节点 的 destory 钩子
invokeDestroyHook(oldVnode)
}
}
}
// 执行 虚拟 dom 的 insert 钩子函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回最新 vnode 的 elm ,也就是真实的 dom节点
return vnode.elm
}
如何判断两个节点是否相同?key、tagName、标签属性、input标签还要比较type类型
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是的时候,type必须相同
)
}
function sameInputType (a, b) {
if (a.tag !== 'input') { return true }
var i;
var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type;
var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type;
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
第二步:patchVnode函数比较两个虚拟节点内部
updateChildren
比较children的差异,这里就是diff算法的核心function patchVnode (
oldVnode, // 旧节点
vnode, // 新节点
insertedVnodeQueue, // 插入节点的队列
ownerArray, // 节点 数组
index, // 当前 节点的
removeOnly // 只有在 patch 函数中被传入,当老节点不是真实的 dom 节点,当新老节点是相同节点的时候
) {
// 如果新节点和旧节点 相等(使用了 同一个地址,直接返回不进行修改)
// 这里就是 当 props 没有改变的时候,子组件不会做渲染,而是直接复用
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// 当 当前节点 是 注释节点(被 v-if )了,或者是一个 异步函数节点,那不执行
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 当前节点 是一个静态节点的时候,或者 标记了 once 的时候,那不执行
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
// 调用 prepatch 的钩子函数
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
// 调用 update 钩子函数
if (isDef(data) && isPatchable(vnode)) {
// 这里 的 update 钩子函数式 vnode 本身的钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 这里的 update 钩子函数 是 用户传过来的 钩子函数
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 新节点 没有 text 属性
if (isUndef(vnode.text)) {
// 如果都有子节点,对比更新子节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 新节点存在,但是老节点不存在
// 如果老节点是 text, 清空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 增加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 老节点存在,但是新节点不存在,执行删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) { // 如果老节点是 text, 清空
nodeOps.setTextContent(elm, '')
}
// 新旧节点 text 属性不一样
} else if (oldVnode.text !== vnode.text) {
// 将 text 设置为 新节点的 text
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 执行 postpatch 钩子函数
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
第三步:updateChildren函数子节点进行比较diff算法
oldStartIdx++
&& newStartIdx++
),真实dom不变,进入下一次循环;不相似,进入第二步。oldEndIdx--
&& newEndIdx--
),真实dom不变,进入下一次循环;不相似,进入第三步。oldStartIdx++
&& newEndIdx--
),未确认dom序列中的头移到尾,进入下一次循环;不相似,进入第四步。oldEndIdx--
&& newStartIdx++
),未确认dom序列中的尾移到头,进入下一次循环;不相似,进入第五步。newStartIdx++
);否则,vnode对应的dom(vnode[newStartIdx].elm
)插入当前真实dom序列的头部,新头指针后移(即 newStartIdx++
)。但结束循环后,有两种情况需要考虑:
newStartIdx > newEndIdx
)。那就需要把多余的旧dom(oldStartIdx -> oldEndIdx
)都删除,上述例子中就是c,d
;oldStartIdx > oldEndIdx
)。那就需要把多余的新dom(newStartIdx -> newEndIdx
)都添加。function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 未定义表示被移动过
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // 头头相似
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相似
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // 头尾相似
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // 尾头相似
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 根据旧子节点的key,生成map映射
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 在旧子节点数组中,找到和newStartVnode相似节点的下标
idxInOld = oldKeyToIdx[newStartVnode.key]
if (isUndef(idxInOld)) {
// 没有key,创建并插入dom
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// 有key,找到对应dom ,移动该dom并在oldCh中置为undefined
elmToMove = oldCh[idxInOld]
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = undefined
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
// 循环结束时,删除/添加多余dom
if (oldStartIdx > oldEndIdx) {
before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
所以时间复杂度就是O(n^3),复杂度太高,算法不推荐使用
只比较同一层级
tag不同,直接删掉,不再继续深度比较
tag和key都相同不再深度比较,认为是相同节点
key
可以优化v-for
的性能,到底是怎么回事呢?因为v-for
大部分情况下生成的都是相同tag
的标签,如果没有key标识,那么相当于每次头头比较都能成功。你想想如果你往v-for
绑定的数组头部push数据,那么整个dom将全部刷新一遍(如果数组每项内容都不一样)
解析模板为render函数
触发响应式,监听data的getter和setter,初次渲染并不会触发setter,但是如果模板中使用data中的数据
就会触发getter
<div id="app">
<p>{{Value1}}p>
div>
<script>
var app = new Vue({
el: '#app',
data: {
Value1 : '你好' ,// 触发getter
value2 : '哈哈哈' // 不触发getter
}
})
script>
执行render函数,生成虚拟dom,patch(elem,vnode)
<body>
<div id="app">
<input type="text" v-model="inputValue">
<p>{{inputValue}}p>
div>
<script>
var app = new Vue({
el: '#app',
data: {
inputValue : ''
}
})
script>
body>
在input中输入完内容的时候,调用了change事件,改变了inputValue的值:
v-model 会忽略所有表单元素的 value、checked、selected 特性的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值。
v-model 在不同的 HTML 标签上使用会监控不同的属性和抛出不同的事件:
value
属性和 input
事件;checked
属性和 change
事件;value
作为 prop 并将 change
作为事件。在通过响应式原理,通过监听inputValue的改变出发视图的变化
<input v-model="val">
<input :value="val" @input="val = $event.target.value">
hash变化触发网页的跳转,即浏览器的前进和后退
hash变化不会刷新页面,spa必须的特点
hash永远不会提交到server端
1、hash模式较丑,history模式较优雅;
2、pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL;
3、pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中;
4、pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串;
5、pushState可额外设置title属性供后续使用;
6、hash兼容IE8以上,history兼容IE10以上;
7、history模式需要后端配合将所有访问都指向index.html,否则用户刷新页面,会导致404错误。