一、前言
二、相关工具
1、vue2.6版本源码 github.com/vuejs/vue/t…
2、vue2 模板树生产工具 v2.template-explorer.vuejs.org/#
三、目录结构
- |----benchmarks 性能测试
- |----scripts 脚本文件
- |----scr 源码
- | |----compiler 模板编译相关
- | |----core vue2核心代码
- | |----platforms 平台相关
- | |----server 服务端渲染
- | |----sfc 解析单文件组件
- | |----shared 模块间共享属性和方法
四丶VUE实例化在源码中的核心流程
如下代码为例
<template>
<div id="app">
<p>{{ testString }}--- {{ testNumber }}--- {{ testApp }}</p>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
testArray: [{a: 'a'}, 'a'],
testString: 'string',
testNumber: 123
}
},
watch:{
testString(newVal, oldVal){
console.log(newVal, oldVal)
}
},
computed:{
testApp(){
return this.testString+this.testNumber
}
},
methods: {
changeTestString(){
this.testString = 'testString'
}
}
}
</script>
涉及到Vue的几个核心内容:模板语法,数据双向绑定,监听器,计算属性。
beforeCreate(){
console.log('beforeCreate data:', this.$data) // undefined
console.log('beforeCreate watch:', this.$watch) // function()
console.log('beforeCreate method', this.changeTestString) // undefined
},
created(){
console.log('created data:', this.$data) // obj{}
console.log('created watch:', this.$watch) // function()
console.log('created method', this.changeTestString) // function()
console.log('created el:', this.$el) // undefined
},
通过生命周期函数可以看出,在created阶段Vue已经完成了除dom节点挂载以外的初始化。
响应式数据(initData)
要实现数据的双向绑定,就要创建响应式数据,原理就是重写了$data中每项数据的getter和setter,这样就可以拦截到每次的取值或者改值的操作了,取值的时候收集依赖,改值的时候通知notify 。
this.$data.noGetterString = 'watcheGetterString'
console.log('mounted data:', this.$data)
可以看到,如果后续在data上面添加数据是不会创建getter和 setter方法的,同样也无法用watch去监听参数的变化。对此Vue在原型中拓展了 $set, $delete方法在data中创建响应式数据以及删除响应式数据。
监听器(Watcher)
-
Vue中的Watcher主要分为3类
render watcher、computed watcher和watcherAPI -
render watcher: 负责视图更新。data的数据更新存在异步的问题,在源码中 通过一个
dep的方法将observe与watcher关联在一起 -
computed watcher: 负责计算属性更新
-
watcher API: 用户注册在this.$watch 中的方法
AST树和render函数
<template>
<div id="app">
<p>{{ testString }}--- {{ testNumber }}--- {{ testApp }}</p>
</div>
</template>
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v(_s(testString) + "--- " + _s(testNumber) + "--- " +
_s(testApp))])])
}
使用with,vue实列执行到这个方法时,则会去找当前实例的属性。 而 _c, _s, _v等函数是用来将对应类型节点转换虚拟dom的,render执行后就能生成对应的虚拟dom树了。
依赖收集(发布订阅)
1.取值:在模板中取值的时候它就会进行依赖收集,执行dep.depend() , 最后会去重的watcher存在依赖的subs[] 中。去重是,如果模板中重复取了两次值,那也不会重复收集watcher。
2.改值:在值发生变更的时候,就会触发dep.notify() ,会遍历执行其dep.subs中的所有watcher.update() ,最后还是会执行到watcher.get() ,那么就执行了 _update(_render()) 把变化更新到dom上了。
patch进行 diff 优化
patch文件中 主要函数为patchVnode() 和 updateChildren() 。petch函数对虚拟dom进行比较,如果是相似节点则进入**patchVnode() 否则直接进行替换。
patchVnode() 把两个节点进行比较,进行文本更新,属性更新,子节点更新。
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) { // 子节点比较
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 清空旧节点的文本,插入新节点的内容
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
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)) {// 文本替换
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
根据上面代码可以把函数作用具体分为:
-
新老节点均有children子节点,则对子节点进行diff操作,调用
updateChildren()。 -
如果新节点有子节点,而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点。
-
如果新节点没有子节点,而老节点有子节点,则移除该节点所有的子节点。
-
当新老节点都没有子节点的时候,只是文本的替换。
updateChildren
函数在处理子节点会进行如下几步操作:
-
分别按新旧节点的子节点分为两组数组,然后在每组的头和尾安排两个指针指向头尾的节点。
-
对新旧两组节点的指针按顺序进行 6 种方式的比较,每次比较后会移动指针,6 种分别为:
-
- 当前旧节点头指针处是否有节点,没有时,头指针向右移一位。
- 当前旧节点尾指针处是否有节点,没有时,尾指针向左移一位。
- 新旧节点的头指针进行对比,匹配时保持当前DOM 结构不变,将新旧节点的头指针右移一个单位
- 新旧节点的尾指针进行对比,匹配时保持当前DOM 结构不变,将新旧节点的尾指针左一个单位
- 新节点的尾指针与旧节点的头指针进行对比。匹配时,将旧节点的头指针指向的节点的 DOM 元素插入到尾指针指向的节点的 DOM 元素之后。同时新节点尾指左移一个单位,旧节点头指针右移一个单位。
- 新节点的头指针与旧节点的尾指针进行对比。匹配时,将旧节点的尾指针指向的节点的 DOM 元素插入到头指针指向的节点的 DOM 元素之前。同时新节点头指针右移一个单位,旧节点尾指针左移一个单位。
-
-
如果 2. 中的方式的比较都没有相同的节点,则按下面三个方法来更新(执行完毕后新节点头指针向右移动一位):
-
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 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] -
- 按旧节点的子节点的
key值来生成一个hash表,每个key值对应其VNode节点,在查看当前新节点的头指针指向的VNode的key是否存在于hash表中,存在则插入到旧节点头指针所对应的元素之前,并移除这个同类型的旧节点。 - 如果没有时,就查询旧节点两个指针之间是否存在一个
VNode对象与新节点头指针执行的VNode相等,相等则插入到旧节点头指针所对应的元素之前,并移除这个同类型的旧节点。
- 按旧节点的子节点的
-
- 否则在旧节点头指针所对应的元素之前新创建一个元素。
-
-
当新旧节点的指针任意一对相交(超过)时,结束比较。
-
此时根据新旧序列谁完成的交叉判定处理新增节点还是卸载旧节点
- 旧节点完成的交叉,则说明可能新节点还有部分没有遍历完毕,需要进行新增
- 新节点完成的交叉,则说明旧节点可能还有部分没有遍历完毕,需要卸载
五、概览
图一为vue2的生命周期函数,当我们从源码的角度将其扩展一下就得出图二的流程。