vue 编译方式分为两种,
一种是离线编译 工程化 + vue-loader
一种是在线编译 runtime + compiler
vue2 的模板编译是基于正则表达式的, 所以vue 2 运行时间长会有卡顿, 正则表达式会有回溯的过程,造成性能问题
vue3 的模板编译是状态机
vue 是编译时优化,+ 静态更新
react 是动态优化+ fiber
vue2 Objetc.defineProperty 是重写对象的某一个key
Objetc.defineProperty 新增的属性都不能触发set get 可以处理数组的变化 不是不能处理数组的变化,是因为会出现频繁的get set
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // 所以在这个声明周期拿不到data
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化数据
initProvide(vm)
callHook(vm, 'created') // 最早能拿到data的声明周期
浏览器的渲染流程:
vue 的渲染机制采用了虚拟dom 的方式,避免直接去操作dom 首先生成虚拟dom 然后在统一进行操作dom,提升性能。
vm._render()方法将render函数转化为Virtual DOM,然后再调用vm._update() 方法将虚拟dom转化成真实dom 。
NEW 一个实例渲染到页面的过程:
new vue 实例,首先是对options 进行merage操作,然后在进行一系列的init 操作,然后在调用$mount 进行挂载,$mount触发mountComponent的执行 ,mountComponent触发watch 中的 vmupdate进行页面渲染。
vm_update 触发 vm. patch__ vm._patch 触发patch 函数 然后渲染到真是dom 树_
/**
* @description:
* @param {*} vnode // vnode
* @param {*} hydrating //是否是服务端渲染
* @return {*}
* @author: liushuhao
*/
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
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)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
然后走到createElm方法中 这个方法最后就是调用insert 方法将 生成的dom 插到dom 树中
vm._update内部执行patch实现vnode到真实dom的映射过程,在patch中通过createElm去映射,映射其实就是根据vnode调用原生的操作dom的方法去创建dom节点并挂载到页面上,然后遍历它的children,递归调用createElm创建真实的dom节点;对于组件vnode会调用createComponent。
总结:
- options合并以及一些初始化操作
- $mount中如果是entry-runtime-with-compiler模式且没有render就编译
- render生成vnode
- patch vnode到真实dom
组件相关:
首先创建组件的vnode 然后patch 到dom树
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
上面代码先获取vnode.data上的init hook,获取到了就执行init hook,然后判断vnode.componentInstance是否存在,存在就调用initComponent,然后调用insert插入到dom树实现挂载。
在_render阶段生成了组件vnode,在patch阶段执行createElm遇到了组件vnode将执行createComponent,通过在createComponent中执行组件的init hook创建组件实例,并走完组件从init-patch的阶段,然后在createComponent最后调用insert把组件的dom插入到dom树中。
异步组件:
普通异步对象: 实质是一个Promise 内部定义了resolve, reject,在resolve中通过ensureCtor获取到异步组件的构造函数,然后在sync为false的情况下执行forceRender更新视图
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {}
}
高级异步组件: 实质也是promise,判断res对象的component是否是一个Promise对象,如果是就表明这是一个高级异步组件;loader 实质settimeout
响应式对象:
核心是Object.definproperty,能代理对象和数组的变化,重新定义get,set。
也是挂载vue实例时候初始化init 在initState方法中进行挂载的,方法中进行判断,options.data是否存在,如果不存在就新建一个observe 新建一个{},如果data中有值,就initData 进行初始化, initData中调用observe 方法进行数据初始化, 实际上就是对于对象进行代理,通过watch 添加dep 进行维护关系进行响应式对象的构建,对于数组vue 对能改变数组索引的方法进行了重写,因为Object.definproperty不能处理改变数组索引的变化,所以重写了数组的方法,新增的数据也进行了响应式处理。
依赖收集是发生在触发了响应式数据的getter特性函数的时候
domdiff:
比对标签
Diff
算法首先会对根节点进行比较,如果 tag
标签不一致,直接用新的 VNode
生成真实的 Dom 节点替换旧的 VNode
生成的Dom节点。
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
如果标签一致,有可能都是文本节点,那就比较文本的内容即可。
如果是文本,文本都没有 tag
,文本内容不一致,直接修改文本即可。
如果标签一致且不是文本就要比对属性。
比对属性
比对属性核心是复用Dom节点,并且将新的VNode
中的属性更新或新增到Dom节点中,并且删除多余的属性。
简单实现代码如下:
// 复用标签,并且更新属性
let el = vnode.el = oldVnode.el;
updateProperties(vnode,oldVnode.data);
function updateProperties(vnode,oldProps={}) {
let newProps = vnode.data || {};
let el = vnode.el;
// 比对样式
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
for(let key in oldStyle){
if(!newStyle[key]){
el.style[key] = ''
}
}
// 删除多余属性
for(let key in oldProps){
if(!newProps[key]){
el.removeAttribute(key);
}
}
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
el.className = newProps.class;
} else {
el.setAttribute(key, newProps[key]);
}
}
}
比对子节点
比对子节点只要存在4种情况:
- 新老
VNode
都有子节点,需要比对里面的子节点。核心是updateChildren
方法 - 新的
VNode
节点有子节点,老的VNode
节点没有子节点,则直接将VNode
子节点转成真实节点,插入真实的父节点即可 - 新的
VNode
节点没有子节点,老的VNode
节点有子节点,则真实的父节点删除子节点即可 - 新老
VNode
都没有子节点,不需进行处理
updateChildren
方法是dom-diff 的核心
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, vnodeToMove, refElm
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
} else if (isUndef(oldEndVnode)) {
} else if (sameVnode(oldStartVnode, newStartVnode)) {
} else if (sameVnode(oldEndVnode, newEndVnode)) {
} else if (sameVnode(oldStartVnode, newEndVnode)) {
} else if (sameVnode(oldEndVnode, newStartVnode)) {
} else {
}
}
updateChildren共分为七种条件
- oldStartVnode 为undefined ,oldStartIdx 递增,oldStartVnode指向下一节点。
- oldEndVnode 为undefined, oldEndIdx递减,oldEndVnode指向上一节点。
- oldStartVnode = newStartVnode ,真实dom中的相应节点会移到Vnode相应的位置,oldStartIdx 递增,newStartIdx递增,oldStartVnode指向下一节点,newStartVnode指向下一节点。
- oldStartVnode = newEndVnode 真实dom中的相应节点会移到Vnode相应的位置,oldStartIdx 递增,newEndIdx递减 oldStartVnode指向下一节点。newEndVnode指向上一节点。
- oldEndVnode和newEndVnode相似 ,真实dom中的相应节点会移到Vnode相应的位置,oldEndIdx递减,oldEndVnode指向上一节点。newEndIdx递减, newEndVnode指向上一节点。
- oldEndVnode = newStartVnode ,真实dom中的相应节点会移到Vnode相应的位置,newStartIdx递增,newStartVnode指向下一节点。oldEndIdx递减,oldEndVnode指向上一节点。
- 都不是,在情况7中在旧子节点数组中找newStartIndex指向的节点,因此就把它对应的dom节点移到insert到第一个位置,并把之前所在位置为undefined;
例子
const vm = new Vue({
el: '#app',
template: `<div>
<ul>
<li v-for="item in list" :key="itemy">{{item}}</li>
</ul>
<button @click="changeList">click me</button>
</div>`,
data() {
return {
list: [1, 3, 5, 4]
}
},
methods: {
changeList() {
this.list.push(2);
this.list.sort((a, b) => b - a);
}
}
});
就上面这个例子,list一开始是[1, 3, 5, 4];点击按钮执行changeList后,list变为[5, 4, 3, 2, 1];
list更新后,会进入到updateChildren的逻辑中,接下来我们通过图片来看一下patch diff的过程:
step1:
一开始oldStartIndex指向oldVnode_1,oldEndIndex指向oldVnode_4;newStartIndex指向newVnode_5,newEndIndex指向newVnode_1;第一步,因为oldStartVnode等于newEndVnode,所以会进入情况5,就变成如下:
step2:
**
在情况5中,通过把newVnode_5的配置更新到oldVnode_1上,然后调用原生的insertBefore方法把dom_1插入到dom_4后面,最后oldStartIndex加1,newEndIndex减1;接着进入下一个while循环,进入情况7;
step3:
在情况7中在旧子节点数组中找newStartIndex指向的节点,这里是newVnode_5,因此就把它对应的dom节点移到insert到第一个位置,并把之前所在位置即oldVnode_5置为undefined;newStartIndex指向newVnode_4;进入下一个循环,因为此时oldEndIndex指向与newStartIndex相同,所以会进入情况6:
step4:
在情况6中,更新newVnode_4的配置到oldVnode_4,然后把oldVnode_4对应的dom节点insert到oldStartVnode对应dom节点前面,这里是dom_3的前面;接着进入下一个循坏;因为oldEndIndex为undefined,所以会进入情况2:
step5:
如上,oldEndIndex会指向oldVnode_3,下一个循环因为oldStartIndex和newStartIndex是相等的,所以会进入情况3:
step6:
在情况3中,首先更新oldVnode_3的配置并patch到dom上,然后oldStartIndex和newStartIndex递增,当这一个循环结束后,因为oldStartIndex大于oldEndIndex,所以不会进入while循环了;
接下来就进行undateChildren的最后一步:
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
此时oldStartIdx是2,oldEndIdx是1.所以会进入第一个if逻辑,这里会把newCh[newEndIdx + 1]的elm节点赋值给refElm,这里就是newVnode_1这个节点;接着执行addVnodes:
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
}
}
addVnodes逻辑不复杂,就是把还在newVnodes中的却不再oldVnodes中,即新创建的节点insert到上面获取到的refElm前面,这里就是把新创建的节点dom_2添加到dom_1前面,如下:
此时页面就已经完全更新了;
从上面我们可以看到,patch diff不会修改旧vnode数组的数据顺序,只有一个置undefined的操作,更改视图是直接对比新旧vnode数组操作dom节点来实现更新;新生成的vnode数组在_update中挂载到了vm._vnode上,当再次触发更新的时候,旧vnode就是从vm._vnode上获取,与新生成的vnode做一个比较;
为什么要进行一个patch diff而不是重新挂载所有dom节点呢?
因为创建一个dom的开销是比较大的,除了创建dom,我们还有挂载在dom上的一些基本属性,还有监听事件等,当更新的地方很多时,重新创建一遍dom跟刷新浏览器重新请求资源加载无异,而对我们上面的例子,仅仅是更改了dom的顺序和新增一个dom节点,性能上来说明显比重新创建一遍dom要好得多。