文章内容输出来源:拉勾教育大前端高薪训练营
在拉钩训练营学习已经有一段时间了,感觉这段时间的收获远比自己独自学习强的多,自己学习的时候经常会因为惰性,无法坚持,在这里有班主任时刻关注你的学习进度(感觉对我这种懒人蛮好的),最重要的是有了一个学习的计划,不用无头苍蝇一样东一点西一点,最后什么都没记住。学习都是需要方法和技巧的,在训练营会有专业的老师为你答疑解惑,同时会教你解决问题的思路及方法,让你能够触类旁通。这篇文章主要是Vue源码学习中阶段最核心的部分,初始化、响应式原理及虚拟DOM:
vueSource
├─ scripts - - - - - - - -构建相关
├─ dist - - - - - - - -构建后文件输出目录
├─ examples- - - - - - - -应用案例
├─ flow- - - - - - - - - -类型声明,flow
├─ src- - - - - - - - - - 源码相关
│ ├─ compiler- - - - - - 编译器代码的存放目录,将 template 编译为 render 函数
│ ├─ core- - - - - - - - 存放通用的,与平台无关的代码
│ ├─ platforms- - - - - -包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里
│ │ ├─ web- - - - - - - web平台
│ │ └─ weex- - - - - - -混合应用
│ ├─ server- - - - - - - 包含服务端渲染(server-side rendering)的相关代码
│ ├─ sfc- - - - - - - - -包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│ └─ shared- - - - - - - 包含整个代码库通用的代码
打包工具 Rollup
设置 sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
调试
Vue 的不同构建版本
UMD | CommonJS | ES Module (基于构建工具使用) | ES Module (直接用于浏览器) | |
---|---|---|---|---|
完整版 | vue.js | vue.common.js | vue.esm.js | vue.esm.browser.js |
只包含运行时版 | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js | - |
完整版 (生产环境) | vue.min.js | - | - | vue.esm.browser.min.js |
只包含运行时版 (生产环境) | vue.runtime.min.js | - | - | - |
我们通过npm run dev切入:
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
当我们执行 npm run dev 时,根据 scripts/config.js 文件中的配置:
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
}
入口文件为 web/entry-runtime-with-compiler.js,最终输出 dist/vue.js。
接下来我们根据入口文件开始分析:
在web/entry-runtime-with-compiler.js中:
import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount //缓存Vue上挂载的$mount
Vue.prototype.$mount = function ( //重写$mount 用于处理template
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) //获取元素
//el不能是body或者html
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to or - mount to normal elements instead.`
)
return this
}
const options = this.$options
//用于将template转换为render
//优先判断render,存在则不再处理template
if (!options.render) {
...省略代码
}
//调用mount渲染dom
return mount.call(this, el, hydrating)
}
// 在 Vue 上添加一个全局API `Vue.compile` 其值为上面导入进来的 compileToFunctions
Vue.compile = compileToFunctions
在./runtime/index中:
import Vue from 'core/index'
在core/index中:
// 从 Vue 的出生文件导入 Vue
import Vue from './instance/index'
最后在./instance/index中没有在进行引入Vue,说明这里就是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'
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)
}
//挂载实例方法
//注册vm的——init方法初始化vm
initMixin(Vue)
//注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
//初始化事件相关方法
//$on/$once/$off/$emit
eventsMixin(Vue)
//初始化生命周期相关的混入方法
//_update/$forceUpdte/$destroy
lifecycleMixin(Vue)
//混入render
//$nextTick/_render 同时通过installRenderHelpers 添加一系列方法
renderMixin(Vue)
export default Vue
我们可以看到上面是./instance/index.js 文件中全部的代码,首先分别从 ./init.js、./state.js、./render.js、
./events.js、./lifecycle.js 这五个文件中导入五个方法,分别是:initMixin、stateMixin、renderMixin、
eventsMixin 以及 lifecycleMixin,然后定义了 Vue 构造函数,其中使用了安全模式来提醒你要使用 new 操作
符来调用 Vue,接着将 Vue 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 Vue。
这五个方法分别在Vue的原型上挂载了对应的方法(见注释):
Vue.prototype._init = function (options?: Object) {
// ... _init 方法的函数体,此处省略
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){//...}
Vue.prototype.$on = function (): Component {}
Vue.prototype.$once = function (): Component {}
Vue.prototype.$off = function (): Component {}
Vue.prototype.$emit = function (): Component {}
Vue.prototype.$once = function (): Component {}
Vue.prototype.$off = function (): Component {}
Vue.prototype.$emit = function (): Component {}
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (fn: Function) {}
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
现在./instance/index.js中做了那些事情就基本了解了,现在我
们在返回到上一级core/index中看看做了哪些事情:
// 从 Vue 的出生文件导入 Vue
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 将 Vue 构造函数作为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
//挂载一系列静态方法
initGlobalAPI(Vue)
//服务端渲染相关
// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
// 在 Vue.prototype 上添加 $ssrContext 属性
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
return this.$vnode && this.$vnode.ssrContext
}
})
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
// Vue.version 存储了当前 Vue 的版本号
Vue.version = '__VERSION__'
// 导出 Vue
export default Vue
通过上面的代码我们可以知道,在 Vue.prototype 上分别添加了两个只读的属性,分别是:
$isServer 和 $ssrContext。接着又在 Vue 构造函数上定义了 FunctionalRenderContext
静态属性,这些是 ssr 相关代码。最后,在 Vue 构造函数上添加了一个静态属性 version,
存储了当前 Vue 的版本号。
这里面最主要的是initGlobalAPI方法它Vue 上添加一些全局的API,这些全局API以静态属性和
方法的形式被添加到 Vue 构造函数上,我们来看看 initGlobalAPI 方法都做了什么:
src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
//挂载只读对象config
Object.defineProperty(Vue, 'config', configDef)
//不推荐使用
//util 以及 util 下的四个方法都不被认为是公共API的一部分,要避免依赖他们,
//但是你依然可以使用,只不过风险你要自己控制。
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
//可以将一个对象转换为响应式对象
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
//空对象,此对象没有原型
Vue.options = Object.create(null)
//ASSET_TYPES = ['component','directive','filter']
//相当于挂载额三个方法,用于存放全局指令,组件,过滤器
//Vue.optionscomponents: Object.create(null),
//Vue.optionsdirectives: Object.create(null),
// Vue.optionsfilters: Object.create(null),
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
//builtInComponents导出一个对象里面有一个KeepAlive方法
// export default {
// KeepAlive
// }
//注册全局组件
extend(Vue.options.components, builtInComponents)
//此时Vue.options的值就是下面的值
// Vue.options = {
// components: {
// KeepAlive
// },
// directives: Object.create(null),
// filters: Object.create(null),
// _base: Vue
// }
//Vue.use
initUse(Vue)
//Vue.mixin
initMixin(Vue)
//Vue.extend
initExtend(Vue)
// Vue.component
// Vue.directive
// Vue.filter
// 上面的方法用于全局注册组件,指令和过滤器
initAssetRegisters(Vue)
}
现在 core/index.js 文件的作用我们也大致分析完毕,我们继续返回上一级,platforms/web/runtime/index.js,
之前的两个文件都是在core文件中是与平台无关的代码,现在platform中是与平台相关的代码,现在我们开始分析
platforms/web/runtime/index.js
/* @flow */
import Vue from 'core/index'
// 覆盖默认值,安装平台特定的工具方法
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安装平台特定的指令与组件
//platformDirectives -- model, show
//platformComponents -- Transition TransitionGroup
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
//此时 Vue。option
// Vue.options = {
// components: {
// KeepAlive,
// Transition,
// TransitionGroup
// },
// directives: {
// model,
// show
// },
// filters: Object.create(null),
// _base: Vue
// }
// 挂载__patch__
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 挂载$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// devtools 全局钩子
if (inBrowser) {
...省略
}
export default Vue
到目前为止Vue的构造函数大致做了哪些事情我们基本就分析完成了。后面我们再根据
./instance/index => core/index => ./runtime/index => web/entry-runtime-with-compiler.js的顺序
对其内部的相关方法做进一步的解释.
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'
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)
}
//注册vm的——init方法初始化vm
initMixin(Vue)
//注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
//初始化事件相关方法
//$on/$once/$off/$emit
eventsMixin(Vue)
//初始化生命周期相关的混入方法
//_update/$forceUpdte/$destroy
lifecycleMixin(Vue)
//混入render
//$nextTick/_render
renderMixin(Vue)
export default Vue
从上述代码可以看出当我们开始实例化Vue的时候,我们实际上是将参数传给this._init,
在最开始进行分析的时候我们知道this._init是在initMixin中初始化的,因此我们先来分析
initMixin做了什么
Vue.prototype._init = function (options?: Object) {
const vm: Component = this //vm Vue实例
// a uid 唯一标识
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */ //性能测试相关
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 表示当前实例时Vue实例
vm._isVue = true
//将Vue构造函数的options与传入的options进行合并
//内部实现了Vue 选项的规范化及合并
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {//渲染时的代理对象
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if *///性能测试相关
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) { //挂载实例
vm.$mount(vm.$options.el)
}
}
我们开始对代码进行详细分析:
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
这段代码实际上是对Vue 实例vm上添加了 $options,他是合并Vue构造函数的options以及实例化时传入的options,
内部实现了Vue选项的规范化及合并.此时Vue的构造函数的optins是这样的:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
我们继续往下执行:
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {//渲染时的代理对象
vm._renderProxy = vm
}
这里主要时给vm添加一个_renderProxy属性,如果是开发环境我们调用了initProxy:
//判断当前环境是否支持 js 原生的 Proxy 特性的
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy);
// 声明 initProxy 变量
let initProxy
if (process.env.NODE_ENV !== 'production') {
// ... 其他代码
// 在这里初始化 initProxy
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
}
// 导出
export { initProxy }
这里面主要就是导出一个initProxy,当环境为非生产环境时initProxy是一个函数,生产环境则导出undefined,
非生产环境时initProxy会在vm上挂在一个_render方法,执行时判断当前环境是否支持 js 原生的 Proxy 特性,
支持则vm._renderProxy = new Proxy(vm, handlers),不支持vm._renderProxy = vm。这里就执行完了,我们继续向下执行:
initLifecycle(vm)
vm._self = vm
//vm声明周期相关变量初始化
initLifecycle(vm)
这里是对vm生命周期相关变量初始化:
export function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent
//找到当前组件的父组件,将当前组件添加到父组件的$children中
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
//_开头表示私有成员
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
在initLifecycle之后要执行的就是initEvents:
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// 获取父元素上添加的事件
const listeners = vm.$options._parentListeners
if (listeners) {
//注册自定义事件
updateComponentListeners(vm, listeners)
}
}
上述代码的作用主要就是定义了_events,_hasHookEvent实例属性,同时将父组件上
附加的事件注册到当前组件上,接下来要执行的就是initRender
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
//插槽相关
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
//将模板template编译成编译生成的render时,使用_c方法进行渲染的方法
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
//手写render函数进行渲染的方法,h函数 将虚拟DOM转换为真实DOM
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
//定义响应式数据
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
在initrender之后执行的是:
callHook(vm, 'beforeCreate') //钩子函数
initInjections(vm) // 在初始化 data/props之前初始化inject
initState(vm)
initProvide(vm) // 在初始化 data/props之后初始化provide
callHook(vm, 'created')//钩子函数
我们看下initInjections,initProvide中是怎么执行的:
export function initInjections (vm: Component) {
//查看inject中的属性是否在在vm的_provided上,存在就将结果返回
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
//设置vm._provided
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
上面执行的实际上就是将inject中存在于vm._provided上的属性值返回,将inject中的属性在vm上设置成响应式数据,。
下面是一个provide inject例子可以帮助我们理解:
let vm = new Vue({
provide: {
msg: '100' //vm._provided
},
el: '#app',
components: {
'MyButton': {
inject: ['msg'], //inject 会被定义为响应式数据
template: `{{msg}}`,
}
}
})
接下来就是initState的实现:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
//将props中的数据注入到vm上并设置为响应式数据
if (opts.props) initProps(vm, opts.props)
//将methods中的值注入到vm,
//同时判断methods中的值是否是函数
//是否在props中有重名
//是否已$或者_开头
if (opts.methods) initMethods(vm, opts.methods)
//判断是否是函数,是函数就执行获得结果
//判断data是否是对象
//判断在methods,props中有重名
//最后使用observe将data转换为响应式对象
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
//将计算属性与侦听器注入实例
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState 包括了:initProps、initMethods、initData、initComputed 以及 initWatch。
主要是用于初始化相关选项的,最后initState要执行的是:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这里的 m o u n t 来 自 于 入 口 文 件 e n t r y − r u n t i m e − w i t h − c o m p i l e r . j s 开 始 我 们 也 已 经 分 析 过 , 这 里 面 主 要 是 重 写 了 mount 来自于入口文件entry-runtime-with-compiler.js开始我们也已经分析过,这里面 主要是重写了 mount来自于入口文件entry−runtime−with−compiler.js开始我们也已经分析过,这里面主要是重写了mount,在内部添加了将template模板处理为render函数的方法,将其挂载在options.render
上,然后在执行代理的runtime/index.js中的$mount方法:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
从上面我们可以了解到runtime/index.js中的$mount方法是使用instance/lifecircle.js中的
mountComponent方法渲染页面:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...省略
callHook(vm, 'beforeMount')//钩子
let updateComponent
/* istanbul ignore if */
//定义updateComponent
updateCompoCnent = () => {
vm._update(vm._render(), hydrating)
}
//执行Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
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')
}
return vm
}
这里面主要就是执行了两个钩子,以及顶一个一个updateCompoCnent将其作为参数传递给了Watcher对象,
并且初始执行一次。以上就是Vue初始化的大致流程。后续我们继续对Vue的响应式原理继续机型分析。
响应式原理过程比较复杂,我们从状态的初始化开始开始:
状态的初始化位于./instance/index.js的initMixin函数中的initState中,
他初始化了_data,_props,methods等
export function initState (vm: Component) {
...省略
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
...省略
}
//\core\instance\state.js
function initData (vm: Component) {
...省略 相关代码用于判断data上的属性在methods或者props上已经定义
// 响应式处理的入口
observe(data, true /* asRootData */)
}
这里observe就是响应式处理的入口:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
//判断当前value是否已经存在__ob__表逝已经是响应式数据
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (//判断当前数据是否可以进行响应式处理
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
//当前数据不是响应式数据则进行响应式处理
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
这里可以看到observer中主要是对传入的数据进行判断是否已经是响应式数据,
当前数据是否可以进行响应式处理,当传入的数据符合处理的条件时,将数据传入
Observer构造函数进行处理
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() //这里的dep主要是用于vm.$set $delete
this.vmCount = 0
//给当前数据定义一个不可遍历的属性__ob__,值就是当前Observer实例
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {//判断当前环境是否支持对象的__proto__属性
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
//对数组进行响应式处理
//数组的响应式处理只有push/pop等会改变数组自身的方法才能触发响应式
//同时会进一步遍历数组中的每一项,如果时对象/数组仍会进行对应的响应式处理
this.observeArray(value)
} else {
//处理数据
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
//遍历value上的每一项进行响应式处理
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
//数组的响应式处理
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
从上述代码我们可以知道在Observer中主要时对 传入的数组进行区分,
数组与对象执行不同的响应式处理,传入的数组为数组时会对传入的数组
原型上的方法(会改变数组自身的方法)进行重写这里主要设计到了
arrayMethods这个定义的常量,我们可以看到protoAugment中主要时将value的
proto__指向了传入的arrayMethods,copyAugment中因为不存在__proto,所以
使用的是definedPrototype的方法:
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
我们看下arrayMethods是怎么处理的:
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
//缓存结果
const result = original.apply(this, args)
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
})
})
以上代码其实是缓存了Array原型,在arrayMethods上定义了push等属性,
当value需要执行push方法是实际上执行的是它__proto__也就是arrayMethods.push,
而arrayMethods.push实际上是我们自定义的方法,他中间执行了我们插入的一些代码,
最后返回的result实际上是我们最开始缓存的Array.push的执行结果。
而对象的响应式处理则设计到defineReactive这个方法:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean //是否深度监听
) {
const dep = new Dep() //依赖收集
const property = Object.getOwnPropertyDescriptor(obj, key)
//是否可配置
if (property && property.configurable === false) {
return
}
//缓存原有的set get
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// $set $delete相关,同时对传入的val进行响应式处理-深度监听
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) {// Watcher
dep.depend() //收集依赖
if (childOb) {//用于vm.$set $delete执行时能触发更新,对象与数组处理方式不同
childOb.dep.depend()
//如果属性是数组 则特殊处理对象数组依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 新值旧值是否相等 特殊值 NaN
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
//对nweVal进行响应式处理
childOb = !shallow && observe(newVal)
//派发更新
dep.notify()
}
})
}
现在我们知道依赖收集实在defineReactive中进行定义的,它对每个属性设置的setter,getter
当触发getter时会进行依赖的收集,触发setter时会进行派发更新的操作,这里面涉及到Watcher及Dep
现在我们来分析Watcher,Dep
conexport function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...省略分非生产环境提示相关代码
callHook(vm, 'beforeMount')//钩子
let updateComponent
/* istanbul ignore if */
//定义updateComponent
updateCompoCnent = () => {
//_render 生成虚拟DOM
//调用了__patch__ 对比两个虚拟DOM的差别,最后将差异更新到真实DOM
vm._update(vm._render(), hydrating)
}
//执行Watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* 表示时渲染watcher */)
hydrating = false
//钩子
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
Watxher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
//存储了所有的Watcher 包括用户Watcher Computed Watcher 渲染Watcher
vm._watchers.push(this)
// 记录选项
if (options) {
this.deep = !!options.deep
this.user = !!options.user //是否是用户watcher
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
//第三个参数记录渲染Watcher之外的两个Watcher的回调
this.cb = cb
this.id = ++uid // uid for batching
this.active = true //是否是活动watcher
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// 第二个参数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) //返回用户watcher监听的属性对应的值得函数
if (!this.getter) {
//错误处理
}
}
this.value = this.lazy
? undefined
: this.get()
}
//入栈将当前Watcher赋值给Dep。Target
//父子组件嵌套时先把父组件对应的Watcher入栈
//再去处理子组件Watcher,子组件处理完毕出栈,继续执行组组件Watcher
// export function pushTarget (target: ?Watcher) {
// targetStack.push(target)
// Dep.target = target
// }
// export function popTarget () {
//出栈
// targetStack.pop()
// Dep.target = targetStack[targetStack.length - 1]
// }
get () {
pushTarget(this)
let value
const vm = this.vm
try {
//此处进行处理时可能会包含子组件,子组件中也会存在Watcher
//页面渲染
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {//深度监听
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
//添加依赖
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)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
//省略
}
//数据更新
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {//渲染Watcher
//将Watcher加入队列执行 最后会执行Watcher.run
queueWatcher(this)
}
}
run () {
if (this.active) {//Watcher 存活
const value = this.get() //更新视图,渲染Watcher为undefined
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
//用户Watcher 侦听器会执行
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
teardown () {
//省略
}
}
Dep.js
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
depend () { //依赖收集
if (Dep.target) {
Dep.target.addDep(this)
}
}
//触发更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
根据上述代码我们基本了解了 依赖收集与更新的过程:
当我们需要给一个对象或者数组新增一个属性时,需要使用vm.$set或者Vue.set
进行设置,现在我们来看下set的源码
export function set (target: Array<any> | Object, key: any, val: any): any {
//省略错误提示
//数组的处理方法
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
//判断设置的苏醒是否时已存在的属性
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
//如果时Vue实例或者根数据
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
//如果不是响应式数据
if (!ob) {
target[key] = val
return val
}
//对象新增的属性进行响应式处理
defineReactive(ob.value, key, val)
//派发更新
ob.dep.notify()
return val
}
上面就是set的源码,传入的数据必须对象,如果是数组则使用splice更新数据,这里的splice是
通过响应式处理的方法会触发dep.notify,如果是对象则判断当前设置的key是否已存在,存在则额直接
赋值,会触发set方法,不能是Vue实例或者$data根数据,通过ob判断target是否是响应式数据,不是直接赋值,
如果是则对新增对象进行响应式处理,并使用dep.notify派发更新。
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
//将回调加入callbacks中
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
//调用
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
//timerFunc 支持Primise的情况,不支持微任务会转为宏任务调用
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
//flushCallbacks 调用callbacks里面的任务
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
使用js对象描述虚拟DOM,Vue的虚拟DOM借鉴了snabbdom.js
const vm = new Vue({
el:"app",
render(h) {
return h('h1', {}, 'sss')
}
})
这里面的h函数其实就是createElement:
vm.$createElement 用户传入的render函数内的h vm._c 模板编译生成的render函数内的h
// vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 判断第三个参数
// 如果 data 是数组或者原始值的话就是 children,实现类似函数重载的机制
//根据传入数据的不同判断参数如何传递
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
//根据结果不同用于处理children参数
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
//判断传入的data参数是否是响应式数据 -- 不应该传入响应式数据
if (isDef(data) && isDef((data: any).__ob__)) {
……
// 返回一个空的虚拟节点
return createEmptyVNode()
}
// object syntax in v-bind
//
//判断是否是component组件
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
……
//作用于插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
// 去处理 children 用户传入的rendr函数
// 当手写 render 函数的时候调用
// 判断 children 的类型,如果是原始值的话转换成 VNode 的数组
// 如果是数组的话,继续处理数组中的元素
// 如果数组中的子元素又是数组(slot template),递归处理
// 如果连续两个节点都是字符串会合并文本节点
//最终需要赶回一维数组
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 把二维数组转换为一维数组
// 如果 children 中有函数组件的话,函数组件会返回数组形式
// 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组
children = simpleNormalizeChildren(children)
}
let vnode, ns
// 判断 tag 是字符串还是组件
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) ||
config.getTagNamespace(tag)
// 如果是浏览器的保留标签,创建对应的 VNode
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor =
resolveAsset(context.$options, 'components', tag))) {
// component
// 否则的话创建组件的Vnode对象
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
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()
}
}
以上就是vNode的创建过程,现在我们将创建好的vNode传递给vm._update 方法
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
//首次渲染 不存在说明时首次渲染
if (!prevVnode) {
//对比差异更新到真实DOM
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
//对比差异更新到真实DOM
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.
}
这里面主要实现的时根据_vnode判断是否存在已处理过的vNode,不存在说明是首次渲染,根据不同情况
传递不同参数给__patch__,这里面主要是把对比传入的两个DOM使用diff提取差异更新到真丝DOM:
// 浏览器环境
export const patch: Function = createPatchFunction({
nodeOps,//dom相关api
modules //平台相关模块(style...)与平台无关模块(指令、ref)的合集
})
// createPatchFunction 返回patch函数
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 把模块中的钩子函数全部设置到 cbs 中,将来统一触发
// cbs --> { 'create': [fn1, fn2], ... }
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
……
……
……
return function patch (oldVnode, vnode, hydrating, removeOnly) {
}
}
patch函数的执行过程
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果没有 vnode 但是有 oldVnode,执行销毁的钩子函数
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = [] //插入的vNode的数组,为了以后触发interted钩子
if (isUndef(oldVnode)) {
// 如果没有 oldVnode,创建 vnode 对应的真实 DOM
//表示只创建vNode但是不挂载
isInitialPatch = true
//将vNode转换为真实DOM
createElm(vnode, insertedVnodeQueue)
} else {
// 判断当前 oldVnode 是否是真实 DOM 元素(首次渲染)
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果不是真实 DOM,并且两个 VNode 是 sameVnode,这个时候开始执行 Diff
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null,removeOnly)
} else {//如果是真实DOM 首次渲染
if (isRealElement) {// 如果是真实DOM
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
……
//将真实DOM转换为vNode
oldVnode = emptyNodeAt(oldVnode)
}
// 真实DOM元素
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 转换为真实DOM
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 处理父节点占位符
if (isDef(vnode.parent)) {
...
}
// 如果父节点存在
if (isDef(parentElm)) {
//删除所有旧节点并触发对应钩子
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
//触发destory 钩子
invokeDestroyHook(oldVnode)
}
}
}
//触发insertedVnodeQueue的inserted钩子,isInitialPatch 是否立即执行钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
patch中有两个核心函数createElm与patchVnode,现在我们来看他们的实现:
createElm主要是将vNode转换为真实DOM
function createElm (
vnode, //虚拟DOM
insertedVnodeQueue,
parentElm,//需要插入的父节点
refElm,
nested,
ownerArray,
index
) {
//vnode.el表示是否渲染过 ownerArray是否有子节点
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // 组件
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) { //标签
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
//判断标签是否是自定义标签,是否注册了对应组件
if (isUnknownElement(vnode, creatingElmInVPre)) {
//警告
}
}
//是否有命名空间 svg
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
//给dom元素设置作用域
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
……
} else {
//将子元素转换为DOM对象
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
//触发所有的create钩子函数
invokeCreateHooks(vnode, insertedVnodeQueue)
}
//将创建好的DOM插入真实DOM
insert(parentElm, vnode.elm, refElm)
}
} else if (isTrue(vnode.isComment)) {//vNode是否是注释节点
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else { //文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
patchVnode主要是对比新旧vNode差异,然后更新到真实DOM上
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 如果新旧节点是完全相同的节点,直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
……
// 触发 prepatch 钩子函数
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 获取新旧 VNode 的子节点
const oldCh = oldVnode.children
const ch = vnode.children
// 触发 update 钩子函数
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)
}
// 如果 vnode 没有 text 属性(说明有可能有子元素)
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)
}
// 如果新节点有子节点,并且旧节点有 text
// 清空旧节点对应的真实 DOM 的文本内容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 把新节点的子节点添转换成真实 DOM,添加到 elm
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果旧节点有子节点,新节点没有子节点
// 移除所有旧节点对应的真实 DOM
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果旧节点有 text,新节点没有子节点和 text
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果新节点有 text,并且和旧节点的 text 不同
// 直接把新节点的 text 更新到 DOM 上
nodeOps.setTextContent(elm, vnode.text)
}
// 触发 postpatch 钩子函数
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode,vnode)
}
}
在上述实现中,diff算法的核心是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
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
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)) {
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)) { // Vnode moved right
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)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
//oldKeyToIdx为oldNode中key与index对应对象
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)//判断老节点中是否有新节点key
if (isUndef(idxInOld)) { // 没有说明是新节点 执行插入操作在老节点之前
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {// 有 说明key存在
vnodeToMove = oldCh[idxInOld]//老节点需要处理的额那个子节点
if (sameVnode(vnodeToMove, newStartVnode)) {//新旧节点选择器也相同 说明需要更新
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined//将旧节点中被更新的那个节点赋值为undefined,否则下次会继续对比
//将跟新节点插入到当前旧开始节点之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {//key相同但是元素不同,说明新建元素,插入
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)
}
}