从零写一个 Vue(六)组件化

640 阅读2分钟

写在前面

本篇是从零实现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 的功能就基本完成了。

本篇代码:github.com/buppt/YourV…