首先我们要读的是vue2.6版本的源码,因为2.7版本和vue2已经有些区别了,我们用vue2普遍用的还是2.6的版本。
前置知识
rollup
无论是 vue 还是 react 框架都用了 rollup 作为构建工具,而不是webpack,为什么呢?
rollup vs webpack
模块化规范不同
rollup 是 ESmodule 规范下的构建工具,而 webpack 使用 CommonJS 规范。
ESmodule 更好的支持 Tree-Shaking, 因为它不需要导入整个工具,支持解构。
const utils = require('./utils') // CommonJS
import { ajax } from './utils' // ESmodule
ESmodule 是异步加载模块,可以避免加载进程的阻塞。
CommonJS 是同步加载模块,需要等待模块加载完成再继续往下执行。
因此 rollup 能构建出体积更小、速度更快的单文件,更受类库开发的青睐。
HMR和code-splitting
webpack 支持 HMR 和 code-splitting,因此更适合作为应用程序的打包构建工具
生态
webpack 的插件和 loader 众多,扩展性更强。
rollup 都用插件进行文件的处理,例如下面要说的 terser 等。
flow
Vue2 使用 flow 作为类型校验工具,因为当时2018以前 typescript 生态并不成熟,尤雨溪大大说它是资本控制的产品。而且当时 eslint 和 babel 都有对应的 flow 插件。当然,2018年以后就真香了,因此 Vue3 用 typescript 重构了。
flow 相关的配置在 .flowconfig文件中。
阅读思路
package.json
看 package.json,其中 files 告诉我们重点应该去看 src, dist 以及 types 目录
"files": [
"src",
"dist/*.js",
"types/*.d.ts"
],
src
-
compiler 负责编译转换 AST
-
core 集成了核心能力
-
platform 分 web 端和 weex 端的能力,目前我们只关注 web 端
-
server 集成了 ssr 的能力
-
sfc 是解析 .vue 文件的过程
-
shared 提供通用的能力
scripts
package.json 当中的 scripts 具有 dev, build 等能力,我们要去看的就是从 build 开始我们的 Vue 做了什么,所以我们首先去看scripts
下的build
。
build
// 如果dist目录不存在就创建dist
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist')
}
这里就可以看到我们最终的产物是放到dist
里面的。
let builds = require('./config').getAllBuilds() // 处理所有的配置项
我们就要去看config
中的配置项
config
builds对象
config
当中主要关注builds
对象,它包含的就是所有的参数以及参数对应的配置项,这些配置项可以去rollup官网去看。下面举一个例子。
const builds = {
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'), // 入口
dest: resolve('dist/vue.runtime.common.dev.js'), // 打包产物
format: 'cjs', // 格式
env: 'development', // 环境
banner // 文案
}, ...
}
接着去看alias
和resolve
函数
const aliases = require('./alias') // alias就是参数
const resolve = p => {
const base = p.split('/')[0] // /前面的参数
// aliases[base]就是alias.js文件中base对应的路径
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
这里的resolve
是将/
前的能力去掉,找到src
下对应的能力。
以上面的web-runtime-cjs-dev
为例,resolve('web/entry-runtime.js')
它将前面的web去掉,在alias
中找到了web: resolve('src/platforms/web')
,因此确定web-runtime-cjs-dev
的能力集成在src/platforms/web
的目录下。
alias
alias
主要就是将builds的参数与他们的能力相对应的src
中的文件联系起来。
注意这里的resolve
函数是path.resolve
// 每个模块对应的入口文件
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
genconfig
接下去是一个genconfig
函数,它是提供给build
文件中的buildEntry
用的,这部分我们只关注生成config
的能力,忽略环境和weex
的内容。
function genConfig (name) {
const opts = builds[name] // 根据传入的name获取builds中对应的配置对象
const config = {
input: opts.entry, // 入口
external: opts.external, // 需要排除的外部依赖
plugins: [
flow(), // 用flow做类型保护
alias(Object.assign({}, aliases, opts.alias)) // 收集所有的参数
].concat(opts.plugins || []),
// 出口
output: {
file: opts.dest, // 目标文件
format: opts.format, // 格式化类型
banner: opts.banner, // 文案
name: opts.moduleName || 'Vue'
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
}
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
return config // 这样就生成了方便rollup去解析的config对象
}
这块也建议去跟着rollup官网的配置项走一遍。
getAllBuilds
最后就是对builds
每个对象进行genconfig
处理并返回,注意返回的是数组。
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig) // 对builds中所有的配置项进行处理
}
过滤builds数组
回到build
当中,接下来根据npm run build
接受的参数对config
返回的builds
进行过滤,还是只关注 web 端。
if (process.argv[2]) {
const filters = process.argv[2].split(',') // 多个参数用逗号隔开
// 从builds中去找有参数对应的出口文件的
// 也就是说过滤出参数对应的配置
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
}
执行rollup
接着执行build
,递归的使用next
函数调用buildEntry
。
为什么这样设计呢?因为rollup
构建完成返回的是一个Promise
build(builds)
// 这里是rollup的执行过程
function build (builds) {
let built = 0
const total = builds.length
// 递归地去调用构建函数
const next = () => {
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
}
next()
}
buildEntry
这里的功能就是使用rollup
去打包构建,其中isProd
判断是否是生产环境,并使用terser进行压缩。
function buildEntry (config) {
const output = config.output
const { file, banner } = output // 解构出打包产物和文案
const isProd = /(min|prod)\.js$/.test(file) // 判断生产环境
return rollup.rollup(config) // 返回一个Promise
.then(bundle => bundle.generate(output))
.then(({ output: [{ code }] }) => {
// 生产环境需要用terser进行代码压缩
if (isProd) {
const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
toplevel: true,
output: {
ascii_only: true
},
compress: {
pure_funcs: ['makeMap'] // 丢弃纯函数
}
}).code
return write(file, minified, true)
} else {
return write(file, code)
}
})
}
其中,toplevel
的官网示例如下,判断顶层变量是否进行转换
var code = {
"file1.js": "function add(first, second) { return first + second; }",
"file2.js": "console.log(add(1 + 2, 3 + 4));"
};
var options = { toplevel: true };
var result = await minify(code, options);
console.log(result.code);
// console.log(3+7);
ascii_only: true
转义字符串和正则表达式中的Unicode字符(影响带有非ascii字符的指令无效)
write
最后要去写文件,通过调用gzip
来对代码进行压缩,然后通过fs
去写文件,最终告诉你文件的大小。
function write (dest, code, zip) {
return new Promise((resolve, reject) => {
function report (extra) {
console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
resolve()
}
// 通过fs写文件
fs.writeFile(dest, code, err => {
if (err) return reject(err)
// 用gzip压缩
if (zip) {
zlib.gzip(code, (err, zipped) => {
if (err) return reject(err)
report(' (gzipped: ' + getSize(zipped) + ')')
})
} else {
report()
}
})
})
}
// 压缩后的大小
function getSize (code) {
return (code.length / 1024).toFixed(2) + 'kb'
}
入口配置文件
在build
当中我们提到我们获取到了参数对应的入口配置文件,我们接下来去看入口配置文件的内容。
在 Vue2 中,我们分成 runtime 和 runtime+compiler 两种模式,区别在于 runtime 需要额外的 loader 对 .vue 文件进行处理,而 runtime+compiler 可以直接编译,因此我们去看runtime+compiler。
根据config
中的builds
对象,我们可以找到入口文件是位于src/platform/web
目录下的entry-runtime-with-compiler.js
entry-runtime-with-compiler.js
可以看到很多针对 Vue 原型上的操作,所以我们首先要找到 Vue 的入口,也就是 Vue 是在哪创建的。
// entry-runtime-with-compiler.js
import Vue from './runtime/index' // 去找Vue的入口
// runtime/index.js
import Vue from 'core/index' // 还不是,继续找
// core/index.js
import Vue from './instance/index' // 还不是,继续找
最终在core/instance/index
中找到了 Vue 的入口文件。
那么为什么不在一个文件夹里,能通过core/*
映射出来呢,原因是我们的 .flowconfig 中提供了映射:
module.name_mapper='^core/\(.*\)$' -> '<PROJECT_ROOT>/src/core/\1'
core/index
然后我们顺着我们找到 Vue 入口的这条路,倒回去看每一步都做了什么。
core/index
主要做的就是初始化API,以及创建 ssr 环境。
initGlobalAPI(Vue)
// 创建ssr环境
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
global-api
可以参考官网全局API,我们同样忽略开发环境相关内容
export function initGlobalAPI (Vue: GlobalAPI) {
// 在vue.config中修改的是这里的对象
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 这里可以看到我们的$set和$nextTick都是在这里初始化的
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 新增 observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null) // 就是component、directive和filter
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
其中,ASSET_TYPES
指的是:
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
Vue 入口文件
入口文件代码很短,也都是重点,我直接贴出来。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// Vue的构造函数
// 为什么用函数不用class? 因为Vue2中代码是通过mixin去集成的
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue) // 限制Vue只能用new关键字创建
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 所有的mixin,都是针对于Vue构造函数的操作
initMixin(Vue) // 初始化
stateMixin(Vue) // 状态
eventsMixin(Vue) // 事件
lifecycleMixin(Vue) // 生命周期
renderMixin(Vue) // 渲染
export default Vue
我们可以看到 Vue 的方法是通过 mixin 的方式引入的,不过这里的 mixin 和我们使用的 mixin 不一样。
同时这里也解释了为什么要用new Vue()
来创建,因为 Vue 本质上就是一个构造函数。
那么为什么不用 class 而用 function 呢?原因就在于 Vue2 通过 mixin,在 prototype 上集成了 Vue 的能力。
那么new Vue()
这一步做了什么呢?也就是去看this._init(options)
,此时this
指向new Vue()
创建出来的实例。吐槽一下 mixin 注入的方法,按ctrl+左键没法跳转,只能去找。
Vue.prototype._init
init方法做了以下操作:
-
通过
uid
标记Vue实例 -
进行内部组件初始化
-
合并
options
-
依次进行生命周期、事件、渲染的初始化
-
调用生命周期 hooks 函数
-
执行渲染。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++ // 通用的id,判断是创建的第几个Vue实例
// 防止Vue实例被监听
vm._isVue = true
// 合并options
if (options && options._isComponent) {
initInternalComponent(vm, options) // 内部组件初始化
} else {
// 如果是一个元素,就合并其中的options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
// 这里就是生命周期相关操作
// 针对Vue实例添加属性和方法
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
// 以下就是beforeCreate和created之间的差别
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 执行渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
mergeOptions
递归去执行options
的合并。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 如果传进来的是构造函数的形式,就去找options
if (typeof child === 'function') {
child = child.options
}
// 只合并原始options对象,而不是合并后的结果
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
init
init
的过程其实就是在实例vm
上添加了各种属性。
callHook
export function callHook (vm: Component, hook: string) {
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook) // 通过全局emit暴露出对应的方法
}
popTarget()
}
beforeCreate -> created
这两个生命周期就差了当中这三步,也就是created
能通过inject
和provide
获取到data
和props
。
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
Vue.prototype.$mount
接下去就是渲染的过程了,这个方法在entry-runtime-with-compiler.js
中,我们拆分来看,他接受el
参数,他可以是字符串,或是对象,通过query
去查找对应的元素,并且不能将Vue挂载到html
或者body
上。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
}
query
可以看到query
是通过document.querySelector
方法实现的。
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
判断render
接下去是一个判断,判断options
是否没有render
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
...
}
那么什么叫有没有render
?
// 没有render
new Vue({
template: '<div>{{ helloworld }}</div>'
})
// 有render
new Vue({
render (h) {
return h('div', this.helloworld)
}
})
所以我们 runtime+compiler 要处理的就是没有render
,而是template
的情况。
template处理
如果template
是一个字符串
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
}
idToTemplate
用来通过 id 获取真实的 DOM 元素。
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
如果template是一个对象,其中有nodeType属性
else if (template.nodeType) {
// 用innerHTML的方式获取template
template = template.innerHTML
}
如果是el
else if (el) {
template = getOuterHTML(el) // 处理是el属性的情况
}
compiler
接下去是compiler
的能力。
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
这块要进入到compiler
的入口去看。
const { compile, compileToFunctions } = createCompiler(baseOptions)
最终在src/compiler/index.js
中找到了入口,可以看到这里就是AST解析的过程。
其中,parse
函数将HTML
转换成AST
树。
optimize
函数将AST树中不需要更改的DOM部分直接转为常量,无需重复创建节点。
generate
避免script
标签被渲染。
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
最后调用mount
,注意是运行时态的mount
,而不是他自己调用自己。
return mount.call(this, el, hydrating)
runtime
mount
运行时的mount
主要去看mountComponent
函数
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent
这个函数很长,它接受vm
和el
作为参数。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el;
...
}
首先判断是否有render
,因为这里已经是运行时了,需要检查是否需要compiler。
if (!vm.$options.render) {
// 如果没有render,就要执行compiler的过程,将template或el转换成VNode
vm.$options.render = createEmptyVNode
}
其次去调用beforeMount
钩子
callHook(vm, 'beforeMount')
然后添加Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
Watcher
Watcher
用的是设计模式中的观察者模式的思想,Watcher 在这里起到两个作用(后面会详细讲解)
-
初始化的时候会执行回调函数;
-
当 vm 实例中的监测的数据发生变化的时候执行回调函数;
mounted
接下去就是mounted
hooks
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
beforeMount -> mounted
beforeMount
和mounted
之间做了哪些操作呢?
-
vm._update(vm._render(), hydrating)
-
new Watcher()
也就是说,通过mount
,我们的数据的变化可以被监听到,并且通过_render
生成了vnode
,通过_update
更新了DOM。
_render
_render
的核心是vnode = render.call(vm._renderProxy, vm.$createElement)
,而createElement
返回了_createElement
的结果所以重点要去看_createElement
方法。
return _createElement(context, tag, data, children, normalizationType)
首先要了解VNode
的概念
VNode
具体的含义我写在注释中了,这里只列出重点。
export default class VNode {
tag: string | void; // 标签
data: VNodeData | void; // VNode所包含的数据
children: ?Array<VNode>; // 子节点
text: string | void; // 文本内容
elm: Node | void; // 用来直接操作DOM的节点
context: Component | void; // 当前VNode所在的上下文环境
key: string | number | void;
componentOptions: VNodeComponentOptions | void; // 组件options
componentInstance: Component | void; // 组件实例
parent: VNode | void; // 父节点
raw: boolean; // 包含原生HTML
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.context = context
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
}
}
_createElement
核心一共两部分,
-
创建VNode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
-
返回VNode
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
_update
这是最后一步,可以看到这里最关键的就是__patch__
方法,对应的是diff算法的入口。
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
// 这两行就表示,如果没有对应的DOM,就去创建,如果有就去更新
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
patch
同样忽略环境和ssr,比较新旧节点的方法如下:
如果新vnode为空,那么就销毁旧的vnode。
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
如果旧的vnode为空,就创建vnode。
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
}
如果都不为空,就再分情况讨论:
如果是相同的节点,就进行复用
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
相同节点指的是:key,标签相同
function sameVnode (a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
如果是真实DOM,就根据旧节点创建VNode
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
找到oldVnode
的虚拟DOM的父元素,将父元素中的oldElm
替代为新的节点。
return function patch (oldVnode, vnode, hydrating, removeOnly) {
else {
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
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)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
updateChildren
patch
当中的updateChildren
就是我们的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]
然后循环进行以下操作:
头头比较,相同则复用,新旧头指针同时右移。
if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
尾尾比较,相同则复用,新旧尾指针同时左移。
if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
旧头和新尾比较,相同则将旧头插入新节点的尾部,旧头指针右移,新尾指针左移。
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]
}
旧尾和新头比较,相同则将旧尾插入新节点的头部,旧尾指针左移,新头指针右移。
if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
都没有找到,根据key
去直接findIndex
。
else {
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)
}
最后,如果新节点数多于旧节点数,就新增节点,如果少于,就移除多余节点
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)
}
可以看到最好的情况下,Vue2使用的双端diff算法复杂度是O(n),因为新旧节点能匹配上。
而最坏情况下,复杂度是O(n^2),因为需要findIndex
。
这样我们Vue从构建到运行再到编译最后进行渲染和更新的路径就完成了。