extractPropsFromVNodeData() 方法
文件位置:/src/core/vdom/helpers/extract-props.js
/** * <comp msg="hello vue"></comp> * * 提取 props,得到 res[key] = val * 以 props 配置中的属性为 key,父组件中对应的的数据为 value * * 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里 * 这样子组件中相应的数据就会被更新 */ export function extractPropsFromVNodeData ( data: VNodeData, Ctor: Class<Component>, tag?: string ): ?Object { // 组件的 props 选项,{ props: { msg: { type: String, default: xx } } } // 这里只提取原始值,验证和默认值在子组件中处理 // we are only extracting raw values here. // validation and default values are handled in the child // component itself. const propOptions = Ctor.options.props // 没有定义 props 直接返回 if (isUndef(propOptions)) { return } /* 以组件 props 配置中的属性为 key,父组件传递下来的值为 value 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里 这样子组件中相应的数据就会被更新 */ const res = {} const { attrs, props } = data if (isDef(attrs) || isDef(props)) { // 遍历 propsOptions for (const key in propOptions) { // 将小驼峰形式的 key 转换为 连字符 形式 const altKey = hyphenate(key) /* 提示,如果声明的 props 为小驼峰形式(testProps),但由于 html 不区分大小写, 所以在 html 模版中应该使用 test-props 代替 testProps */ if (process.env.NODE_ENV !== 'production') { const keyInLowerCase = key.toLowerCase() if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { tip( `Prop "${keyInLowerCase}" is passed to component ` + `${formatComponentName(tag || Ctor)}, but the declared prop name is` + ` "${key}". ` + `Note that HTML attributes are case-insensitive and camelCased ` + `props need to use their kebab-case equivalents when using in-DOM ` + `templates. You should probably use "${altKey}" instead of "${key}".` ) } } checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false) } } return res } 复制代码
checkProp() 方法
文件位置:/src/core/vdom/helpers/extract-props.js
// 得到 res[key] = val function checkProp ( res: Object, hash: ?Object, key: string, altKey: string, preserve: boolean ): boolean { if (isDef(hash)) { /* 判断 hash(props/attrs)对象中是否存在 key 或 altKey 存在则设置给 res => res[key] = hash[key] */ if (hasOwn(hash, key)) { res[key] = hash[key] if (!preserve) { delete hash[key] } return true } else if (hasOwn(hash, altKey)) { res[key] = hash[altKey] if (!preserve) { delete hash[altKey] } return true } } return false } 复制代码
createFunctionalComponent() 方法
文件位置:/src/core/vdom/create-functional-component.js
/** * 执行函数式组件的 render 函数生成组件的 VNode : * 1、设置组件的 props 对象 * 2、设置函数式组件的渲染上下文,传递给函数式组件的 render 函数 * 3、调用函数式组件的 render 函数生成 vnode * * @param {*} Ctor 组件的构造函数 * @param {*} propsData 额外的 props 对象 * @param {*} data 节点属性组成的 JSON 字符串 * @param {*} contextVm 上下文 * @param {*} children 子节点数组 * @returns Vnode or Array<VNode> */ export function createFunctionalComponent ( Ctor: Class<Component>, propsData: ?Object, data: VNodeData, contextVm: Component, children: ?Array<VNode> ): VNode | Array<VNode> | void { // 组件配置项 const options = Ctor.options // 获取 props 对象 const props = {} // 组件本身的 props 选项 const propOptions = options.props // 设置函数式组件的 props 对象 if (isDef(propOptions)) { /* 说明该函数式组件本身提供了 props 选项, 则将 props.key 的值设置为组件上传递下来的对应 key 的值 */ for (const key in propOptions) { props[key] = validateProp(key, propOptions, propsData || emptyObject) } } else { // 当前函数式组件没有提供 props 选项,则将组件上的 attribute 自动解析为 props if (isDef(data.attrs)) mergeProps(props, data.attrs) if (isDef(data.props)) mergeProps(props, data.props) } // 实例化函数式组件的渲染上下文 const renderContext = new FunctionalRenderContext( data, props, children, contextVm, Ctor ) // 调用 render 函数,生成 vnode,并给 render 函数传递 _c 和 渲染上下文 const vnode = options.render.call(null, renderContext._c, renderContext) // 在最后生成的 VNode 对象上加一些标记,表示该 VNode 是一个函数式组件生成的,最后返回 VNode if (vnode instanceof VNode) { return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext) } else if (Array.isArray(vnode)) { const vnodes = normalizeChildren(vnode) || [] const res = new Array(vnodes.length) for (let i = 0; i < vnodes.length; i++) { res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext) } return res } } 复制代码
installComponentHooks() 方法
文件位置:/src/core/vdom/create-component.js
const hooksToMerge = Object.keys(componentVNodeHooks) /** * 在组件的 data 对象上设置 hook 对象, * hook 对象增加四个属性,init、prepatch、insert、destroy, * 负责组件的创建、更新、销毁 */ function installComponentHooks (data: VNodeData) { const hooks = data.hook || (data.hook = {}) // 遍历 hooksToMerge 数组,hooksToMerge = ['init', 'prepatch', 'insert' 'destroy'] for (let i = 0; i < hooksToMerge.length; i++) { // 比如 key = init const key = hooksToMerge[i] // 从 data.hook 对象中获取 key 对应的方法 const existing = hooks[key] // componentVNodeHooks 对象中 key 对象的方法 const toMerge = componentVNodeHooks[key] // 合并用户传递的 hook 方法和框架自带的 hook 方法,其实就是分别执行两个方法 if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge } } } 复制代码
componentVNodeHooks 对象
文件位置:/src/core/vdom/create-component.js
/* patch 期间在组件 vnode 上调用内联钩子 inline hooks to be invoked on component VNodes during patch */ const componentVNodeHooks = { // 初始化 init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { /* 被 keep-alive 包裹的组件 kept-alive components, treat as a patch */ const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { // 创建组件实例,即 new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例 const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) /* 执行组件的 $mount 方法,进入挂载阶段,接下来就是通过编译器得到 render 函数, 接着走 挂载、patch 流程,直到组件渲染到页面 */ child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, // 更新 VNode,用新的 VNode 配置更新旧的 VNode 上的各种属性 prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { // 新 VNode 的组件配置项 const options = vnode.componentOptions // 旧 VNode 的组件配置项 const child = vnode.componentInstance = oldVnode.componentInstance // 用 vnode 上的属性更新 child 上的各种属性 updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ) }, // 执行组件的 mounted 声明周期钩子 insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode // 如果组件未挂载,则调用 mounted 声明周期钩子 if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } // 处理 keep-alive 组件的异常情况 if (vnode.data.keepAlive) { if (context._isMounted) { // vue-router#1212 // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } }, /* 销毁组件 1、如果组件被 keep-alive 组件包裹,则使组件失活,不销毁组件实例,从而缓存组件的状态 2、如果组件没有被 keep-alive 包裹,则直接调用实例的 $destroy 方法销毁组件 */ destroy (vnode: MountedComponentVNode) { // 从 vnode 上获取组件实例 const { componentInstance } = vnode // 如果组件实例没有被销毁 if (!componentInstance._isDestroyed) { // 组件没有被 keep-alive 组件包裹,则直接调用 $destroy 方法销毁组件 if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { // 组件被 keep-alive 组件包裹,负责让组件失活,不销毁组件实例,从而缓存组件的状态 deactivateChildComponent(componentInstance, true /* direct */) } } } } 复制代码
createComponentInstanceForVnode 对象
文件位置:/src/core/vdom/create-component.js
/* new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例 */ export function createComponentInstanceForVnode ( // flow 不知道它是 MountedComponentVNode vnode: any, // 处于生命周期状态的 activeInstance parent: any ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } // 检查内联模版渲染函数 const inlineTemplate = vnode.data.inlineTemplate if (isDef(inlineTemplate)) { options.render = inlineTemplate.render options.staticRenderFns = inlineTemplate.staticRenderFns } return new vnode.componentOptions.Ctor(options) } 复制代码
总结
组件是如何变成 VNode?
- 组件实例
init
初始化,最后执行$mount
进入挂载阶段 - 如果是只包含 运行时 的
vue.js
,只直接进入挂载阶段,因为此时组件已经变成了渲染函数;编译过程 通过模块打包器 + vue-loader + vue-template-compiler
完成的 - 如果没有使用 预编译,则必须使用全量的
vue.js
- 挂载时如果发现组件配置项上没有
render
选项,则进入 编译阶段 - 将模版字符串编译成
AST
语法树 —— 普通的JS
对象 - 然后优化
AST
,遍历AST
对象,标记每一个节点是否为 静态节点;然后再进一步标记出 静态根节点,在组件后续更新时会跳过这些静态节点的更新,以提高性能 - 接下来从
AST
生成 渲染函数 :
- 负责生成动态节点
VNode
的render
函数 staticRenderFns
数组,其中每个元素都是一个生成静态节点VNode
的函数,这些函数会作为render
函数的组成部分,负责生成静态节点的VNode
- 接下来将渲染函数放到组件的配置对象上,进入 挂载阶段,即执行
mountComponent
方法 - 最终负责渲染组件和更新组件的是一个叫
updateComponent
方法,该方法每次执行前首先需要执行vm._render
函数,该函数负责执行编译器生成的render
,得到组件的VNode
- 将组件生成
VNode
的具体工作是由render
函数中的_c、_o、_l、_m
等方法完成的,这些方法都被挂载到Vue
实例上,负责在运行时生成组件VNode
- 设置组件配置信息,然后通过
new VNode(组件信息)
生成组件的VNode
render helper 的作用就是:在
Vue
实例上挂载一些运行时的工具方法,这些方法用在编译器生成的渲染函数中,用于生成组件的VNode
props 响应式的原理是什么?
- 在处理组件的
props
时,会提取组件的props
数据,以组件的props
配置中的属性为key
,父组件中对应的数据为value
生成一个propsData
对象 - 当组件更新时生成新的
VNode
,又会进行上一步,得到更新后的propsData
对象