1. Vue v2.6.9 源码
调试 vue 项⽬的⽅式
安装依赖:
npm i
安装打包⼯工具:
npm i rollup -g
修改 package.json ⾥里里⾯面 dev 脚本:
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
执⾏行行打包:
npm run dev
修改 samples ⾥里里⾯面的⽂文件引⽤用新⽣生成的 vue.js
vue 是如何启动的
vue 响应式机制逐⾏分析
1.1. 整理理启动顺序
platforms/web/entry-runtime-with-compiler.js:
/* new Vue({ template:dom }).$mount('#app') */ // 定义 mount:new Vue({ template:dom }).$mount('#app') const mount = Vue.prototype.$mount; // 挂载时执⾏mountComponent,将dom内容追加⾄el Vue.prototype.$mount = function(el?: string | Element, hydrating?: boolean): Component { el = el && query(el); // ... const options = this.$options; // 如果options 有 render,直接调用,如果没有 render,根据 template 生成 render 函数 // resolve template/el and convert to render function !!! if (!options.render) { let template = options.template; if (template) { // 判断 template 传入的类型做不同处理 if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template); } } else if (template.nodeType) { template = template.innerHTML; } } else if (el) { template = getOuterHTML(el); } if (template) { const { render, staticRenderFns } = compileToFunctions( template, { // ... }, this, ); options.render = render; options.staticRenderFns = staticRenderFns; } } // 调用 mount() return mount.call(this, el, hydrating); };
src\platforms\web\runtime\index.js
挂载时执⾏ mountComponent,将 dom 内容追加⾄ el
// public mount method Vue.prototype.$mount = function(el?: string | Element, hydrating?: boolean): Component { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating); // mountComponent! };
core/instance/lifecycle.js
创建组件更新函数,创建组件 watcher 实例。
// Line 172 updateComponent = () => { // ⾸先执行 vm._render() 返回VNode // 然后VNode作为参数执行update做dom更新 vm._update(vm._render(), hydrating); }; new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } }, }, true /* isRenderWatcher */, );
src\core\index.js
initGlobalAPI(Vue); // 主要做这些事 Vue.set(), Vue.delete, Vue.nextTick...
src\core\instance\index.js
function Vue(options) { this._init(options); } initMixin(Vue); // 实现上⾯面的_init这个初始化⽅方法 stateMixin(Vue); eventsMixin(Vue); lifecycleMixin(Vue); renderMixin(Vue);
initMixin(Vue):
// 重要,这里列出了执行顺序 initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props。defineReactive(vm, key, result[key]) 所以跨层注入也有响应式。 initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, 'created');
- initLifecycle: parent, children 等。parent 先于 child 创建
- initEvents: 事件监听初始化
- initInjections: 获取注⼊入数据并做响应化
- initState: 初始化 props, methods, data, computed, watch 等
- initProvide: 注⼊入数据处理理
stateMixin: 实现 watch, set, $delete
eventsMixin(Vue): 实现 emit, on..
lifecycleMixin(Vue): 实现 _update, forceUpdate, destroy
renderMixin(Vue): _render, $nextTick
1.2. 数据响应式
src\core\instance\state.js
// Line54: InitData(), 数组和对象响应化处理理逻辑 proxy(vm, `_data`, key); // 把 vue.$data.key 挂载到 vue.key 中 observe(data, true /* asRootData */);
src\core\observer\index.js
// Line 124 ob = new Observer(value) return ob /** * 对象响应化调用 defineReactive */ walk (obj: Object) {} /** * 数组元素响应化 */ observeArray (items: Array<any>) {} /** line 132 对象响应化处理理逻辑 * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() // ... let childOb = !shallow && observe(val) // 数据拦截 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // Dep watch 管理 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // ... if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
数组响应式
数组比较特别,它的操作⽅法不会触发 setter,需要特别处理 Observer 把修改过的数组拦截⽅法替换到当前数组对象上可以改变其⾏为
// line 48 if (hasProto) { //数组存在原型就覆盖其原型 protoAugment(value, arrayMethods); } else { //不不存在就直接定义拦截⽅方法 copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value);
src\core\observer\array.js
修改数组 7 个变更⽅法使其可以发送更新通知 arrayMethods
methodsToPatch.forEach(function(method) { // cache original method const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { //该⽅方法默认⾏为 const result = original.apply(this, args); //得到observer const ob = this.__ob__; let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2); break; } if (inserted) ob.observeArray(inserted); // notify change // 额外的事情是通知更新 ob.dep.notify(); return result; }); });
src\core\observer\watcher.js
// watcher和dep互相添加引⽤ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
watcher 更新逻辑: 通常情况下会执⾏ queueWatcher,执行异步更新
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
src\core\observer\scheduler.js
queueWatcher 推⼊入队列,下个刷新周期执⾏批量任务,这是 vue 异步更新实现的关键
// Line 158 queue.push(watcher); nextTick(flushSchedulerQueue);
nextTick 将 flushSchedulerQueue 加⼊入回调数组,启动 timerFunc 准备执⾏
callbacks.push(() => cb.call(ctx)); timerFunc(); // line 44
timerFunc 指定了了 vue 异步执⾏策略,根据执⾏环境,⾸选 Promise,备选依次为: MutationObserver、setImmediate、setTimeout
1.3. 虚拟 DOM
src\core\instance\render.js: 获取组件 vnode
// Line 71 const { render, _parentVnode } = vm.$options; // Line 91 vnode = render.call(vm._renderProxy, vm.$createElement);
src\core\instance\lifecycle.js: 执行 patching 算法,初始化或更新 vnode ⾄$el
// line 67 if (!prevVnode) { // initial render: 如果没有老vnode,说明在初始化 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // updates: 更新周期直接diff,返回新的dom vm.$el = vm.__patch__(prevVnode, vnode); }
src\platforms\web\runtime\patch.js: 定义组件实例补丁方法
Vue.prototype.__patch__ = inBrowser ? patch : noop;
src\core\vdom\patch.js: 创建浏览器平台特有 patch 函数,主要负责 dom 更新操作
// 扩展操作:把通⽤模块和浏览器中特有模块合并 const modules = platformModules.concat(baseModules); // 工厂函数:创建浏览器特有的patch函数,这⾥主要解决跨平台问题 export const patch: Function = createPatchFunction({ nodeOps, modules });
1.3.1. patch
那么 patch 如何⼯作的呢?
⾸先说⼀下 patch 的核⼼ diff 算法:通过同层的树节点进⾏⽐较⽽⾮对树进⾏逐层搜索遍历的⽅式,所以时间复杂度只有 O(n),是⼀种相当⾼效的算法。
同层级只做三件事:增删改。
具体规则是:new VNode 不存在就删;old VNode 不存在就增;都存在就⽐较类型,类型不同直接替换、类型相同执⾏更新;
/*createPatchFunction的返回值,⼀个patch函数*/
return function patch(oldVnode, vnode, hydrating, removeOnly) {
/*vnode不存在则删*/
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
const insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
/*oldVnode不存在则创建新节点*/
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
/*oldVnode有nodeType,说明传递进来⼀个DOM元素*/
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
/*是组件且是同⼀个节点的时候打补丁*/
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
/*传递进来oldVnode是dom元素*/
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;
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 将该dom元素清空
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(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
/*调⽤destroy钩⼦*/
invokeDestroyHook(oldVnode);
}
}
}
/*调⽤insert钩⼦*/
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};
1.3.2. patchVnode
两个 VNode 类型相同,就执⾏更新操作,包括三种类型操作:属性更新 PROPS、⽂本更新 TEXT、⼦节点更新 REORDER
patchVnode 具体规则如下:
- 如果新旧 VNode 都是静态的,同时它们的 key 相同(代表同⼀节点),并且新的 VNode 是 clone 或 者是标记了 v-once,那么只需要替换 elm 以及 componentInstance 即可。
- 新⽼节点均有 children ⼦节点,则对⼦节点进⾏ diff 操作,调⽤ updateChildren,这个 updateChildren 也是 diff 的核⼼。
- 如果⽼节点没有⼦节点⽽新节点存在⼦节点,先清空⽼节点 DOM 的⽂本内容,然后为当前 DOM 节 点加⼊⼦节点。
- 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该 DOM 节点的所有⼦节点。
- 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。
/*patch VNode节点*/
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
/*两个VNode节点相同则直接返回*/
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = (vnode.elm = oldVnode.elm);
/* 如果新旧VNode都是静态的,同时它们的key相同(代表同⼀节点), 并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染⼀次), 那么只需要替换elm以及componentInstance即可。 */
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.elm = oldVnode.elm;
vnode.componentInstance = oldVnode.componentInstance;
return;
}
/*如果存在data.hook.prepatch则要先执⾏*/
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
/*执⾏属性、事件、样式等等更新操作*/
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) {
cbs.update[i](oldVnode, vnode);
}
if (isDef((i = data.hook)) && isDef((i = i.update))) {
i(oldVnode, vnode);
}
}
/*开始判断children的各种情况*/
/*如果这个VNode节点没有text⽂本时*/
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
/*新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren*/
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
} else if (isDef(ch)) {
/*如果⽼节点没有⼦节点⽽新节点存在⼦节点,先清空elm的⽂本内容,然后为当前节点加⼊⼦ 节点*/
if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
/*当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除所有ele的⼦节点*/
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
/*当新⽼节点都⽆⼦节点的时候,只是⽂本的替换,因为这个逻辑中新节点text不存在,所以 清除ele⽂本*/ nodeOps.setTextContent(
elm,
'',
);
}
} else if (oldVnode.text !== vnode.text) {
/*当新⽼节点text不⼀样时,直接替换这段⽂本*/
nodeOps.setTextContent(elm, vnode.text);
}
/*调⽤postpatch钩⼦*/
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) {
i(oldVnode, vnode);
}
}
}
1.3.3. updateChildren
updateChildren 主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个 VNode 的 children 得出最⼩操作补丁。执⾏⼀个双循环是传统⽅式,vue 中针对 web 场景特点做了特别的算法优化。
在新⽼两组 VNode 节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。 当 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。
下⾯是遍历规则:
⾸先,oldStartVnode、oldEndVnode 与 newStartVnode、newEndVnode 两两交叉⽐较,共有 4 种⽐较 ⽅法。
当 oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 满⾜ sameVnode,直接将该 VNode 节点进⾏ patchVnode 即可,不需再遍历就完成了⼀次循环。如下图,
如果 oldStartVnode 与 newEndVnode 满⾜ sameVnode。说明 oldStartVnode 已经跑到了 oldEndVnode 后⾯去了,进⾏ patchVnode 的同时还需要将真实 DOM 节点移动到 oldEndVnode 的后⾯。
如果 oldEndVnode 与 newStartVnode 满⾜ sameVnode,说明 oldEndVnode 跑到了 oldStartVnode 的前 ⾯,进⾏ patchVnode 的同时要将 oldEndVnode 对应 DOM 移动到 oldStartVnode 对应 DOM 的前⾯。
如果以上情况均不符合,则在 old VNode 中找与 newStartVnode 满⾜ sameVnode 的 vnodeToMove,若 存在执⾏ patchVnode,同时将 vnodeToMove 对应 DOM 移动到 oldStartVnode 对应的 DOM 的前⾯。
当然也有可能 newStartVnode 在 old VNode 节点中找不到⼀致的 key,或者是即便 key 相同却不是 sameVnode,这个时候会调⽤ createElm 创建⼀个新的 DOM 节点。
⾄此循环结束,但是我们还需要处理剩下的节点。
当结束时 oldStartIdx > oldEndIdx,这个时候旧的 VNode 节点已经遍历完了,但是新的节点还没有。说 明了新的 VNode 节点实际上⽐⽼的 VNode 节点多,需要将剩下的 VNode 对应的 DOM 插⼊到真实 DOM 中,此时调⽤ addVnodes(批量调⽤ createElm 接⼝)。
但是,当结束时 newStartIdx > newEndIdx 时,说明新的 VNode 节点已经遍历完了,但是⽼的节点还有 剩余,需要从⽂档中删 的节点删除。
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, elmToMove, refElm;
// 确保移除元素在过度动画过程中待在正确的相对位置,仅⽤于<transition-group> const canMove = !removeOnly
// 循环条件:任意起始索引超过结束索引就结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left } else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
/*分别⽐较oldCh以及newCh的两头节点4种情况,判定为同⼀个VNode,则直接patchVnode即可*/
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
canMove &&
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
/* ⽣成⼀个哈希表,key是旧VNode的key,值是该VNode在旧VNode中索引 */
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
/*如果newStartVnode存在key并且这个key在oldVnode中能找到则返回这个节点的索引*/
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;
if (isUndef(idxInOld)) {
/*没有key或者是该key没有在⽼节点中找到则创建⼀个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
} else {
/*获取同key的⽼节点*/
elmToMove = oldCh[idxInOld];
if (sameVnode(elmToMove, newStartVnode)) {
/*如果新VNode与得到的有相同key的节点是同⼀个VNode则进⾏patchVnode*/
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
/*因为已经patchVnode进去了,所以将这个⽼节点赋值undefined,之后如果还有新节 点与该节点key相同可以检测出来提示已有重复的key*/
oldCh[idxInOld] = undefined;
/*当有标识位canMove实可以直接插⼊oldStartVnode对应的真实DOM节点前⾯*/
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
} else {
/*当新的VNode与找到的同样key的VNode不是sameVNode的时候(⽐如说tag不⼀样或 者是有不⼀样type的input标签),创建⼀个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
}
}
}
}
if (oldStartIdx > oldEndIdx) {
/*全部⽐较完成以后,发现oldStartIdx > oldEndIdx的话,说明⽼节点已经遍历完了,新节 点⽐⽼节点多,所以这时候多出来的新节点需要⼀个⼀个创建出来加⼊到真实DOM中*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
/*如果全部⽐较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,⽼节 点多余新节点,这个时候需要将多余的⽼节点从真实DOM中移除*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
1.3.4. 属性更新如何实现的
//patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'];
export function createPatchFunction(backend) {
// 传递进来的扩展模块和节点操作对象
const { modules, nodeOps } = backend;
for (i = 0; i < hooks.length; ++i) {
// cbs['update'] = []
cbs[hooks[i]] = [];
//modules: [ attrs, klass, events, domProps, style, transition]
for (j = 0; j < modules.length; ++j) {
// modules[0]['update'] 是创建属性执⾏函数,其他hook以此类推
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
// cbs['update']: [fn,fn,fn....]
}
function patchVnode(args) {
if (isDef(data) && isPatchable(vnode)) {
// 每次patch时先对属性做更新
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
}
}
1.4. 模板编译
模板编译的主要⽬标是将模板(template)转换为渲染函数(render)
Vue 2.0 需要⽤到 VNode 描述视图以及各种交互,⼿写显然不切实际,因此⽤户只需编写类似 HTML 代码 的 Vue 模板,通过编译器将模板转换为可返回 VNode 的 render 函数。
1.4.1. 体验模板编译
带编译器的版本中,可以使⽤ template 或 el 的⽅式声明模板
<div id="demo">
<h1>Vue.js测试</h1>
<p>{{foo}}</p>
</div>
<script>
// 使⽤el⽅式
new Vue({ data: { foo: 'foo' }, el: '#demo' });
</script>
<script>
const app = new Vue({});
// 输出render函数
console.log(app.$options.render);
</script>
输出结果⼤致如下:
function anonymous() {
with (this) {
return _c('div', { attrs: { id: 'demo' } }, [
_c('h1', [_v('Vue.js测试')]),
_v(' '),
_c('p', [_v(_s(foo))]),
]);
}
}
- 元素节点使⽤ createElement 创建,别名_c
- 本⽂节点使⽤ createTextVNode 创建,别名_v
- 表达式先使⽤ toString 格式化,别名_s
1.4.2. 模板编译过程
实现模板编译共有三个阶段:解析、优化和⽣成
1. 解析 - parse
解析器将模板解析为抽象语法树 AST,只有将模板解析成 AST 后,才能基于它做优化或者⽣成代码字符 串。 调试查看得到的 AST,/src/compiler/parser/index.js,结构如下:
解析器内部分了 HTML 解析器、⽂本解析器和过滤器解析器,最主要是 HTML 解析器,核⼼算法说明:
//src/compiler/parser/index.js
parseHTML(template, {
start(tag, attrs, unary) {}, // 遇到开始标签的处理
end() {}, // 遇到结束标签的处理
chars(text) {}, // 遇到⽂本标签的处理
comment(text) {}, // 遇到注释标签的处理
});
2. 优化 - optimize
优化器的作⽤是在 AST 中找出静态⼦树并打上标记。静态⼦树是在 AST 中永远不变的节点,如纯⽂本节 点。 标记静态⼦树的好处:
- 每次重新渲染,不需要为静态⼦树创建新节点
- 虚拟 DOM 中 patch 时,可以跳过静态⼦树
代码实现,src/compiler/optimizer.js - optimize
export function optimize(root: ?ASTElement, options: CompilerOptions) {
if (!root) return (isStaticKey = genStaticKeysCached(options.staticKeys || ''));
isPlatformReservedTag = options.isReservedTag || no; // 找出静态节点并标记
markStatic(root); // 找出静态根节点并标记
markStaticRoots(root, false);
}
标记结束
1.4.3. 3. 代码⽣成 - generate
将 AST 转换成渲染函数中的内容,即代码字符串。 generate ⽅法⽣成渲染函数代码,src/compiler/codegen/index.js
export function generate(ast: ASTElement | void, options: CompilerOptions): CodegenResult {
const state = new CodegenState(options);
const code = ast ? genElement(ast, state) : '_c("div")';
return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns };
}
//⽣成的code⻓这样
`_c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("Vue.js测试")]),
_c('p',[_v(_s(foo))])
])`;
1.4.4. v-if、v-for 学习
着重观察⼏个结构性指令的解析过程
// 解析v-if,parser/index.js
function processIf(el) {
const exp = getAndRemoveAttr(el, 'v-if');
// 获取v-if=“exp"中exp并删除v-if属性
if (exp) {
el.if = exp; // 为ast添加if表示条件
addIfCondition(el, {
// 为ast添加ifConditions表示各种情况对应结果
exp: exp,
block: el,
});
} else {
// 其他情况处理
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true;
}
const elseif = getAndRemoveAttr(el, 'v-else-if');
if (elseif) {
el.elseif = elseif;
}
}
}
// 代码⽣成,codegen/index.js
function genIfConditions(
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string,
): string {
const condition = conditions.shift();
// 每次处理⼀个条件
if (condition.exp) {
// 每种条件⽣成⼀个三元表达式
return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(
conditions,
state,
altGen,
altEmpty,
)}`;
} else {
return `${genTernaryExp(condition.block)}`;
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp(el) {}
}
解析结果:
⽣成结果:
`with(this){return _c('div',{attrs:{"id":"demo"}},[
(foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]), _v(" "),_c('abc')],1)}`;
1.4.5. 插槽
组件编译的顺序是先编译⽗组件,再编译⼦组件。
普通插槽是在⽗组件编译和渲染阶段⽣成 vnodes ,数据的作⽤域是⽗组件,⼦组件渲染的时候直接拿 到这些渲染好的 vnodes 。
作⽤域插槽,⽗组件在编译和渲染阶段并不会直接⽣成 vnodes ,⽽是在⽗节点保留⼀个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染⼦组件阶段才会执⾏这个渲染函数⽣成 vnodes ,由于是在⼦组件环境执⾏的,所以对应的数据作⽤域是⼦组件实 例。
解析相关代码:
// processSlotContent:处理<template v-slot:xxx="yyy">
const slotBinding = getAndRemoveAttrByRegex(el, slotRE);
if (slotBinding) {
// 查找v-slot:xxx
const { name, dynamic } = getSlotName(slotBinding); // name是xxx
el.slotTarget = name; // xxx赋值到slotTarget
el.slotTargetDynamic = dynamic;
el.slotScope = slotBinding.value || emptySlotScopeToken; // yyy赋值到 slotScope
}
// processSlotOutlet:处理<slot>
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name');
// 获取slot的name并赋值到slotName
}
⽣成相关代码:
// genScopedSlot:这⾥把slotScope作为形参转换为⼯⼚函数返回内容
const fn =
`function(${slotScope}){` +
`return ${
el.tag === 'template'
? el.if && isLegacySyntax
? `(${el.if})?${genChildren(el, state) || 'undefined'}:undefined`
: genChildren(el, state) || 'undefined'
: genElement(el, state)
}}`;
// reverse proxy v-slot without scope on this.$slots
const reverseProxy = slotScope ? `` : `,proxy:true`;
return `{key:${el.slotTarget || `"default"`},fn:${fn}${reverseProxy}}`;