内容乃本人学习Vue2源码的一点笔记,若有错误还望指正。

源码版本:

vue: 2.6

vue-loader: 13.x

vue-template-compiler: 2.6

之前的相关学习笔记:

众所周知,Vue的脚手架项目是通过编写.vue文件来对应vue里组件,然后.vue文件是通过vue-loader来解析的,下面是我学习组件渲染过程和模板解析中的一些笔记。

Vue实例挂载方法$mount

一个普通vue应用的初始化:

import Vue from "vue";
import App from "./App.vue"; Vue.config.productionTip = false; new Vue({
render: (h) => h(App),
}).$mount("#app");

vue是在模板解析的过程中对组件渲染所依赖的数据进行收集的,而模板解析是挂载方法.$mount执行过程中的操作,.$mount方法又是在什么时候定义的呢?

1. build相关脚本

package.json中,我们可以看到有几个build相关的脚本:

{
"scripts": {
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
}
}

普通打包运行的是不带后缀的脚本build,即不带参数。

// scripts/build.js
// ... let builds = require('./config').getAllBuilds() // filter builds via command line arg
if (process.argv[2]) {
const filters = process.argv[2].split(',')
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
} build(builds) function build (builds) {
let built = 0
const total = builds.length
const next = () => {
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
} next()
} // ...

不带参数的build脚本,即代表process.argv[2]为false,进入下面这段代码:

let builds = require('./config').getAllBuilds()

// filter builds via command line arg
if (process.argv[2]) {
// ...
} else {
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}

由上述代码可知,builds是由./config模块执行getAllBuilds()所得:

// scripts/config.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)

getAllBuilds()方法是对Object.keys(builds)数组做映射操作并将结果返回,再继续看scripts/config.js中的builds变量,可以看到,是针对不同编译包不同的配置,关于weex的可以不看,因为b.output.file.indexOf('weex') === -1将weex相关的配置过滤掉了,其余的就是不同模块系统的打包配置,如cjs、es、es in browser、umd等等。

下面是es的打包配置:

// scripts/config.js
const builds = {
// ...
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
// ...
} const aliases = require('./alias')
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}

可以看到有两个,一个只有运行时的代码,另一个还包含了编译器compiler的部分。

根据aliases的配置,我们可以找到'web/entry-runtime.js'的路径解析:

// scripts/alias.js
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')
}

这里看只包含运行时代码的编译配置,找到它的入口文件resolve('web/entry-runtime.js')

// src/platforms/web/entry-runtime.js
import Vue from './runtime/index' export default Vue

继续找到src/platforms/web/runtime/index.js

// src/platforms/web/runtime/index.js
/* @flow */ import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index' import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index' // ... // install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop // public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
} // ... export default Vue

至此我们就找到了Vue原型对象上的$mount方法定义。

el拿到真实的dom节点,而mountComponent我们也可以看到,是在src/core/instance/lifecycle.js中定义的。

组件挂载mountComponent

// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
// ...
}
}
callHook(vm, 'beforeMount') let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// ...
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
} // we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
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
}

如果我们没有传入一个render函数,就会将render赋值为一个创建空VNode的函数:vm.$options.render = createEmptyVNode

再继续可以看到,创建了一个Watcher实例,并将这个watcher实例标记为renderWatcher。

在之前学习Watcher代码的时候我们有看到,在实例被创建时,如果没有设置lazy,会立即执行一遍expOrFn,也就是说此处传入的updateComponent会立即被调用,也就是会执行实例的_update方法。

updateComponent = () => {
vm._update(vm._render(), hydrating)
}

可以看到在执行_update之前会先调用_render,并将结果作为参数传给_update

渲染方法vm._render

在执行vm._update(vm._render(), hydrating)时,传入了vm._render(),即vm实例会去执行_render方法。

1. _render定义

// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
} // set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
// ...
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
// ...
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}

vnode = render.call(vm._renderProxy, vm.$createElement),如果render未定义,根据mountComponent中的代码可知使用的是createEmptyVNode,调用render时绑定this为vm实例,传入参数vm.$createElement

由vue应用初始化代码可以看到,根节点组件传入了render:

render: (h) => h(App),

调用render.call(vm._renderProxy, vm.$createElement)可以简单看作执行vm.$createElement(App);,根据上述代码查找vm实例的$createElement方法,

2. vm.$createElement

initRender中定义的:

// src/core/instance/render.js
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
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
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.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // ...
}

3. 调用_createElement

继续查找createElement函数及其调用的内部_createElement函数:

// src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
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> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
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 = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
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()
}
}

App.vue已经被webpack中的vue-loader解析为一个模块,所以此时传入_createElement的App是一个对象,即此处的形参tag

因为只有contexttag两个入参:vmApp,所以可以直接跳到看vnode = createComponent(tag, data, context, children)

createComponent返回vnode实例,_createElement函数最后也是返回一个vnode实例。

4. createComponent

// src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
} const baseCtor = context.$options._base // plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
} // if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
} // async component
let asyncFactory
if (isUndef(Ctor.cid)) {
// ... Ctor.cid有定义,此段代码可暂时忽略
} data = data || {} // resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor) // transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
} // extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
} // extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot // work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
} // install component management hooks onto the placeholder node
installComponentHooks(data) // return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
) // Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
} return vnode
}

installComponentHooks(data)使在data上挂上一个hook的属性,并且将const componentVNodeHooks的属性挂到data.hook对象上。

context.$options._base查找_base的定义,在src/core/global-api/index.js文件中的initGlobalAPI函数中定义。

Vue.options._base = Vue

baseCtor.extend(Ctor)查找extend的定义,在src/core/global-api/extend.js文件中定义。

Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
} const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
} const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super // For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
} // allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use // create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
} // keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options) // cache constructor
cachedCtors[SuperId] = Sub
return Sub
}

可以看出在Vue.extend方法中,将原本的Ctor对象改造成了一个继承Vue的子类,并且该子类在实例化时会执行实例的_init方法。

const Sub = function VueComponent (options) {
this._init(options)
}

原本Ctor对象上带有的属性都被挂载子类的options属性上。

Sub.options = mergeOptions(
Super.options,
extendOptions
)

最后,createComponent函数创建了一个vnode实例并将此实例返回:

const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }, /* componentOptions */
asyncFactory
)

可以看出,createComponent创建的vnode实例返回给createElement函数,最终传递给了vm._update

更新方法vm._update

1. 方法定义

// src/core/instance/lifecycle.js
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
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 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.
}

setActiveInstance(vm):设置activeInstance为当前vm实例。

因为是初次渲染,所以没有旧的节点,即进入下面这个条件:

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

2. vm.__patch__——>createPatchFunction

通过src/platforms/web/runtime/index.js,我们可以找到vm.__patch__方法的定义。

// src/platforms/web/runtime/index.js
import { patch } from './patch' // install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src/platforms/web/runtime/patch.js
/* @flow */ import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index' // the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules) export const patch: Function = createPatchFunction({ nodeOps, modules })

nodeOps是访问和操作真实dom的一些api。

// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction (backend) {
let i, j
const cbs = {} const { modules, nodeOps } = backend 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]])
}
}
} function emptyNodeAt (elm) {
// ...
} function createRmCb (childElm, listeners) {
// ...
} function removeNode (el) {
// ...
} function isUnknownElement (vnode, inVPre) {
// ...
} let creatingElmInVPre = 0 function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
} function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// ...
} function initComponent (vnode, insertedVnodeQueue) {
// ...
} function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// ...
} function insert (parent, elm, ref) {
// ...
} function createChildren (vnode, children, insertedVnodeQueue) {
// ...
} function isPatchable (vnode) {
// ...
} function invokeCreateHooks (vnode, insertedVnodeQueue) {
// ...
} // set scope id attribute for scoped CSS.
// this is implemented as a special case to avoid the overhead
// of going through the normal attribute patching process.
function setScope (vnode) {
// ...
} function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
// ...
} function invokeDestroyHook (vnode) {
// ...
} function removeVnodes (vnodes, startIdx, endIdx) {
// ...
} function removeAndInvokeRemoveHook (vnode, rm) {
//...
} function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// ...
} function checkDuplicateKeys (children) {
// ...
} function findIdxInOld (node, oldCh, start, end) {
// ...
} function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...
} function invokeInsertHook (vnode, queue, initial) {
// ...
} let hydrationBailed = false
// list of modules that can skip create hook during hydration because they
// are already rendered on the client or has no need for initialization
// Note: style is excluded because it relies on initial clone for future
// deep updates (#7063).
const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key') // Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
// ...
} function assertNodeMatch (node, vnode, inVPre) {
// ...
} return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
}
}

可以看到,这个函数主要做了三件事:

  • 首先对本地的hooks和传入的modules做了一次遍历

    通过查找可以看到,modules是以下两个数组合并的结果:

    // src/platforms/web/runtime/modules/index.js
    export default [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
    ]
    // src/core/vdom/modules/index.js
    export default [
    ref,
    directives
    ]

    首先函数中定义了一个本地变量cbs,通过遍历hooks在cbs上添加名为hooks[i]的属性,属性对应的值为数组;接着再通过嵌套遍历modules,如果modules[j]中存在与hooks[i]同名的属性,就将此属性对应的值(函数)塞进数组。

    可以看出此嵌套遍历就是找出hooks对应的所有回调。

  • 然后定义了一系列的内部方法和变量

    这些方法基本就是用于vnode的操作,比对、更新、移除、创建节点等等。

  • 最后返回了一个函数patch,即vue实例的__patch__方法

3. 调用vm.__patch__

调用vm.__patch__方法,即调用了下面的patch函数。

// src/core/vdom/patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
} let isInitialPatch = false
const insertedVnodeQueue = [] if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
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
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
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([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
} invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}

根据前面的步骤vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */),可知传入的参数分别是vm.$elvnodehydratingfalse,可以得出:

  • isUndef(vnode)为false

  • isUndef(oldVnode)为false

  • const isRealElement = isDef(oldVnode.nodeType)为true,真实dom节点

    执行oldVnode = emptyNodeAt(oldVnode),根据下述代码:

    function emptyNodeAt (elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
    }

    可知根据此真实dom节点创建了一个对应的虚拟节点vnode,并给它设置以下属性:

    • tag:真实dom的标签
    • data:空对象
    • children:空数组
    • text:undefined
    • elm:原真实dom
  • sameVnode(oldVnode, vnode)为false

  • (ssr暂时不管)

  • isDef(vnode.parent)为false(根节点的话)

故主要关注下面这段代码:

// 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)
)
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
} vnode.isRootInsert = !nested // for transition enter check
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)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
} vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode) /* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
} if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}

nested未传递为undefined,所以vnode.isRootInsert被赋值为true;

接着进入if判断执行createComponent(vnode, insertedVnodeQueue, parentElm, refElm)函数:

// src/core/vdom.patch.js createPatchFunction的内部函数
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}

可以看到在此处调用了data.hook上的init方法,即上述在create-component.jscomponentVNodeHooks的init对应方法:

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},

可以看到在init方法中,当vnode.componentInstance不存在时,即vnode对应的组件实例不存在时,会调用createComponentInstanceForVnode来创建组件实例。

// src/core/vdom/create-component.js
export function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode函数中,取出vnode对应组件的构造器Ctor进行实例化操作并传入参数,使用new操作创建新的组件实例。

由前文可知,此构造器函数继承自Vue,在实例化时会调用实例_init方法。

当组件实例创建完成后,会继续执行组件实例的$mount方法,即这一步:child.$mount(hydrating ? vnode.elm : undefined, hydrating),进入vnode对应组件的挂载操作,即重新走一遍上述的流程。

在该组件的_init过程中,会取出构造器的options中的render方法挂在组件实例的$options上。

现在主要看该render()方法,此方法在vue-loader中通过模板解析生成。

vue-loader生成的render方法

1. vue-loader

vue-loader/lib/loader.js

const parts = parse(
content,
fileName,
this.sourceMap,
sourceRoot,
cssSourceMap
)

通过vue-loader/lib/parser.js文件中导出的方法将传入的内容解析:

module.exports = (content, filename, needMap, sourceRoot, needCSSMap) => {
const cacheKey = hash((filename + content).replace(/\\/g, '/'))
let output = cache.get(cacheKey)
if (output) return output
output = compiler.parseComponent(content, { pad: 'line' })
if (needMap) {
if (output.script && !output.script.src) {
output.script.map = generateSourceMap(
filename,
content,
output.script.content,
sourceRoot
)
}
if (needCSSMap && output.styles) {
output.styles.forEach(style => {
if (!style.src) {
style.map = generateSourceMap(
filename,
content,
style.content,
sourceRoot
)
}
})
}
}
cache.set(cacheKey, output)
return output
}

parser调用了vue-template-compiler/build.js中的parseComponent函数,将内容解析为四部分:script、styles、template和customBlocks(自定义部分)。

// vue-template-compiler/build.js
var isSpecialTag = makeMap('script,style,template', true);
// vue-template-compiler/build.js
if (isSpecialTag(tag)) {
checkAttrs(currentBlock, attrs);
if (tag === 'style') {
sfc.styles.push(currentBlock);
} else {
sfc[tag] = currentBlock;
}
} else { // custom blocks
sfc.customBlocks.push(currentBlock);
}

继续看loader的解析:vue-loader/lib/loader.js

// vue-loader/lib/loader.js
const functionalTemplate = templateAttrs && templateAttrs.functional output += '/* template */\n'
const template = parts.template
if (template) {
if (options.esModule) {
output +=
(template.src
? getImportForImport('template', template)
: getImport('template', template)) + '\n'
} else {
output +=
'var __vue_template__ = ' +
(template.src
? getRequireForImport('template', template)
: getRequire('template', template)) +
'\n'
}
} else {
output += 'var __vue_template__ = null\n'
} // template functional
output += '/* template functional */\n'
output +=
'var __vue_template_functional__ = ' +
(functionalTemplate ? 'true' : 'false') +
'\n'

parts.template.attrs对象上如果没有functional属性,__vue_template_functional__就为false。

继续看esm并且没有src的分支。

// vue-loader/lib/loader.js
function getImport (type, part, index, scoped) {
return (
'import __vue_' + type + '__ from ' +
getRequireString(type, part, index, scoped)
)
}
// vue-loader/lib/loader.js
function getRequireString (type, part, index, scoped) {
return loaderUtils.stringifyRequest(
loaderContext,
// disable all configuration loaders
'!!' +
// get loader string for pre-processors
getLoaderString(type, part, index, scoped) +
// select the corresponding part from the vue file
getSelectorString(type, index || 0) +
// the url to the actual vue file, including remaining requests
rawRequest
)
}
// vue-loader/lib/loader.js
function getRawLoaderString (type, part, index, scoped) {
let lang = part.lang || defaultLang[type] let styleCompiler = ''
if (type === 'styles') {
// ...
} let loader =
options.extractCSS && type === 'styles'
? loaders[lang] || getCSSExtractLoader(lang)
: loaders[lang] const injectString =
type === 'script' && query.inject ? 'inject-loader!' : '' if (loader != null) {
if (Array.isArray(loader)) {
loader = stringifyLoaders(loader)
} else if (typeof loader === 'object') {
loader = stringifyLoaders([loader])
}
if (type === 'styles') {
// ...
}
// if user defines custom loaders for html, add template compiler to it
if (type === 'template' && loader.indexOf(defaultLoaders.html) < 0) {
loader = defaultLoaders.html + '!' + loader
}
return injectString + ensureBang(loader)
} else {
// unknown lang, infer the loader to be used
switch (type) {
case 'template':
return (
defaultLoaders.html +
'!' +
templatePreprocessorPath +
'?engine=' +
lang +
'!'
)
// ...
}
}
}

最后将所有内容传入一个函数中执行

output +=
'var Component = normalizeComponent(\n' +
' __vue_script__,\n' +
' __vue_template__,\n' +
' __vue_template_functional__,\n' +
' __vue_styles__,\n' +
' __vue_scopeId__,\n' +
' __vue_module_identifier__\n' +
')\n'

normalizeComponent函数:

output +=
'var normalizeComponent = require(' +
loaderUtils.stringifyRequest(loaderContext, '!' + componentNormalizerPath) +
')\n'

componentNormalizerPath函数:

const componentNormalizerPath = normalize.lib('component-normalizer')
// vue-loader/lib/component-normalizer.js
module.exports = function normalizeComponent (
rawScriptExports,
compiledTemplate,
functionalTemplate,
injectStyles,
scopeId,
moduleIdentifier /* server only */
) {
var esModule
var scriptExports = rawScriptExports = rawScriptExports || {} // ES6 modules interop
var type = typeof rawScriptExports.default
if (type === 'object' || type === 'function') {
esModule = rawScriptExports
scriptExports = rawScriptExports.default
} // Vue.extend constructor export interop
var options = typeof scriptExports === 'function'
? scriptExports.options
: scriptExports // render functions
if (compiledTemplate) {
options.render = compiledTemplate.render
options.staticRenderFns = compiledTemplate.staticRenderFns
options._compiled = true
} // functional template
if (functionalTemplate) {
options.functional = true
} // ... return {
esModule: esModule,
exports: scriptExports,
options: options
}
}

__vue_template_functional__为false的情况,即functionalTemplate为false。

可以看到是把compiledTemplate.render放在了返回的对象的options上。

所以就是要看compiledTemplate.render的定义。

2. vue-template-compiler

在上述vue-loader/lib/loader.js中的getRawLoaderString函数定义中,可以看到使用了defaultLoaders.html这个loader来处理template中的html内容。

// vue-loader/lib/loader.js
const defaultLoaders = {
html: templateCompilerPath + templateCompilerOptions,
// ...
}

这个loader定义在template-compiler/index.js文件中:

可以看到此loader的返回中包含以下代码:

// template-compiler/index.js
code =
transpile(
'var render = ' +
toFunction(compiled.render, stripWithFunctional) +
'\n' +
'var staticRenderFns = [' +
staticRenderFns.join(',') +
']',
bubleOptions
) + '\n'

这就是vue-loader生成的render方法!

// template-compiler/index.js
function toFunction (code, stripWithFunctional) {
return (
'function (' + (stripWithFunctional ? '_h,_vm' : '') + ') {' + code + '}'
)
}

compiled的定义:

// template-compiler/index.js
const compiled = compile(html, compilerOptions)

compile的定义:

// vue-template-compiler/build.js
var ref = createCompiler(baseOptions);
var compile = ref.compile;

createCompiler的定义:

// vue-template-compiler/build.js
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});

可以看到baseCompile函数做了三件事:

  • 根据options配置,将template转为ast
  • 调用optimize优化ast
  • 通过执行generate得到最终的code

可以看到render方法中的具体代码,是通过generate方法将ast转换得到:

// vue-template-compiler/build.js
function generate (
ast,
options
) {
var state = new CodegenState(options);
// fix #11483, Root level <script> tags should not be rendered.
var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}

可以看到此处的render是一个字符串,最终会通过上述template-compiler/index.js文件中的toFunction转为函数。

genElement就是分别处理不同的元素内容,最终得到的code会被设置到render的函数体中,在render被执行时,code部分的代码就会被执行。

// vue-template-compiler/build.js
function genElement (el, state) {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
} if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
var code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
var data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
} var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}

看下这里的genIf

// vue-template-compiler/build.js
function genIf (
el,
state,
altGen,
altEmpty
) {
el.ifProcessed = true; // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
} function genIfConditions (
conditions,
state,
altGen,
altEmpty
) {
if (!conditions.length) {
return altEmpty || '_e()'
} var 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) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}

从return的代码字符串中可以看出,在render方法被调用时,v-if中的表达式即condition.exp会被求值,又此时vue实例在调用$mount时已经创建了自身对应的renderWatcher,加上数据经过响应式改造,v-if中被访问的属性其对应的getter会被触发,也就收集到了组件渲染的依赖。

其他元素中的表达式也是类似,会被收集为组件渲染的依赖。

小结

父组件调用$mount方法时,执行了mountComponent函数,触发beforeMount钩子,然后会创建组件自身的renderWatcher,在watcher初始化过程中会调用_render方法,然后调用_update方法。

render执行过程中,基于Vue创建了一个组件子类,接着生成虚拟节点vnode,并且此vnode的data属性会挂上一些hook方法。

_update内部调用__patch__方法时,调用了createComponent(vnode, insertedVnodeQueue, parentElm, refElm)方法,调用了此vnode的data属性上hooks中的init创建了对应的组件实例,在组件实例化过程中通过调用_init对该实例进行初始化,然后调用$mount实例方法,在调用$mount时,该实例也会创建一个自身的renderWatcher。

子组件对应.vue文件通过vue-loader解析,在template解析时得到其对应的render方法,在render方法被调用时,模板中对应的表达式会被求值,即组件的数据会被访问,就被收集为组件渲染的依赖。

mountComponent函数的最后,触发了mounted钩子。

父子组件初始化时触发钩子的顺序:

父:beforeCreate => 父:created => 父:beforeMount => 子:beforeCreate => 子:created => 子:beforeMount => 子:mounted => 父:mounted

Vue 2.x源码学习:render方法、模板解析和依赖收集的更多相关文章

  1. Spring5.0源码学习系列之浅谈循环依赖问题

    前言介绍 附录:Spring源码学习专栏 在上一章的学习中,我们对Bean的创建有了一个粗略的了解,接着本文浅谈Spring循环依赖问题,这是一个面试比较常见的问题 1.什么是循环依赖? 所谓的循环依 ...

  2. vue虚拟DOM源码学习-vnode的挂载和更新流程

    代码如下: <div id="app"> {{someVar}} </div> <script type="text/javascript& ...

  3. JDK1.8源码学习-String-hashCode方法为什么选择数字31作为乘子

    1. 背景 某天,我在写代码的时候,无意中点开了 String hashCode 方法.然后大致看了一下 hashCode 的实现,发现并不是很复杂.但是我从源码中发现了一个奇怪的数字,也就是本文的主 ...

  4. vue 2.0源码学习笔记—new Vue ( Vue 初始化过程 )

    new Vue(Vue 初始化) 一个vue实例化到底经历了什么?已下是博主自己的总结,不正确的地方请指出,谢谢~ 一.简述 从使用角度来看,挂载的顺序如下 1. $slots 2. $scopedS ...

  5. Vue 源码学习(1)

    概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...

  6. Vue.js 源码学习笔记

    最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...

  7. Vue2.x源码学习笔记-源码目录结构整理

    先从github上下载或者clone一个vue分支项目 https://github.com/vuejs/vue 查看下目录结果 先列出一些目录 Vue |— build 打包相关的配置文件,其中最重 ...

  8. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  9. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  10. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...

随机推荐

  1. 五分钟教你使用GitHub寻找优质项目

    前言 经常会有同学会问如何使用GitHub找到自己想要的项目,今天咱们就出一期快速入门教程五分钟教你使用GitHub寻找优质项目.GitHub作为世界上最大的项目开源平台之一,上面有着无数优质的开源项 ...

  2. VUE3、ElementPlus 重构若依vue2 表单构建功能

    Vue3 + ElementPlus + Vite 重构 若依Vue2 表单构建功能 若依官方的Vue3 版本发布已经有段时间了,就是这个表单构建功能一直没有安排计划去适配到Vue3! 前段时间公司需 ...

  3. 常见的 NoSQL 数据库有哪些?

    前言 今天我们来介绍一下工作开发中常见的一些NoSQL数据库及其基本特点.欢迎在评论区留下文章中没有介绍且好用的​NOSQL数据库. 什么是NOSQL数据库 非关系型数据库又被称为 NoSQL(Not ...

  4. SpringBoot对接OpenAI

    SpringBoot对接OpenAI 随着人工智能技术的飞速发展,越来越多的开发者希望将智能功能集成到自己的应用中,以提升用户体验和应用的功能.OpenAI作为一家领先的人工智能公司,提供了许多先进的 ...

  5. Prompt Playground 7月开发记录

    Prompt Playground 2023年7月开发记录 上个月的时候,出于日常工作需求,做了一个简单的提示词调试工具 Prompt Playground. 这个工具的初衷是为了方便测试,所以没有做 ...

  6. Redis系列18:过期数据的删除策略

    Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...

  7. SpringBoot3.x原生镜像-Native Image实践

    前提 之前曾经写过一篇<SpringBoot3.x 原生镜像-Native Image 尝鲜>,当时SpringBoot处于3.0.0-M5版本,功能尚未稳定.这次会基于SpringBoo ...

  8. ABP Framework 7.4 RC 新增功能简介:增强微服务架构支持

    ABP Framework 版本号:7.4.0-rc.1 发布时间:2023.8.16 阅读原文:ABP.IO Platform 7.4 RC Has Been Published 翻译:iEricL ...

  9. What...MiniGPT-4居然开源了,提前感受 GPT-4 的图像对话能力!

    说在前面的话: 一个月前,OpenAI向外界展示了GPT-4如何通过手绘草图直接生成网站,令当时的观众瞠目结舌. 在GPT-4发布会之后,相信大家对ChatGPT的对话能力已有所了解.圈内的朋友们应该 ...

  10. 细数2019-2023年CWE TOP 25 数据,看软件缺陷的防护

    本文分享自华为云社区<从过去5年CWE TOP 25的数据看软件缺陷的防护>,作者:Uncle_Tom. "以史为鉴,可以知兴替".CWE 已经连续5年发布了 CWE ...