MVVM: M(Model),V(View),VM(ViewModel),即视图和数据模型之间通过ViewModel实现了双向绑定,数据的修改会引发视图的更新,这里视图表示的就是DOM元素,即数据驱动视图,而DOM通过事件监听可以来触发数据的修改。
对于对象(data),核心API就是-Object.defineProperty(obj, prop, descriptor),通过对descriptor对象中的set
函数做劫持,达到对于对象属性改变的监听。Vue通过defineReactive函数对于对象进行深度监听。
对于数组,考虑到不能污染全局Array的原型,Vue在监听数组时采用劫持数组对象实例的原型,重写了数组push,pop,shift,unshift,splice,sort,reverse
方法,再其执行时触发视图更新的逻辑。
// 重新定义数组原型
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: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
Object.defineProperty的缺点
对于数组监听的局限
为什么要有vDOM? 因为DOM操作非常耗费性能,考虑把DOM的计算转移到JS中计算,因为JS的执行速度较快,于是就产生了vDOM,即通过JS模拟DOM结构,计算出最小的变更,操作DOM。
Vue2的VDOM参考了Snabbdom,所以通过Snabbdom来了解VDOM会比较直观
// 官方例子
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
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");
// h函数(类似Vue的render函数,返回值为一个vnode)
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函数是关键,有两种情况,一种是patch(DOMElement,vnode),类似初次渲染vnode到DOM结点上
patch(container, vnode);
// 新的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数据改变,patch(oldVnode,newVNode);
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
VNode的diff算法
传统的diff算法,时间复杂度太高,几乎不可用,vue的diff算法将时间复杂度优化到O(N),具体优化措施为
diff关键-patch过程
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 用于收集所有插入的元素
const insertedVnodeQueue: VNodeQueue = [];
// 先调用 pre 回调
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果老节点非 vnode , 则创建一个空的 vnode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 如果是同个节点,则进行修补
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 不同 Vnode 节点则新建
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
// 插入新节点,删除老节点
if (parent !== null) {
api.insertBefore(
parent,
vnode.elm as Node,
api.nextSibling(elm)
);
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 遍历所有收集到的插入节点,调用插入的钩子,
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks)
.insert as any)(insertedVnodeQueue[i]);
}
// 调用post的钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
pre
钩子vnode,
则新创建空的 vnode
sameVnode
的话,则调用 patchVnode
更新 vnode
, 否则创建新节点insert
钩子post
钩子diff关键-patchVnode 过程
prepatch
钩子update
钩子, 这里主要为了更新对应的 module
内容api.setTextContent(elm, vnode.text as string);
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
// 设置 vnode.elem
const elm = vnode.elm = oldVnode.elm!;
// 旧 children
let oldCh = oldVnode.children as VNode[];
// 新 children
let ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
// hook 相关
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
// vnode.text === undefined (vnode.children 一般有值, 非文本节点)
if (isUndef(vnode.text)) {
// 新旧都有 children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 新 children 有,旧 children 无 (旧 text 有)
} else if (isDef(ch)) {
// 清空 text
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 添加 children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 旧 child 有,新 child 无
} else if (isDef(oldCh)) {
// 移除 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// 旧 text 有
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
// else : vnode.text !== undefined (vnode.children 无值)
} else if (oldVnode.text !== vnode.text) {
// 移除旧 children
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置新 text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
diff关键-updateChildren
patchVnode
里面最重要的方法,也是整个 diff
里面的最核心方法
updateChildren
主要的逻辑如下:
不提供 key 的情况下,如果只是顺序改变的情况,例如第一个移动到末尾。这个时候,会导致其实更新了后面的所有元素。这也是为什么v-for生成的结点要有key
前置知识——with语法
const obj = {a:100,b:200};
with(obj){
// with会改变{}作用域内自由变量的查找方式,这里将自由变量当做obj的属性来查找
console.log(a); // 100
console.log(b); // 200
console.log(c); // 报错
}
模板中的条件渲染、指令、循环、事件都是编译成JS代码来计算和执行的,利用vue-template-compiler可以查看模板编译后的代码
// // 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
const res = compiler.compile(template)
console.log(res.render)
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// // 条件
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")])])}
// // 从 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;
// }
/* _c 是vue的createElement,类似h函数,返回值是一个vNode
模板编译的结果是一个render函数,执行render函数返回vnode,基于vnode再执行patch和diff
初次渲染:
更新过程:
new Vue({
template: '<div>{{val}}</div>',
data(){
return {
val: 'init'
}
},
mounted(){
this.val = '第一次修改';
this.val = '第二次修改'
}
})
上面这一段代码中,在mounted里给val属性进行了两次赋值,如果页面渲染与数据的变化完全同步的话,页面应该是在mounted里有两次渲染。
而由于Vue内部的渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的val只做一次页面渲染。
整个过程可以简述为以下过程:
notify () { // 通知存储的依赖更新
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// 循环watcher,发布订阅模式
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 调用watcher中的update方法
}
}
*watcher.update()中会使用queueWatcher()*将watcher通过id去重(多次修改同一数据,只会触发一次渲染)放到队列中
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 过滤watcher,每个watcher有一个id,多个属性依赖同一个watcher
if (has[id] == null) { // 如果没有就会添加进去
has[id] = true
if (!flushing) {
queue.push(watcher) // 并且将watcher放到队列中去
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue() // 会做一个清空queue的操作
return
}
nextTick(flushSchedulerQueue) // 在下一个tick中刷新watcher队列
}
}
}
在nextTick回调中执行flushSchedulerQueue(), 取出每一个队列中的watcher,调用watcher.run()
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // 触发一个before方法
}
id = watcher.id
has[id] = null
watcher.run() // 开始执行watcher,执行完页面就渲染完成啦
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue) // 更新完成后会调用updated钩子
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
*watcher.run()*中会调用wacher.get(), 然后调用实例的getter方法,实例的getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update。实例的_update函数执行后,将会把两次的虚拟节点传入传入vm的 patch 方法执行渲染操作。
Vue.prototype._update = function (vnode, hydrating) { var vm = this; ... var prevVnode = vm._vnode; vm._vnode = vnode; if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // updates vm.$el = vm.__patch__(prevVnode, vnode); } ...};
hash路由
// hash变化不会引起页面刷新window.onhashchange = (event)=>{ // event.oldURL // event.newURL // location.hash}
H5 history路由
使用url规范的路由,但是跳转的时候不会刷新页面
history.pushState(state, title, url)
window.onpopstate
$('.btn').click(()=>{ const state = {name:'history路由'}; history.pushState(state, '标题参数', 'page1'); // 这里路由跳转了})// 监听路由器前进 or 后退window.onpopstate = (event) => { console.log(event.state, location.pathname)}
history路由的实现需要后端配合,否则手动刷新页面的时候会发送http请求到后端,而不是前端处理路由。
首先,key必须具有唯一性,不要使用index或者random。
diff算法中,通过key和tagName来判断是否是sameNode,这样不需要大量的销毁重建DOM,可以做更加细化的判断,减少渲染次数,提升渲染性能
父子组件: props | $emit事件
非父子组件: eventBus,vuex, provide/inject
当一个组件被定义,data
必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例
也就是说,在很多页面中,定义的组件可以复用在多个页面
如果data
是一个纯碎的对象,则所有的实例将共享引用同一份data
数据对象,无论在哪个组件实例中修改data
,都会影响到所有的组件实例
如果data
是函数,每次创建一个新实例后,调用data
函数,从而返回初始数据的一个全新副本数据对象
这样每复用一次组件,会返回一份新的data
数据,类似于给每个组件实例创建一个私有的数据空间,让各个组件的实例各自独立,互不影响,保持低耦合
Options API生命周期:
Compostion API:
Composition API带来了什么?
更好的代码组织
同一块逻辑所涉及的函数、响应式数据可以放在一块,而不是分散的放在data和methods中
更好的逻辑复用
功能代码可以抽离成单个函数,方便复用,而原先实现一个功能的data和method是分离的
更好的类型推导
Vue组件定义时的methods和data最后都是挂载到vue实例上的(this.xxx可以访问),不利于类型的推导
简单功能使用Options API,复杂功能使用Composition API
ref是什么
toRef
将reactive的对象的一个属性创建ref,并保持引用关系
export default {
setup(){
const state = reactive({
age: 20,
name: "test"
})
const nameRef = toRef(state, 'name');
return {
state,
nameRef
}
}
}
toRefs
将一个reactive的对象变成普通对象,这个普通对象的每个属性都是ref
export default {
setup(){
const state = reactive({
age: 20,
name: "test"
})
const stateAsRefs = toRefs(state); // stateAsRefs是一个普通对象
// const {age:ageRef, name:nameRef} = stateAsRefs;
// return {ageRef,nameRef}
return stateAsRefs // 注意这里实际是返回{age,name},只是这所有的属性都是ref
}
}
实际上作用就是可以把reactive的对象解构,在模板中不用写reactiveObj.xxx,因为reactiveObj如果解构的话,就没有响应式了。
为何需要ref?(为什么不全用reactive)
为什么ref类型需要.value
ref作用于值类型,因为我们无法对基础数据类型值做拦截,只能将基础数据类型转换成了对应的包装类实例,比如
// let str = "abc"; 基础数据类型无法做拦截// 转化成包装类, 再拦截let str = new String('abc');str.valueOf(); // 'abc';
creatApp
emits属性: 声明事件
生命周期
多事件
Fragment
移除.sync
异步组件写法
移除filter
Teleport
Suspense
Composition API
// useMousePosition.js
import { reactive, ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update)
})
return {
x,
y
}
}
export default useMousePosition
// 分割===========================================
// 组件中
export default {
name: 'MousePosition',
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
}
}
// 依赖收集
const targetMap = new WeakMap(); // 存放多个对象的响应式数据
let activeEffect = null; // 表示当前运行的 effect
// 注册effect
function effect(eff) {
activeEffect = eff;
activeEffect(); // 调用eff的时候,会触发proxy.get的逻辑, 进而在track中将eff收集到deps中
activeEffect = null;
}
// 响应式依赖收集
function track(target, key) {
if (activeEffect) { // 防止多次重复的依赖收集
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
}
// 触发effect更新
function trigger(target, key) {
let depsMap = targetMap.get(target);
if (depsMap) {
let deps = depsMap.get(key);
if (deps) {
deps.forEach((effect) => effect());
}
}
}
// Proxy的handler
const reactiveHandler = {
get(target, key, receiver) {
track(target, key); // 只在effect注册的时候执行一次track收集依赖
return Reflect.get(target, key, receiver);
// ######!!如果要实现深度监听,return reactive(Reflect.get(target, key, receiver))!!!!!!#######
},
set(target, key, value, receiver) {
const oldVal = target[key];
console.log(`oldValue:${oldVal}, value:${value}`);
const res = Reflect.set(target, key, value, receiver); // 先赋值,再触发effect计算
if (oldVal !== value) {
trigger(target, key);
}
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key);
},
};
function reactive(obj) {
return new Proxy(obj, reactiveHandler);
}
function ref(raw) {
const r = {
get value() {
track(r, "value");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(r, "value");
},
};
return r;
}
function computed(getter) {
const r = ref();
effect(() => {
r.value = getter();
});
return r;
}
实例代码:
let obj = reactive({ a: 10, b: 20 });let timesA = ref(0);let sum = 0;effect(() => { timesA.value = obj.a * 10;});sum = computed(()=>{ return timesA.value + obj.b;})console.log(timesA); // 100obj.a = 100; // obj.a更新console.log(obj.a); // 多次使用obj.a的getter(之前在effect里面用过),只会收集一次依赖console.log(timesA.value); // 1000console.log(sum.value); // 期望 1020
对比Vue2响应式的实现Object.defineProperty的优势
Proxy实现响应式的局限(或者说Proxy的局限)
export default { setup(){ const numberRef = ref(0); const state = reactive({ name: 'Sam', age: 26 }) // 监听ref watch(numberRef, (newVal, oldVal)=>{ console.log(newVal); },{ immediate: true }) // 监听reactive, 第一个参数是一个函数,返回需要监听的属性 watch(()=>state.age,(newVal)=>{ console.log(newValu); }) // watchEffect,初始化时一定会执行一次,因为要收集依赖 watchEffect(()=>{ console.log('修改了:',state.name); console.log('修改了:',state.age); }) watchEffect(()=>{ console.log('只监听age:', state.age) }) // 如果延迟修改state.age,则上述两个watchEffect都会执行 // ... return { numberRef, state } }}
watchEffect高阶使用
/* onInvalidate作为一个参数传递,目的是消除副作用比如下面这个例子, id.value在短时间之内触发了多次修改,导致多次异步请求,但是无法保证响应回来的顺序的正确性因此要取消之前的请求*/watchEffect(onInvalidate => { const token = performAsyncOperation(id.value) onInvalidate(() => { // id has changed or watcher is stopped. // invalidate previously pending async operation token.cancel() })})
/*watchEffect的副作用函数是一个异步函数的情况需要在最前面注册清楚函数(异步函数都会隐式地返回一个 Promise, 必须要在Promise被resolve之前注册)*/const data = ref(null)watchEffect(async onInvalidate => { onInvalidate(() => { /* ... */ }) // 我们在Promise解析之前注册清除函数 data.value = await fetchData(props.id)})
import { onMounted, getCurrentInstance } from 'vue'
export default{
data() {
return {
x: 1,
y: 2
}
},
setup() {
console.log('this1', this)
onMounted(() => {
console.log('this in onMounted', this)
console.log('x', instance.data.x) // 注意setup函数是beforeCreate和created的合集,实例还没初始化的时候,没有data.x
})
const instance = getCurrentInstance()
console.log('instance', instance)
},
}
Proxy响应式
PatchFlag
hoistStatic
cacheHandler
SSR优化
tree-shaking
Vite为何启动快
ES Module相关特性
<script type='module'>
支持import写法,无需打包编译
支持远程引入import {xxx} from 'http://.........mjs'
支持动态引入(异步引入)
<script type="module">
btn.onClick = async () => {
const add = await import("./src/add.js");
console.log(add); // Module对象, 有一个default指向add函数
add.default(1,2);
const {multi} = await import('./src/math.js');
multi(1,2); // 结构的不用default
}
</script>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。