划水不能停,分析vue2
本文适合对vue框架有一定了解的,如果没有用过不清楚的同学请看一下vue的官网然后再看文章思路会清晰很多。
vue源码地址:github.com/vuejs/vue
最好我们把这个源码给克隆下来方便我们以后阅读,测试。
源码结构
- benchmarks 基准测试,与其他竞品做比较
- dist 打包后的文件
- examples 部分示例
- flow 因为Vue使用flow来进行静态类型检查,这里定义了声明了一些静态类型
- packages vue还可以分别生成其它的npm包
- scripts
- src 主要源码所在位置,需要重点关注
- compiler 模板解析的相关文件
- codegen 根据ast生成render函数
- directives 通用生成render函数之前需要处理的指令
- parser 模板解析
- core 核心代码
- components 全局的组件,这里只有keep-alive
- global-api 全局方法相关,也就是添加在Vue对象上的方法,比如Vue.use,Vue.extend,Vue.mixin等
- instance 初始化相关,包括实例方法,生命周期,事件等
- observer 双向数据绑定相关文件
- util 工具方法
- vdom 虚拟dom相关
- index.js 入口文件
- platforms 平台相关的内容
- web web端独有文件
- compiler 编译阶段需要处理的指令和模块
- runtime 运行阶段需要处理的组件、指令和模块
- server 服务端渲染相关
- util 工具库
- weex weex端独有文件
- web web端独有文件
- server 服务端渲染相关
调用vue
首先我们来看一段vue代码
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
首先我们看到这段代码分析出来是,vue是个构造函数我们用new关键字来实例化一下,然后再传入一些参数。下面我们就去源码里找在那声明的这个构造函数。
src\core\instance\index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue) // 定义了init方法
stateMixin(Vue) // 初始化数据
eventsMixin(Vue) // 是初始events事件,入on emit
lifecycleMixin(Vue) // 定义生命周期
renderMixin(Vue) // 定义render
// 我们可以从这段代码中看到这是一个构造函数然后接受一个options的参数,又执行了init方法。我们顺着看下去
//在initMixin中提供了_init方法下面我们来看看这个方法都做了什么
src\core\instance\init.js
// 首先我们看到init方法也是接收最开始传入的options
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++ // 给每个实例都添加上一个唯一id每次new vue都会+1
vm._isVue = true //给当前实例坐上标记
if (options && options._isComponent) { //如果是组件则进入initInternalComponent方法合并options如果不是则进入else合并options
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
initLifecycle(vm) // 初始化组件实例关系:$parent $root $children等
initEvents(vm)// 初始化自定义事件,<children @click="xxx"> 我们在组件上注册的事件监听者是本身不是父组件。事件的派发者和监听者都是子组件
initRender(vm) // 解析组件的插槽信息,得到$solt.出来渲染函数得到CreateElement方法
callHook(vm, 'beforeCreate') //调用生命周期beforeCreate钩子函数
initInjections(vm) // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
initState(vm) // 初始化rops、methods、data、computed、watch
initProvide(vm) // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
callHook(vm, 'created') // 调用生命周期created钩子函数
if (vm.$options.el) { //如果参数中有el则执行挂在方法
vm.$mount(vm.$options.el)
}
}
$mount
src\platforms\web\entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
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
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
// 这一段代码大概是先提供一个$mount方法,在执行方法去找有没有render没有找到取找template没有找到取找el
以上大概就是new vue(options)干了点什么,接下来我们继续看
响应式
这个地方大家需要特别注意下在initState的时候vue会创建响应式,在initData这个方法中最后除了proxy所有的data还执行了observe这个方法。我们来看一下这个方法是干什么的。
src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
// 这个方法大概是判断了一下传进来的data是不是对象、数组如果是就会执行Observer这个构造函数创建一个观察者
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 在每一个key中都实例化一个Dep
this.vmCount = 0
def(value, '__ob__', this) // 给每个key都绑定一个__ob__ 属性放置Observer实例
if (Array.isArray(value)) { //判断是否是数组
const augment = hasProto //判断是否可以使用__proto__ 属性
? protoAugment // 如果可以使用重写方法重新赋值原型
: copyAugment // 如果不可以则覆盖原型
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 继续循环数组看看有没有数组中有其他对象或者数组
} else {
this.walk(value) // 如果是对象则遍历key给每个key加上setter getter
}
}
下面简单介绍下这么几个类,大家可以先有的大概的印象然后再通过源码一点一点的深入。
- 发布器 Dep类
构造函数中定义了subs集合,保存所有注册订阅者实例,uid位发布器对象的标识。 addSub、removeSub方法,添加和移除订阅器,维护实例集合。depend方法,将当前的watcher实例对象添加到subs集合中。notify方法,便利subs中的订阅者实例,调用update更新函数。Dep就是处理dom和wathcer的。 - 订阅者 watcher类
在实例化的时候调用get方法,获取当前的监听属性的值,同时触发该属性的get方法,调用dep.depend方法将订阅实例添加到发布器Dep中。run方法,收到变化通知,比较数据的前前后值,调用cb实现视图更新
大概就是这样
bar id:1 subs:[watchter,watcher]
foo id:2 subs:[watcher]
zoo id:3 subs:[]
每个id都有一个唯一id 一个存放watcher实例的subs队列
上述代码在Observer中实例化了Dep下面我们来看一下watcher在什么地方实例化的。我们先越过将html解析成ast、ast转换为字符串然后生成渲染函数那段的逻辑。生成render函数之后会调用mount方法。注意这个方法不是entry-runtime-with-compiler.js复写的这个,是Vue原型上的方法在src\platforms\web\runtime\index.js中。里边会调用mountComponent方法
src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的render函数,则会抛出警告
// render是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 没有获取到vue的模板文件
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount') // 执行beforeMount钩子函数
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
vm._watcher = new Watcher(vm, updateComponent, noop) // 给每个组件增加一个wathcer实例(这个地方和vue1不同 vue1是给每个模板变量一个watcher)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted') //调用mounted钩子函数
}
return vm
}
异步更新
我们在改变数据的时候,相当于触发了setter拦截器,回执行dep.notify()方法。我们看一下这个方法做了什么?
src\core\observer\dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
// 先将subs复制一份 然后根据每个id进行排序,遍历subs执行watcher中的每一个update()方法
src\core\observer\watcher.js
update () {
if (this.lazy) { //如果是懒执行 将dirty设置为true 可以让computedGetter执行的时候重新计算computed的回调函数的值
this.dirty = true
} else if (this.sync) {
//同步函数执行的标志,在使用$watcher和watcher选项时增加一个sync:true的配置
// 直接更新
this.run()
} else {
// 这个是我们最长用的一种方法,把watcher放到一个队列中
queueWatcher(this)
}
}
src\core\observer\scheduler.js
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
...
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 判断全局has中有没有这个id,如果已经存在就不放在队列中等后续操作
if (has[id] == null) { // has[id]的值一共有三种可能,null, undefined和true.null表示watcher在更新队列中但已经被更新了或正在更新,true表示watcher在队列中且尚未更新,undefined表示watcher不在队列中.
has[id] = true // 给当前id坐上标记,以供后续判断
if (!flushing) { // 当前没有刷新的队列,直接把watcher放在全局的队列中
queue.push(watcher)
} else {
// 如果已经刷新队列了
// 倒序遍历队列,找到watcher.id对应的队列位置+1处插入watcher,保证队列的排序不会发生变化
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) { // 一个标记,是否向nextTick传递了flushSchedulerQueue方法
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue() // 直接调用
return
}
nextTick(flushSchedulerQueue) // 将回调函数flushSchedulerQueue 方法到callbacks数组中
}
}
}
src\core\util\next-tick.js
const callbacks = []
let pending = false
// 参数一个cb 一个上下文
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将cbf放入callbcaks ,用trycatch方便捕获错误
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // 修改pending状态 执行timerFunc()
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
//主要是上面把callbacks中存放的flushSchedulerQueue方法放在异步中调用,如果浏览器支持原生Promise或者MutationObserver,则表示使用的微任务,否则表示回调放在宏任务中执行,这样只有当执行栈中所有同步代码执行完成,才会执行nextTick方法中传入的flushSchedulerQueue
function flushCallbacks () {
pending = false // 重置pendging
const copies = callbacks.slice(0) // 复制所有的callback
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() // 循环遍历执行
}
}
编译
作为整个vue2中最最困难的一部分,我们来一步一步的分析下.
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
// 初始化完毕之后会执行一个$mount方法进行挂载我们来看一下都生发了什么?
src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) // 挂载的元素
/* istanbul ignore if */
// 不是是body和html元素,因为在后续更新中会出现复制现在所有节点,然后创建复制的节点,再把原节点替换的操作
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
}
const options = this.$options
// resolve template/el and convert to render function
// 如果选项中有render,则跳过编译阶段
if (!options.render) {
let template = options.template
if (template) {
// 处理template
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
// 处理完,进入编译阶段
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 编译模板,得到连个函数一个是动态编译函数,一个是静态编译函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
// 界定符: {{}}
delimiters: options.delimiters,
comments: options.comments
}, this)
// 将编译好的函数放到$options
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 最后进行挂载
return mount.call(this, el, hydrating)
}
src\compiler\to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null) // 创建一个闭包,实现一个缓存
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options) // 合并配置项
const warn = options.warn || baseWarn // 创建错误日志
delete options.warn
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
// detect possible CSP restriction
try {
new Function('return 1')
} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn(
'It seems you are using the standalone build of Vue.js in an ' +
'environment with Content Security Policy that prohibits unsafe-eval. ' +
'The template compiler cannot work in this environment. Consider ' +
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
'templates into render functions.'
)
}
}
}
// 如果有缓存,则跳过编译,直接获取上次的结果
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 执行compile函数得到编译结果
const compiled = compile(template, options)
// check compilation errors/tips
if (process.env.NODE_ENV !== 'production') {
if (compiled.errors && compiled.errors.length) {
if (options.outputSourceRange) {
compiled.errors.forEach(e => {
warn(
`Error compiling template:\n\n${e.msg}\n\n` +
generateCodeFrame(template, e.start, e.end),
vm
)
})
} else {
warn(
`Error compiling template:\n\n${template}\n\n` +
compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
vm
)
}
}
if (compiled.tips && compiled.tips.length) {
if (options.outputSourceRange) {
compiled.tips.forEach(e => tip(e.msg, vm))
} else {
compiled.tips.forEach(msg => tip(msg, vm))
}
}
}
// turn code into functions
const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors) // 给res.render赋值上创建动态函数
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
}) // 给res.staticRenderFns赋值上创建静态函数
// check function generation errors.
// this should only happen if there is a bug in the compiler itself.
// mostly for codegen development use
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
warn(
`Failed to generate render function:\n\n` +
fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
vm
)
}
}
// 将res结果保存起来
return (cache[key] = res)
}
}
src\compiler\create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
// 创建一个finalOptions 他的原型是baseOptions
const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []
let warn = (msg, range, tip) => {
(tip ? tips : errors).push(msg)
}
// 如果有选项,合并选项到finalOptions
if (options) {
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// $flow-disable-line
const leadingSpaceLength = template.match(/^\s*/)[0].length
warn = (msg, range, tip) => {
const data: WarningMessage = { msg }
if (range) {
if (range.start != null) {
data.start = range.start + leadingSpaceLength
}
if (range.end != null) {
data.end = range.end + leadingSpaceLength
}
}
(tip ? tips : errors).push(data)
}
}
// 合并module到finalOptions上
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// 合并directives到finalOptions上
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
//合并其他选项
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
// 编译template
const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
src\compiler\index.js
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
// 将模板字符串解析成ast
if (options.optimize !== false) {
optimize(ast, options) // 优化ast
}
const code = generate(ast, options) // 生成渲染函数
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
diff
当组件更新时,watcher会执行一个update方法,这个方法会执行vm._render()函数得到虚拟dom,然后执行patch。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el // 页面的元素
const prevVnode = vm._vnode // 老vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode // 传递的新vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) { // 如果没有老VNode,表示页面初始化不用对比直接挂载新节点
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 组件更新,对比新老vnode
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
export const patch: Function = createPatchFunction({ nodeOps, modules })
// patch 就是执行了createPatchFunction 方法 传递了一些操作dom的方法,操作attr、class、style、even、 directive 和 ref的方法。
src\core\vdom\patch.js
//这个文件是diff的核心
// 重点看updateChildren方法
// diff做了四种可能,老节点有vnode新节点没有vnode直接批量删除,老节点没有vnode新节点有vnode直接批量新增。老节点有vnode新节点也有vnode,开始循环对比。
//对比采用老节点的第一项节点对比,新节点的第一项。
//如果没有匹配到去对比新节点的最后一项。如果还没有对比到使用老节点的最后一项对比。如果找到相同节点移动到正确位置。
//如果是老vnode循环先结束表示,剩下没有对比到的新vnode是批量新增的。如果新vnode循环结束,说明老vnode节点多执行批量删除
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 初始化:老节点开始、结束索引,新节点开始、结束索引,并且记录老节点的第一个vnode和最后一个vnode,新节点的第一个vnode和最后一个vnode
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
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh) //检查新节点的key是否重复,如果重复报错
}
// 开始循环新老节点,如果其中一个遍历完成就停止循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 调整索引对应的vnode保证vnode存在
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 新老节点的开始节点是同一个节点,执行patchVnode,结束后索引分别+1
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 新老节点的结束节点是同一个节点,执行patchVnode,结束后索引分别 - 1
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 老节点的开始节点和新节点的结束节点是同一个节点,执行patchVnode,结束后老节点索引+1 新节点索引-1
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,老节点的索引 - 1 新节点的索引 + 1
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)) { // 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]
}
}
if (oldStartIdx > oldEndIdx) {
// 如果老vnode先结束批量增加
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 如果新vnode先结束批量删除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
总结
一个大概的Vue分析就已经完成了,对与新手同学看源码晕晕的,看不懂都是正常的。我的学习方法就是打断点进去一步一步的执行看看每一步执行的结果,然后慢慢的给串起来。先画一个大概的脑图然后一步一步的给它完善起来。每当遇到问题的时候先分析一下之前vue都做了什么猜测接下来可能怎么解决。还有就是平时一点一点积累了,不要想着一口吃成个胖子!