写在前面
本篇是从零实现vue2系列第六篇,将在 YourVue 中实现 component。从这篇开始实现的内容,博客上讨论的就比较少了,不过啃源码肯定要啃完整。
文章会最先更新在公众号:BUPPT。
正文
将 main.js 中的内容一部分提取到组件 helloWorld 中,在 YourVue 实例上注册 helloWorld 组件。
const helloWorld = {
data: {
count: 0,
items:[1,2,3,0,5],
},
props:['message'],
template: `
<div>
array: {{items}}
<div>{{count}}</div>
<button @click="addCount">addCount</button>
<h4 style="color: red">{{message}}</h4>
<button @click="decCount">decCount</button>
</div>
`,
methods:{
addCount(){
this.count += 1
this.items.push(this.count)
},
decCount(){
this.count -= 1
this.items.pop()
}
}
}
new YourVue({
el: '#app',
components:{ helloWorld },
data:{
message: "parentMessage"
},
template: `
<div>
<hello-world :message="message"></hello-world>
<button @click="change">parent button</button>
</div>
`,
methods:{
change(){
this.message = this.message.split('').reverse().join('')
}
}
})
我们可以从流程上思考一下哪里发生了变化🤔
从 template -> ast -> gencode -> render 函数这个流程是没有变化的,只不过其中有了一个 tag 为 hello-world 的 VNode,所以需要在生成 VNode 的时候添加判断,是 HTML 标签还是自定义标签。
function createElement (tag, data={}, children=[]){
children = simpleNormalizeChildren(children)
if(isHTMLtag(tag)){
return new VNode(tag, data, children, undefined, undefined)
}else{
return componentToVNode(tag, data, children, this)
}
}
isHTMLtag 就是直接判断 tag 是否在所有 HTML 元素组成的列表里,如果不是 HTML 标签就执行 componentToVNode。
export function componentToVNode(tag, data, children, vm){
if(tag.includes('-')){
tag = toHump(tag)
}
const Ctor = YourVue.extend(vm.$options.components[tag])
const name = tag
data.hooks = {
init(vnode){
const child = vnode.componentInstance = new vnode.componentOptions.Ctor({
_isComponent: true,
_parentVnode: vnode
})
initProps(child, vnode.props.attrs)
child.$mount()
},
prepatch (oldVnode, vnode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
const attrs = options.data.attrs;
for (const key in attrs) {
if(key === 'on'){
continue
}
child._props[key] = attrs[key]
}
}
}
const listeners = data.on
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, vm,
{ Ctor, tag, data, listeners, children}
)
return vnode
}
因为需要将组件定义的参数传入 YourVue 实例,所以定义 Ctor 继承 YourVue,先将组件参数作为 extendOptions 传入,在 Ctor 的构造函数中,将 extendOptions 和 options 融合作为 _init 的参数。并将 Ctor 缓存,再次使用该组件时候可以直接从缓存中读取该组件对应的 Ctor。
export default class YourVue{
static extend(extendOptions){
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const Sub = function VueComponent (options) {
this._init(mergeOptions(options,extendOptions))
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub['super'] = Super
Sub.extend = Super.extend
cachedCtors[SuperId] = Sub
return Sub
}
}
从 componentToVNode 最后可以看出来,返回的 VNode 的 tag 进行了重新命名,data 暂时有两个 hooks,其余参数都传入了 VNode 的最后一个参数 componentOptions 中。
constructor(tag, data={}, children=[], text='', elm, context, componentOptions){
this.componentOptions = componentOptions
}
VNode 创建好了,生成真实 dom 的时候就用到了 patch。在上篇文章中的 createElm 开始添加一个 createComponent 函数,在这个函数中会执行上面提到的 data.hooks.init。
function createElm (vnode, parentElm, afterElm = undefined) {
if (createComponent(vnode, parentElm, afterElm)) {
return
}
...
}
function createComponent (vnode, parentElm, afterElm) {
let i = vnode.props
if (i) {
if (i.hooks&&i.hooks.init) {
i.hooks.init(vnode)
}
if (isDef(vnode.componentInstance)) {
vnode.elm = vnode.componentInstance.vnode.elm
if(isDef(afterElm)){
insertBefore(parentElm,vnode.elm,afterElm)
}else if(parentElm){
parentElm.appendChild(vnode.elm)
}
return true
}
}
}
再返回来看 hooks.init,其中初始化了 Ctor,并传入两个参数标准 component 和记录父 VNode。最后执行 $mount 函数,生成真实 dom。可以从上面代码 vnode.elm = vnode.componentInstance.vnode.elm 发现,父组件中 hello-world component 渲染的 elm,就是子组件的真实 dom。
init(vnode){
const child = vnode.componentInstance = new vnode.componentOptions.Ctor({
_isComponent: true,
_parentVnode: vnode
})
initProps(child, vnode.props.attrs)
child.$mount()
}
initProps(child, vnode.props.attrs) 处理父组件传入子组件的 props,initProps 定义如下。
function initProps(vm, propsOptions){
const props = vm._props = {}
for (const key in propsOptions) {
if(key === 'on'){
continue
}
defineReactive(props, key, propsOptions[key])
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
}
将 propsOptions 传递来的变量通过响应式函数 defineReactive 修改 props 的 get 和 set 方法,实现发布订阅。然后通过 proxy 方法代理,这样就可以直接使用 this 来访问 props 了。
prepatch 钩子是在 patchVnode 中执行。
function patchVnode(oldVnode, vnode){
if (oldVnode === vnode) {
return
}
let i
const data = vnode.props
if (isDef(data) && isDef(i = data.hooks) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
...
}
prepatch (oldVnode, vnode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
const attrs = options.data.attrs;
for (const key in attrs) {
if(key === 'on'){
continue
}
child._props[key] = attrs[key]
}
}
将 componentInstance 赋给新的 vnode,将父组件传递的 props 最新值赋给 _props,触发双向绑定中的 set 函数。
这样,component 从定义到转换成真实 dom 以及父组件向子组件传递 props 的功能就基本完成了。