赞
踩
highlight: tomorrow-night-eighties
通过这篇文章可以了解如下内容
$forceUpdate
原理Vue中实现了具名插槽和作用域插槽两种,而具名插槽在父组件中可以通过slot="header"
属性或v-slot:header
指定插槽内容;先从具名插槽(slot 属性)看起。
先看下父组件demo ```html
```
编译后
javascript _c( "div", [ _c("child", [ _c( // header 的插槽内容 "h1", { attrs: { // 插槽名称 slot: "header", }, slot: "header", // 插槽名称 }, [_v(_s(title))] ), _v(" "), _c("p", [_v(_s(message))]), // 默认插槽 _v(" "), _c( // footer 的插槽内容 "p", { attrs: { // 插槽名称 slot: "footer", }, slot: "footer", // 插槽名称 }, [_v(_s(desc))] ), ]), ], 1 );
编译后的组件代码会有子节点,子节点上会挂载一个slot
属性,值为插槽名称;并且attrs
属性中也会多一个slot
属性。而默认插槽没有添加任何属性
回顾下整个挂载流程,首先执行父组件的_render
方法创建VNode,创建VNode过程中,给响应式属性收集依赖;遇到组件时,为组件创建组件VNode,如果组件有子节点,为子节点创建VNode,并将 子节点VNode添加到componentOptions.children
中,这些子节点其实就是插槽内容。
然后执行 patch 过程创建DOM元素,当遇到组件VNode时,调用组件VNode的init
钩子函数创建组件实例。在组件实例初始化过程中会执行initRender
方法,这个方法有如下逻辑
javascript export function initRender (vm: Component) { const parentVnode = vm.$vnode = options._parentVnode const renderContext = parentVnode && parentVnode.context // options._renderChildren 就是组件VNode 的 componentOptions.children // 在 _init 中会合并 options,如果是组件实例,则将 componentOptions.children 赋值给 options._renderChildren vm.$slots = resolveSlots(options._renderChildren, renderContext) vm.$scopedSlots = emptyObject }
向当前Vue实例上挂载两个属性$slots
和$scopedSlots
vm.$slots
的值是resolveSlots
方法的返回值,resolveSlots
方法的参数是插槽内容(VNode 数组)和父级Vue实例
javascript export function resolveSlots ( children: ?Array<VNode>, context: ?Component // 指向父级 Vue 实例 ){ if (!children || !children.length) { return {} } const slots = {} for (let i = 0, l = children.length; i < l; i++) { const child = children[i] const data = child.data if (data && data.attrs && data.attrs.slot) { delete data.attrs.slot } // 因为 slot 的 vnode 是在父组件实例的作用域中生成的,所以 child.context 指向父组件 if ((child.context === context || child.fnContext === context) && data && data.slot != null ) { const name = data.slot const slot = (slots[name] || (slots[name] = [])) if (child.tag === 'template') { slot.push.apply(slot, child.children || []) } else { slot.push(child) } } else { (slots.default || (slots.default = [])).push(child) } } for (const name in slots) { if (slots[name].every(isWhitespace)) { delete slots[name] } } return slots }
遍历VNode数组,如果有data.attrs.slot
则将此属性删除;然后判断VNode中存储的Vue实例是不是和父级的Vue实例相同,因为插槽的VNode是在父组件实例中创建的,所以这条是成立的,然后判断有没有slot
属性:
template
,则将 当前VNode的所有子节点 添加到slots[name]
中;反之将 当前VNode 添加到slots[name]
中slots.default
中最后,遍历slots
,将注释VNode或者是空字符串的文本VNode去掉;并返回 slots
。vm.$slots
的属性值如下
javascript vm.$slots = { header: [VNode], footer: [VNode], default: [VNode] }
resolveSlots
方法就是生成并返回一个对象slots
,属性名为插槽名称,属性值为VNode数组
当创建完子组件实例后,进入子组件的挂载过程。先看下demo和子组件编译后的代码
```html
```
编译后的代码
javascript _c("div", { staticClass: "container" }, [ _c("header", [_t("header")], 2), _v(" "), _c("main", [_t("default", [_v("默认内容")])], 2), _v(" "), _c("footer", [_t("footer")], 2), ]);
编译后的代码中,<slot>
标签被编译成了 _t
函数,第一个参数是插槽名称,第二个参数是创建后备内容VNode
的函数
子组件在执行render
函数创建 VNode 时,会执行_t
函数,_t
函数对应的就是 renderSlot
方法,它的定义在 src/core/instance/render-heplpers/render-slot.js
中:
```javascript export function renderSlot ( name: string, fallback: ?Array , props: ?Object, bindObject: ?Object ): ?Array { const scopedSlotFn = this.$scopedSlots[name] let nodes if (scopedSlotFn) { // 作用域插槽 } else { nodes = this.$slots[name] || fallback }
const target = props && props.slot if (target) { return this.$createElement('template', { slot: target }, nodes) } else { return nodes } } ```
对于具名插槽,renderSlot
方法会根据传入的插槽名称,返回vm.$slots
中对应插槽名称的VNode,如果没有找到插槽VNode,则调用fallback
去创建后备内容的VNode,此时是在子组件实例中,但是插槽VNode的创建是在父组件实例中创建的
到此创建过程就完成了,插槽内容也放到了对应位置。
当父组件修改响应式属性时,通知父组件的Render Watcher
更新。调用父组件的render
方法创建VNode,在这个过程中,还会创建组件VNode和插槽VNode,将插槽VNode放入componentOptions.children
中。接着进入patch过程,对于组件的更新,会调用updateChildComponent
函数更新传入子组件的属性
javascript export function updateChildComponent ( vm: Component, // 子组件实例 propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, // 组件 vnode renderChildren: ?Array<VNode> // 最新的插槽VNode数组 ) { const needsForceUpdate = !!( renderChildren || // has new static slots vm.$options._renderChildren || // has old static slots hasDynamicScopedSlot ) if (needsForceUpdate) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$forceUpdate() } }
对于有插槽内容的具名插槽来说,vm.$options._renderChildren
有值,所以needsForceUpdate
为true
,调用resolveSlots
从parentVnode
中获取最新的vm.$slots
,并调用vm.$forceUpdate()
去更新组件视图。
javascript Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }
Vue.prototype.$forceUpdate
就是调用Render Watcher
的update
方法去更新视图
使用slot
属性表示插槽内容的具名插槽,在父组件编译和渲染阶段会直接生成 vnodes
并将插槽VNode放到组件VNode的componentOptions.children
中,在创建子组件实例时,将插槽VNode挂载到vm.$slots
上;当创建子组件VNode的时候,根据插槽名称从vm.$slots
获取对应VNode,如果没有则创建后备VNode。
当父组件更新响应式属性时,触发父组件Render Watcher
更新。生成插槽VNode;重新设置vm.$slots
,并调用vm.$forceUpdate()
触发子组件更新视图
先看下 demo 和编译后的代码 ```html
hello from parent {{props.text + props.msg}}
编译后的父组件代码中,添加一个`scopedSlots`属性,属性值是`_u`函数
javascript with (this) { return _c( 'div', [ _c('child', { scopedSlots: _u([ // 这里 { key: 'hello', // 插槽名 fn: function (props) { // 创建插槽内容的VNode return [ _c('p', [ _v( 'hello from parent ' + _s(props.text + props.msg) // 从传入的 props 中拿值 ), ]), ] }, }, ]), }), ], 1 ) }
```
父组件创建VNode时,会执行scopedSlots
属性内的_u
方法,_u
方法对应的就是resolveScopedSlots
方法,定义在src/core/instance/render-helpers/resolve-scoped-slots.js
中
javascript export function resolveScopedSlots ( fns: ScopedSlotsData, res?: Object, hasDynamicKeys?: boolean, contentHashKey?: number ): { [key: string]: Function, $stable: boolean } { // 如果没有传入 res,则创建一个对象; // 对象内有一个 $stable 属性,如果不是动态属性名、插槽上没有 v-for、没有 v-if 则为 true res = res || { $stable: !hasDynamicKeys } for (let i = 0; i < fns.length; i++) { const slot = fns[i] if (Array.isArray(slot)) { resolveScopedSlots(slot, res, hasDynamicKeys) } else if (slot) { if (slot.proxy) { // 使用 v-slot:header (2.6新增的具名插槽)时,proxy 为 true slot.fn.proxy = true } res[slot.key] = slot.fn } } if (contentHashKey) { (res: any).$key = contentHashKey } return res }
其中,fns
是一个数组,每一个数组元素都有一个 key
和一个 fn
,key
对应的是插槽的名称,fn
对应一个函数。整个逻辑就是遍历这个 fns
数组,生成一个对象,对象的 key
就是插槽名称,value
就是渲染函数。这个渲染函数的作用就是生成VNode;
先看 demo 和 编译后的代码 ```html
编译后的子组件
javascript c( "div", { staticClass: "child" }, [t("hello", null, { text: "123", msg: msg })], // 这里 2 ); `` 生成的代码中
标签也被转换成了
t函数,相对于具名插槽,作用域插槽的
t`函数多了一个参数,是一个由子组件中的响应式属性组成的对象
当创建子组件实例时,会调用initRender
方法,这个方法内会创建vm.$scopedSlots = emptyObject
;然后执行子组件的render
函数创建VNode,在_render
函数中有这样一段逻辑
javascript const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) }
如果组件VNode不为空,说明当前正在创建子组件的渲染VNode,执行normalizeScopedSlots
方法,传入组件VNode的scopedSlots
属性、vm.$slots
(这里是空对象)、vm.$scopedSlots
(也是空对象),并将返回值赋值给vm.$scopedSlots
。上面说过,在创建父组件的渲染VNode时,会调用_u
方法,返回一个对象赋值给_parentVnode.data.scopedSlots
,属性名是插槽名称,属性值是创建插槽内容VNode的渲染函数。
```javascript export function normalizeScopedSlots ( slots: { [key: string]: Function } | void, normalSlots: { [key: string]: Array }, prevSlots?: { [key: string]: Function } | void ): any { let res const hasNormalSlots = Object.keys(normalSlots).length > 0 // $stable 在 u 中定义 const isStable = slots ? !!slots.$stable : !hasNormalSlots const key = slots && slots.$key if (!slots) { res = {} } else if (slots.normalized) {} else if () { } else { // 从这里开始 // 创建过程 res = {} // 遍历传入的slots,对每个属性值调用 normalizeScopedSlot 方法 for (const key in slots) { if (slots[key] && key[0] !== '$') { res[key] = normalizeScopedSlot(normalSlots, key, slots[key]) } } } // ...
// 缓存 if (slots && Object.isExtensible(slots)) { (slots: any)._normalized = res } // 将 $stable、$key、$hasNormal 添加到 res 中,并不可枚举 def(res, '$stable', isStable) def(res, '$key', key) def(res, '$hasNormal', hasNormalSlots) // vm.$slots 有属性则为 true,反之为 false return res } ```
创建过程的就是生成一个对象,对象key
是插槽名称,value
是normalizeScopedSlot
函数的返回值
javascript function normalizeScopedSlot(normalSlots, key, fn) { const normalized = function () {} if (fn.proxy) {} return normalized }
normalizeScopedSlot
方法创建并返回了一个normalized
函数;对于作用域插槽来说,fn.proxy
为false
。
回到normalizeScopedSlots
方法中,将生成的对象缓存到slots._normalized
,然后将 $stable
、$key
、$hasNormal
添加到 res
中,返回res
。也就是说vm.$scopedSlots
是一个对象,属性名是插槽名称,属性值是normalized
函数。vm.$scopedSlots
中还有$stable
、$key
、$hasNormal
这三个属性。
vm.$scopedSlots
赋值完成后,接下来执行子组件的render
函数,在这期间会执行_t
函数,也就是renderSlot
函数
```javascript export function renderSlot ( name: string, fallback: ?Array , props: ?Object, bindObject: ?Object ): ?Array { const scopedSlotFn = this.$scopedSlots[name] let nodes if (scopedSlotFn) { // scoped slot props = props || {} // ...
nodes = scopedSlotFn(props) || fallback
} else { nodes = this.$slots[name] || fallback }
const target = props && props.slot if (target) { return this.$createElement('template', { slot: target }, nodes) } else { return nodes } } ```
首先根据传入的插槽名称从$scopedSlots
中获取对应normalized
函数,并调用normalized
函数,将props
传入,这个props
就是子组件通过插槽传递给父组件使用的属性
javascript const normalized = function () { // 调用插槽的渲染函数,创建插槽VNode let res = arguments.length ? fn.apply(null, arguments) : fn({}) res = res && typeof res === 'object' && !Array.isArray(res) ? [res] // single vnode : normalizeChildren(res) return res && ( res.length === 0 || (res.length === 1 && res[0].isComment) // #9658 ) ? undefined : res }
normalized
会执行插槽的渲染函数,并传入props
去创建VNode、对使用到的属性做依赖收集。由此作用域插槽创建VNode过程就结束了。其实可以发现,作用域插槽的VNode的创建是在子组件中创建的,所以创建插槽VNode过程中,收集到的依赖是组件的Render Watcher
当子组件修改响应式属性时,通知子组件Watcher更新,创建子组件的渲染VNode;在创建期间会调用normalizeScopedSlots
根据vm.$scopedSlots
获取key
为插槽名、value
为插槽的渲染函数的对象,对于更新过程,这里做了一个优化(看注释); javascript // initRender const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) }
``javascript export function normalizeScopedSlots ( slots: { [key: string]: Function } | void, normalSlots: { [key: string]: Array<VNode> }, prevSlots?: { [key: string]: Function } | void ): any { let res const hasNormalSlots = Object.keys(normalSlots).length > 0 const isStable = slots ? !!slots.$stable : !hasNormalSlots const key = slots && slots.$key if (!slots) { res = {} } else if (slots._normalized) { // fast path 1: 只有子组件更新,父组件不更新,返回上次创建的对象 // 后面会说怎么判断的,也可以直接全局搜
fast path 1` return slots._normalized } else if ( isStable && prevSlots && prevSlots !== emptyObject && key === prevSlots.$key && !hasNormalSlots && !prevSlots.$hasNormal ) { // fast path 2: 父组件更新,但是作用域插槽没有变化,返回上次创建的对象 return prevSlots } else {
} if (slots && Object.isExtensible(slots)) { (slots: any)._normalized = res } return res } ```
当父组件修改某响应式属性时,通知父组件Render Watcher
更新。在父组件创建VNode阶段调用_u
函数重新获取scopedSlots
属性;在patch过程中,会调用updateChildComponent
方法
```javascript export function updateChildComponent ( vm: Component, // 子组件实例 propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, // 组件 vnode renderChildren: ?Array ) { const newScopedSlots = parentVnode.data.scopedSlots const oldScopedSlots = vm.$scopedSlots const hasDynamicScopedSlot = !!( (newScopedSlots && !newScopedSlots.$stable) || (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) || (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) )
const needsForceUpdate = !!( renderChildren || // has new static slots vm.$options._renderChildren || // has old static slots hasDynamicScopedSlot )
// resolve slots + force update if has children if (needsForceUpdate) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$forceUpdate() } } ```
updateChildComponent
方法内,首先获取新老scopedSlots
对象,判断needsForceUpdate
是否为true
,如果为true
则调用vm.$forceUpdate()
触发更新
needsForceUpdate
为true
的条件是
hasDynamicScopedSlot
为 true
scopedSlots
对象不为空,并且有动态插槽或者插槽上有v-for
或v-if
scopedSlots
对象不为空,并且有动态插槽或者插槽上有v-for
或v-if
scopedSlots
对象不为空,并且新老scopedSlots
对象的$key
不同也就是说对于通过slot
属性指定插槽内容的具名插槽,当父组件修改响应式属性时,必 触发子组件更新,不管父组件的响应式属性有没有在插槽中使用;除非没有插槽内容。
而对于作用域插槽,当父组件修改响应式属性时,只有插槽名是动态的时候,才会触发子组件更新。
作用域插槽在父组件编译和渲染阶段并不会直接生成 vnodes
,而是在父节点 vnode
的 data
中保留一个 scopedSlots
对象,存储着不同名称的插槽以及它们对应的渲染函数,创建子组件实例时,将这个属性挂载到vm.$scopedSlots
中;当创建子组件的渲染VNode时,将子组件响应式属性传入并执行这个渲染函数从而创建传入的插槽VNode;创建过程中会将子组件的Render Watcher
添加到响应式属性的dep.subs
中
当子组件修改响应式属性时(不管这个属性有没有应用到作用域插槽中),触发Watcher更新。重新获取vm.$scopedSlots
;在创建渲染VNode过程中,执行插槽函数创建插槽VNode并传入子组件属性
当父组件修改某响应式属性时,通知父组件Render Watcher
更新。执行render
函数过程中,创建新的插槽对象,如果新老插槽对象中有动态插槽则调用vm.$forceUpdate()
触发子组件更新,反之不触发
在normalizeScopedSlots
中有个fast path 1
,如果父组件触发了子组件更新,执行_u
函数创建插槽对象_parentVnode.data.scopedSlots
,由于新创建的_parentVnode.data.scopedSlots
上面没有挂载_normalized
属性,所以只有子组件更新,父组件不更新时,才会走这个逻辑。
2.6
之后,具名插槽和作用域插槽引入了一个新的统一的语法 (即v-slot
指令)。它取代了slot
和slot-scope
这两个属性
```html
hello from parent {{title}}
```
编译后,和作用域插槽的一个区别就是使用的属性title
是从父组件中获取的,并且proxy
为true
```javascript with (this) { return c( 'div', [ _c('child', { scopedSlots: _u([ { key: 'hello', // 插槽名 fn: function () { // 插槽VNode的渲染函数 return [ _c('p', [v('hello from parent ' + _s(title))]), // 从 this 上拿值 ] }, proxy: true, // 这里为 true }, ]), }), ], 1 ) }
`` 和作用域插槽流程基本一致,先执行
u创建一个插槽对象,属性名是插槽名称,属性值是一个渲染函数,用于创建VNode。然后在子组件的
render方法中,执行
normalizeScopedSlots`方法
javascript export function normalizeScopedSlots ( slots: { [key: string]: Function } | void, normalSlots: { [key: string]: Array<VNode> }, prevSlots?: { [key: string]: Function } | void ): any { let res const hasNormalSlots = Object.keys(normalSlots).length > 0 const isStable = slots ? !!slots.$stable : !hasNormalSlots const key = slots && slots.$key if (!slots) {} else if (slots._normalized) {} else if () { } else { res = {} for (const key in slots) { if (slots[key] && key[0] !== '$') { res[key] = normalizeScopedSlot(normalSlots, key, slots[key]) } } } for (const key in normalSlots) { if (!(key in res)) { res[key] = proxyNormalSlot(normalSlots, key) } } if (slots && Object.isExtensible(slots)) { (slots: any)._normalized = res } def(res, '$stable', isStable) def(res, '$key', key) def(res, '$hasNormal', hasNormalSlots) return res }
创建一个res
对象,属性名为插槽名称,属性值为渲染函数。属性值是通过 normalizeScopedSlot
返回的
javascript function normalizeScopedSlot(normalSlots, key, fn) { const normalized = function () {} if (fn.proxy) { // 如果是 v-slot:header 的方式,向 vm.$slot 中添加属性 header,属性值是 normalized Object.defineProperty(normalSlots, key, { get: normalized, enumerable: true, configurable: true }) } return normalized }
相比于作用域插槽,当创建完normalized
函数后,会将插槽名称添加到vm.$slots
中,属性值为normalized
函数。
子组件和作用域插槽的demo相同
继续执行,直到执行子组件的render
函数,会调用_t
函数,也就是调用renderSlot
函数;如果有插槽内容则执行normalized
函数创建VNode;反之执行创建后备内容VNode的函数。执行过程中,如果使用到了父组件的属性,则对这个属性做依赖收集,将子组件的Render Watcher
添加到此属性的dep.subs
中。收集的是子组件的Render Watcher
创建插槽VNode是在子组件中创建的,所以收集的Watcher是子组件的Render Watcher
,所以触发子组件的Watcher更新,在执行子组件的_render
函数时,执行normalizeScopedSlots
方法,因为在第一次生成vm.$scopedSlots
对象时,会添加_normalized
属性用于缓存插槽对象,所以会直接返回之前的缓存,并通过_t
函数执行对应的插槽函数,执行期间会获取最新的父组件属性值。
父组件创建VNode期间,会重新创建子组件VNode的data.scopedSlots
属性;在更新传入子组件的属性过程中,如果新老scopedSlots
中有动态插槽名,则更新子组件视图,反之不更新
slot
:父组件在编译和渲染阶段就生成vnodes
,并收集了父组件的Render Watcher;修改父组件属性值时触发父组件更新,并重新创建插槽VNode;然后调用子组件的$forceUpdate
方法触发子组件更新。也就是说当修改的响应式属性,没有在插槽中使用时,也会触发子组件更新
v-slot
:父组件在编译和渲染阶段并不会直接生成 vnodes
,而是在父节点 vnode
的 data
中保留一个 scopedSlots
对象,存储着不同名称的插槽以及它们对应的渲染函数;只有在渲染子组件阶段才会执行这个渲染函数生成 vnodes
,此时收集的Watcher是子组件的Render Watcher。当父组件修改响应式属性时,如果修改的属性没有在插槽中使用时,是不会触发子组件更新的;只有使用到的属性更新时,才会触发子组件的Watcher更新,重新执行这个插槽函数,获取最新的属性值
作用域插槽v-slot:header="props"
和v-slot
基本相同,区别是编译后的代码,如果是作用域插槽,渲染函数中的变量是props.test
的形式,也就是说访问的其实是子组件中的响应式属性
javascript Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }
调用当前组件的 Render Watcher 的update
方法更新视图
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。