杂项
- 对
array的相关操作都是触发的getter函数
obj.list[1]/obj.list[1]=1/obj.list.length/obj.list.length=0
with语句的作用是将代码的作用域设置到一个特定的对象中
程序执行步骤
- 初始化流程
- new Vue() -> this._init() -> initState(vm) -> observe(data) 响应式 -> vm.$mount() -> mountComponent()
- mountComponent -> new Watcher(vm, updateComponent ) 重要一步,Watcher 初始化时执行 this.get() 会触发 updateComponent()
- updateComponent() -> vm._update( vm._render() ) 之后每次更新都会触发这个函数
- vm._render -> render: ( h ) => h( App ) -> h = createElement 用传入的参数创建 vnode 节点(重要)并返回
- vm._update -> vm.patch(vm.$el, vnode 上面返回的 ) = patch(oldVnode, vnode) 重要步骤
- 里面 vm._vnode = vnode 这时保存就是之后更新用到的
oldVnode重要步骤
- patch -> createElm 创建真实节点并插入 DOM(首次渲染:先把子节点挨个插入到自己的父节点中,最后整体插入)
- 重要:如果不是初始化渲染会执行
sameVnode()->patchVnode(oldVnode, vnode)->updateChildren()执行 diff 算法
- createElm 函数
- 用于按照 vnode 类型创建不同的真实 DOM 并插入到对应的位置。可能是子节点插入到父节点中,或是父节点插入到 DOM 中
- 初次渲染时 patch 执行的是 createElem 而不是 patchVnode
- 依赖收集
- new Watcher() -> pushTarget() = Dep.target = watcher -> this.getter() -> updateComponent() -> vm._render() 触发依赖收集 -> popTarget() = Dep.target = undefined
- vm._render() = render(h) => h(App) 而内部会用到
this.xxx这里读取某个属性,进而触发响应式的 getter -> dep.depend() 完成依赖收集。重要步骤
- dep.depend() -> Dep.target.addDep() -> dep.addSub(watcher)
- 重要说明:
- 实际开发使用的
<template>会进过编译器转为类似渲染函数内的h(应该可叫做虚拟节点树)的内容,这时已经包含了页面内的所有的代码内容(包括v-show/v-if/compontent等没展示的内容)。
- 后续在执行
render函数时,就会触发程序内写的this.xxx进而触发了getter完成了响应式的收集工作。所以响应式的收集是在render函数执行时,而不是实际插入到DOM时
- 除开用户创建的
computed/watche的Watcher函数外,实际全局用于渲染的Watcher函数就一个(mountComponent函数内的那个)
- 视图更新
- 响应式
setter函数 -> dep.notify() -> wather.update() -> queueWatcher() -> nextTick(flushSchedulerQueue) 优化处理
- nextTick(flushSchedulerQueue) 这里导致了为什么说 this.xxx 是异步而非同步的。主要为了多次更改 this.xxx 时让视图尽量少更新
- flushSchedulerQueue -> watcher.run() -> watcher 内的 this.get() -> vm._update( vm._render() ) 又进行 render 和 update -> patch 视图更新
- vm._render() 更新时都会执行,所以
vnode也每次都会创建一个既newVnode
- 然后vm._update时可用之前保存的
oldVnode和newVnode进行比较。diff 算法的依据
- vm._update() -> vm.__patch() 是走更新的那个 -> patch -> patchVnode
- 所有的DOM视图更新动作,都是在比较判断时同步进行的。通过
nodeOps对象下封装的一系列DOM操作函数
- patch 每次都是从VDOM树的根开始按照
同层比较以及子层diff算法进行
- 同层走的 patchVnode 子层走的 updateChildren。然后 updateChildren 内又走 patchVnode 这么个逻辑
- 生命周期触发
- this._init() 内触发 callHook(vm, 'beforeCreate')/callHook(vm, 'created') 中间有个
initState()所以在created内就可以使用this.xxx了
- mountComponent() 执行 callHook(vm, 'beforeMount')/callHook(vm, 'mounted')
- flushSchedulerQueue() -> watcher.before() 内执行 callHook(vm, 'beforeUpdate') 同时执行 callUpdatedHooks() -> callHook(vm, 'updated')
- Vue.prototype.$destroy 执行 callHook(vm, 'beforeDestroy')/callHook(vm, 'destroyed')
虚拟Dom
介绍说明
- vnode本身是个js对象,它是对节点的描述。而VDOM是多个vnode组成的树形结构
- 用于当某个状态发生改变时,只更新与这个状态相关联的DOM节点
- 在 vue 中虚拟DOM描述了从vnode进行patch进而渲染到视图的过程
- 将对真实Dom的操作转移到VDOM上,即js对象上这使的操作更快速和高效。同时因为并不是直接操作真实DOM所以也提供了跨平台的能力
VNode
- 在vue中存在一个VNode类,使用它可以实例化不同类型的vnode实例。简单地说,vnode可以理解成节点描述对象
- 可以对上次渲染视图所创建的vnode进行缓存。之后在需要更新视图时,将新创建的vnode和上次缓存的进行比较,找出不一样的地方并基于此去修改真实DOM
- VNode的几种类型:注释节点、文本节点、元素节点、组件节点、函数式节点、克隆节点
- 主要的vnode创建函数:new VNode()、createEmptyVNode()、createTextVNode()、cloneVNode()
patch
- 虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的DOM。patch是及对两次vnode节点进行比较,找出不同点然后对现有DOM进行修改。所有的判断处理都是以新的vnode为准
- 创建新增节点
- 只有三种类型的节点会被创建并插入到DOM中:元素节点、注释节点和文本节点(纯文本)这个说法对应着
createElm方法
- 删除已经废弃的节点
- 是将新创建的DOM节点插入到旧节点的前,然后再将旧节点删除,从而完成替换过程(为了知道添加的位置)
- 修改需要更新的节点
- 更新节点时,会判断新旧两个节点是否是静态节点(无状态的)。是的话就跳过不处理
- 更新子节点分为4类操作(遍历方式):更新节点、新增节点、删除节点、移动节点位置
- 新增:将新创建的节点插入到oldChildren中所有未处理节点的前面
- 移动:在oldChildren中找到相同的但是位置不同。移动的位置是oldChildren所有未处理节点的前面
- 更新:两个节点是同一个节点并且位置相同
- 删除(遍历结束后):当newChildren中节点都循环一遍,而oldChildren中还有剩余就需要删除
- 重要:因为oldChildren对应着真实DOM的结构,所以所有判断是以newOld为准,但操作以oldChildren为基础(如:新增、移动)。
- 优化策略 diff 算法
- 因为只做同层比较不深度遍历,所以时间复杂度为O(n)
- 先按照4种查找方式进行比较,如果失败再依据newVnode对oldChildren进行循环便利
- newStart-oldStart、newEnd-oldEnd、newEnd-oldStart、newStart-oldEnd
- 循环时设置新旧头尾双指针,从两头向中间循环。从而获取未被处理的节点
- 当newChildren先循环完,说明oldChildren需要删除反之需要新增
- 重要:
- diff 算法是先同层比较相同
sameVnode->patchVnode->updateChildren,在进入下一层(子节点)然后按照4种方式进行优化比较。如果1种命中就再执行patchVnode->updateChildren进入下一层,没有就按照key进行遍历判断
模版编译原理
模版编译
- 主要目标就是生成渲染函数,而渲染函数的作用就是每次执行它,就会使用当前最新的状态生成一份vnode,然后使用这个vnode进行渲染
- 模版编译大体分为三个部分:将模版解析为AST、遍历AST标记静态节点、使用AST生成渲染函数
- 三个部分分别对应:解析器、优化器、代码生成器
- 静态节点由于无状态,除了初始化时需要渲染。之后的直接复用就好了
解析器
- 主要实现将模版解析为AST,主要有HTML解析器、文本解析器、过滤器解析器
- 文本/过滤器解析器都在HTML解析器的钩子函数中执行
- HTML解析器会在解析到不同内容时触发响应配置的钩子函数。如:start/end/chars/comment 等钩子函数
- start钩子:创建AST、处理AST(指令)、管理AST(维护父子结构)
- end钩子:管理AST(维护父子结构)
- AST层级结构:我们只需要维护一个栈(stack)即可,用栈来记录层级关系,这个层级关系也可以理解为DOM的深度
- 在每次触发钩子函数start时,把当前构建的节点推入栈中;每当触发钩子函数end时,就从栈中弹出一个节点
- 这样就可以保证每当触发钩子函数start时,栈的最后一个节点就是当前正在构建的节点的父节点
- 解析过程是循环HTML模版字符串内容,每次循环从中截取一小段字符串,然后重复以上步骤直到模版内容截取额结束(本质是通过正则语法对模版字符串进行匹配的过程)
- 开始标签分为三部分进行匹配处理,为标签名(startTagOpen)、属性(attribute)、结尾(startTagClose)
- 注意: 解析器生成的结果是一个
AST树结构的

优化器
- 作用是在AST中找出静态子树并打上标记。主要以下两点好处
- 每次重新渲染时,不需要为静态子树创建新节点
- 在虚拟DOM中打补丁(patching)的过程可以跳过
- 主要分类两个步骤。主要是通过节点类型
type判断,分为1、2、3几种类型
- 在AST中找出所有静态节点并打上标记(marktatic -> isStatic)
- 在AST中找出所有**静态根节点(必须有子节点)**并打上标记(markStaticRoots)
- 查找逻辑:找到的第一个有子节点的静态节点,并且它不只有一个静态文本子节点的既是静态根节点
- 注意:是两次处理逻辑
- type类型说明(是在 parseHTML 的回调函数内赋值的)
- 1 - 元素节点
- 2 - 带变量的动态文本节点
- 3 - 不带变量的纯文本节点
代码生成器
- 作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。生成过程是一个递归的过程,从AST顶依次向下处理每一个节点。
- 对应插入DOM的3种节点类型:_c(标签)、_v(文本)、_e(注释)
- 生成元素节点,其实就是生成一个_c的函数调用字符串。主要包含:genElement、genData、genChildren
- genElement按照节点类型调用不同的生成函数
- genStatic会把所有静态节点都保存到
staticRenderFns数组内
程序执行步骤
- Vue.prototype.$mount -> const { render, staticRenderFns } = compileToFunctions(template) = createCompiler().compileToFunctions = createCompileToFunctionFn()
- createCompiler() -> const compiled = baseCompile(template) -> baseCompile() 重要步骤:执行上面的三个逻辑
- createCompileToFunctionFn() -> createFunction()重要步骤:让生成的render代码字符串变为可执行函数
- 注意:这里的柯里化处理逻辑
patchVnode/updateChildren内的diff比较
- 这里说的是当根节点
oldVnode/newVnode.tag相同时。然后同层走的 patchVnode 子层走的 updateChildren
- 所有的比较都是依据
newVnode进行的,而操作都是通过oldVnode进行的。因为只有oldVnode.elm是存在的,并且优化了节点的使用。VDOM使用的核心思想
sameVnode
- 判断
key/tag/isComment是否相同,判断data是否都定义了
updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
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)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
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)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
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 {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
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)
}
}
批量异步更新 nextTick 原理
- 数据触发视图更新流程
setter -> Dep.notify -> Watcher -> patch -> 视图
- 让数据更新和视图更新脱离。实际数据更新是同步的,而视图更新是个异步的过程
- 按照上面的流程实际更新视图步骤在
watcher.update() -> watcher.run()中,并且由于 vue 对视图更新有批量优化处理。让视图进去异步队列在下一次 tick 时调用
- 由于同一个数据的
setter可能多次触发,(因为优化处理)对应的watcher回被重复放入队列。为了防止无效触发在 Watcher 创建时定义了本身的id,并在进入队列时判断全局map[id]方式防止重复,然后在队列执行完后释放
- 注意: 下面有两个队列各自有自己的锁
flag机制,确保了view/this.$nextTick()在本轮事件循环内都只用一个延时机制
- nextTick 原理
- 按照优化逻辑需要在下次 tick 时执行,为了让
data数据先更新完。那就需要实现一个延迟机制 而js内延迟的方式就是让任务在下次事件循环时在执行
let callbacks = [];
let pending = false;
function nextTick (cb) {
callbacks.push(cb);
if (!pending) {
pending = true;
setTimeout(flushCallbacks, 0);
}
}
function flushCallbacks () {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
- 批量异步更新 Watcher 处理
- 由于通过优化处理让数据/视图更新进行了脱离,但实际每次
setter还是多次会触发同个Watcher的进入队列这是没必要的。所有就要有个记录确保同一个Watcher在本次循环时只进入一次,那就给每个Watcher设定唯一id用于区分
- flushSchedulerQueue 源码内有个从小到大的排序处理
- 组件的更新由父到子。因为父组件创建早于子组件,所以对应的
Watcher也同样是这样的顺序
- 用户的自定义
watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的
- 如果一个组件在父组件的
watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行
let uid = 0;
class Watcher {
constructor() {
this.id = ++id;
}
update() {
queueWatcher(this)
}
run() {
}
}
let has = {};
let queue = [];
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
watcher.run();
}
waiting = false;
}