目录
- 前置内容
- 解析模板
- 总结
前面我们分析了v-model的原理,接下来我们看看v-bind的实现又是怎样的呢?
前置内容
<template> | |
<div> | |
<test :propTest="a"></test> | |
<div @click="changeA">点我</div> | |
</div> | |
</template> | |
<script> | |
import test from './test.vue' | |
export default { | |
name: "TestWebpackTest", | |
components:{ | |
test | |
}, | |
mounted() { | |
console.log(this); | |
}, | |
methods:{ | |
changeA(){ | |
this.a = Math.random() | |
} | |
}, | |
data() { | |
return { | |
a:111, | |
}; | |
} | |
}; | |
</script> | |
... | |
<template> | |
<div> | |
{{propTest}} | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'test', | |
mounted() { | |
console.log(this); | |
}, | |
props:{ | |
propTest:Number | |
} | |
} | |
</script> | |
<style> | |
</style> |
解析模板
// App.vue | |
var render = function render() { | |
var _vm = this, | |
_c = _vm._self._c | |
return _c( | |
"div", | |
[ | |
_c("test", { attrs: { propTest: _vm.a } }), | |
_vm._v(" "), | |
_c("div", { on: { click: _vm.changeA } }, [_vm._v("点我")]), | |
], | |
1 | |
) | |
} |
可以看出v-on:propTest='a’会被解析成attrs: { propTest: _vm.a },看过前几篇文章的都知道会触发a变量的get方法收集依赖。目前主要是看当前组件是怎么把attrs属性传递给子组件的:
function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) { | |
if (isArray(data) || isPrimitive(data)) { | |
normalizationType = children; | |
children = data; | |
data = undefined; | |
} | |
if (isTrue(alwaysNormalize)) { | |
normalizationType = ALWAYS_NORMALIZE; | |
} | |
return _createElement(context, tag, data, children, normalizationType); | |
} |
_c(“test”, { attrs: { propTest: _vm.a } })方法主要执行createElement$1方法:
function _createElement(context, tag, data, children, normalizationType) { | |
... | |
else if ((!data || !data.pre) && | |
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) { | |
// component | |
vnode = createComponent(Ctor, data, context, children, tag); | |
} | |
... | |
if (isArray(vnode)) { | |
return vnode; | |
} | |
else if (isDef(vnode)) { | |
if (isDef(ns)) | |
applyNS(vnode, ns); | |
if (isDef(data)) | |
registerDeepBindings(data); | |
return vnode; | |
} | |
else { | |
return createEmptyVNode(); | |
} | |
} |
主要执行createComponent方法,传入参数如图所示:
接下来执行createComponent函数,并创建test的vm函数然后创建test的vnode:
function createComponent(Ctor, data, context, children, tag) { | |
... | |
var baseCtor = context.$options._base; | |
// plain options object: turn it into a constructor | |
if (isObject(Ctor)) { | |
Ctor = baseCtor.extend(Ctor); | |
} | |
... | |
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)) { | |
// @ts-expect-error | |
transformModel(Ctor.options, data); | |
} | |
// extract props | |
// @ts-expect-error | |
var propsData = extractPropsFromVNodeData(data, Ctor, tag); | |
// functional component | |
// @ts-expect-error | |
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 | |
var listeners = data.on; | |
// replace with listeners with .native modifier | |
// so it gets processed during parent component patch. | |
data.on = data.nativeOn; | |
// @ts-expect-error | |
if (isTrue(Ctor.options.abstract)) { | |
// abstract components do not keep anything | |
// other than props & listeners & slot | |
// work around flow | |
var slot = data.slot; | |
data = {}; | |
if (slot) { | |
data.slot = slot; | |
} | |
} | |
// install component management hooks onto the placeholder node | |
installComponentHooks(data); | |
// return a placeholder vnode | |
// @ts-expect-error | |
var name = getComponentName(Ctor.options) || tag; | |
var vnode = new VNode( | |
// @ts-expect-error | |
"vue-component-".concat(Ctor.cid).concat(name ? "-".concat(name) : ''), data, undefined, undefined, undefined, context, | |
// @ts-expect-error | |
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory); | |
return vnode; | |
} |
创建的vnode如下图所示,其中componetnOptions是创建vm函数时的参数。这个在后面实例化test的vm函数时有用
此时所有vnode基本创建完毕。此时执行vm._update(vm._render(), hydrating)方法,该方法主要执行vm.$el = vm._patch_(prevVnode, vnode)方法,该方法执行createChildren去遍历vnode执行createElm方法:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { | |
... | |
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { | |
return; | |
} | |
... | |
} |
由于第一个node是test是一个组件,所有会执行createComponent方法:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { | |
var i = vnode.data; | |
if (isDef(i)) { | |
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; | |
if (isDef((i = i.hook)) && isDef((i = i.init))) { | |
i(vnode, false /* hydrating */); | |
} | |
... | |
} | |
} | |
... | |
init: function (vnode, hydrating) { | |
... | |
else { | |
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)); | |
child.$mount(hydrating ? vnode.elm : undefined, hydrating); | |
} | |
} |
该方法执行componentVNodeHooks的init方法的createComponentInstanceForVnode去创建test组件的vm实例:
function createComponentInstanceForVnode(parent) { | |
var options = { | |
_isComponent: true, | |
_parentVnode: vnode, | |
parent: parent | |
}; | |
// check inline-template render functions | |
var inlineTemplate = vnode.data.inlineTemplate; | |
if (isDef(inlineTemplate)) { | |
options.render = inlineTemplate.render; | |
options.staticRenderFns = inlineTemplate.staticRenderFns; | |
} | |
return new vnode.componentOptions.Ctor(options); | |
} |
实例化过程中使用了vnode过程中创建的vm函数,在实例化的过程中会执行initInternalComponent函数,该函数从父vnode的componentOptions中获取prop数据:
if (options && options._isComponent) { | |
// optimize internal component instantiation | |
// since dynamic options merging is pretty slow, and none of the | |
// internal component options needs special treatment. | |
initInternalComponent(vm, options); | |
} |
到这里为止从App.vue中的attrs属性就已经传到test组件上了。initInternalComponent方法执行完毕继续执行initState方法:
function initState(vm) { | |
var opts = vm.$options; | |
if (opts.props) | |
initProps$1(vm, opts.props); | |
// Composition API | |
initSetup(vm); | |
if (opts.methods) | |
initMethods(vm, opts.methods); | |
if (opts.data) { | |
initData(vm); | |
} | |
else { | |
var ob = observe((vm._data = {})); | |
ob && ob.vmCount++; | |
} | |
if (opts.computed) | |
initComputed$1(vm, opts.computed); | |
if (opts.watch && opts.watch !== nativeWatch) { | |
initWatch(vm, opts.watch); | |
} | |
} |
首先执行initProps$1方法:
function initProps$1(vm, propsOptions) { | |
var propsData = vm.$options.propsData || {}; | |
var props = (vm._props = shallowReactive({})); | |
// cache prop keys so that future props updates can iterate using Array | |
// instead of dynamic object key enumeration. | |
var keys = (vm.$options._propKeys = []); | |
var isRoot = !vm.$parent; | |
// root instance props should be converted | |
if (!isRoot) { | |
toggleObserving(false); | |
} | |
var _loop_1 = function (key) { | |
keys.push(key); | |
var value = validateProp(key, propsOptions, propsData, vm); | |
/* istanbul ignore else */ | |
{ | |
var hyphenatedKey = hyphenate(key); | |
if (isReservedAttribute(hyphenatedKey) || | |
config.isReservedAttr(hyphenatedKey)) { | |
warn$2("\"".concat(hyphenatedKey, "\" is a reserved attribute and cannot be used as component prop."), vm); | |
} | |
defineReactive(props, key, value, function () { | |
if (!isRoot && !isUpdatingChildComponent) { | |
warn$2("Avoid mutating a prop directly since the value will be " + | |
"overwritten whenever the parent component re-renders. " + | |
"Instead, use a data or computed property based on the prop's " + | |
"value. Prop being mutated: \"".concat(key, "\""), vm); | |
} | |
}); | |
} | |
// static props are already proxied on the component's prototype | |
// during Vue.extend(). We only need to proxy props defined at | |
// instantiation here. | |
if (!(key in vm)) { | |
proxy(vm, "_props", key); | |
} | |
}; | |
for (var key in propsOptions) { | |
_loop_1(key); | |
} | |
toggleObserving(true); | |
} |
获取到propsOptions并循环执行_loop_1(key)方法,该方法首先执行validateProp方法校验数据和我们在test组件中定义的props类型是否相同。然后执行defineReactive方法将该prop设置在vm._props中并设置get和set。
此时我们得出一个结论,test组件会先根据props校验propsData的类型并获取值,test组件定义的props会被设置响应式。至此App.vue中的v-on中给的值已经被传到test组件并设置了初始值。
不难看出,当App.vue中的数据发生变化时会重新执行变量中的watcher的update方法重新将值传入test组件。由于该组件已经创建了会存在prevVnode值,所以不会再次创建只会执行vm._patch_(prevVnode, vnode)去更新组件的值:
Vue.prototype._update = function (vnode, hydrating) { | |
var vm = this; | |
var prevEl = vm.$el; | |
var prevVnode = vm._vnode; | |
var 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); | |
} | |
... | |
}; |
至此整个过程结束。
总结
- 会将v-on:propTest="a"解析成attrs: { propTest: _vm.a },并在渲染组件test的时候把该值传过去。
- 在实例化组件的时候会根据props里面创建的值和传进来的值做类型校验,然后并设置响应式同时设置初始值。
- 父组件值变化的时候会触发该变量的set方法父组件执行updateChildren方法对比新旧子组件并更新值。