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

写在前面

本篇是从零实现vue2系列第六篇,将在 YourVue 中实现 component。从这篇开始实现的内容,博客上讨论的就比较少了,不过啃源码肯定要啃完整。

正文

将 main.js 中的内容一部分提取到组件 helloWorld 中,在 YourVue 实例上注册 helloWorld 组件。

 1const helloWorld = {
 2    data: {
 3        count: 0,
 4        items:[1,2,3,0,5],
 5    },
 6    props:['message'],
 7    template: `
 8        <div>
 9            array: {{items}}
10            <div>{{count}}</div>
11            <button @click="addCount">addCount</button>
12            <h4 style="color: red">{{message}}</h4>
13            <button @click="decCount">decCount</button>
14        </div>
15    `,
16    methods:{
17        addCount(){
18            this.count += 1
19            this.items.push(this.count)
20        },
21        decCount(){
22            this.count -= 1
23            this.items.pop()
24        }
25    }
26  }
27new YourVue({
28    el: '#app',
29    components:{ helloWorld },
30    data:{
31        message: "parentMessage"
32    },
33    template: `
34      <div>
35        <hello-world :message="message"></hello-world>
36        <button @click="change">parent button</button>
37      </div>
38    `,
39    methods:{
40        change(){
41            this.message = this.message.split('').reverse().join('')
42        }
43    }
44})

我们可以从流程上思考一下哪里发生了变化????

从 template -> ast -> gencode -> render 函数这个流程是没有变化的,只不过其中有了一个 tag 为 hello-world 的 VNode,所以需要在生成 VNode 的时候添加判断,是 HTML 标签还是自定义标签。

1function createElement (tag, data={}, children=[]){
2    children = simpleNormalizeChildren(children)
3    if(isHTMLtag(tag)){
4        return new VNode(tag, data, children, undefined, undefined)
5    }else{
6        return componentToVNode(tag, data, children, this)
7    }
8}

isHTMLtag 就是直接判断 tag 是否在所有 HTML 元素组成的列表里,如果不是 HTML 标签就执行 componentToVNode。

 1export function componentToVNode(tag, data, children, vm){
 2    if(tag.includes('-')){
 3        tag = toHump(tag)
 4    }
 5    const Ctor = YourVue.extend(vm.$options.components[tag])
 6    const name = tag
 7    data.hooks = {
 8        init(vnode){
 9            const child = vnode.componentInstance = new vnode.componentOptions.Ctor({
10                _isComponent: true,
11                _parentVnode: vnode
12            })
13            initProps(child, vnode.props.attrs)
14            child.$mount()
15        },
16        prepatch (oldVnode, vnode) {
17            const options = vnode.componentOptions
18            const child = vnode.componentInstance = oldVnode.componentInstance
19            const attrs = options.data.attrs;
20            for (const key in attrs) {
21                if(key === 'on'){
22                    continue
23                }
24                child._props[key] = attrs[key]
25            }
26        }
27    }
28    const listeners = data.on
29    const vnode = new VNode(
30        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
31        data, undefined, undefined, undefined, vm,
32        { Ctor, tag, data, listeners, children}
33    )
34    return vnode
35}

因为需要将组件定义的参数传入 YourVue 实例,所以定义 Ctor 继承 YourVue,先将组件参数作为 extendOptions 传入,在 Ctor 的构造函数中,将 extendOptions 和 options 融合作为 _init 的参数。并将 Ctor 缓存,再次使用该组件时候可以直接从缓存中读取该组件对应的 Ctor。

 1export default class YourVue{
 2    static extend(extendOptions){
 3        extendOptions = extendOptions || {}
 4        const Super = this
 5        const SuperId = Super.cid
 6        const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
 7        if (cachedCtors[SuperId]) {
 8            return cachedCtors[SuperId]
 9        }
10        const Sub = function VueComponent (options) {
11            this._init(mergeOptions(options,extendOptions))
12        }
13        Sub.prototype = Object.create(Super.prototype)
14        Sub.prototype.constructor = Sub
15        Sub.cid = cid++
16        Sub['super'] = Super
17        Sub.extend = Super.extend
18        cachedCtors[SuperId] = Sub
19        return Sub
20    }
21}

从 componentToVNode 最后可以看出来,返回的 VNode 的 tag 进行了重新命名,data 暂时有两个 hooks,其余参数都传入了 VNode 的最后一个参数 componentOptions 中。

1constructor(tag, data={}, children=[], text='', elm, context, componentOptions){
2    this.componentOptions = componentOptions
3}

VNode 创建好了,生成真实 dom 的时候就用到了 patch。在上篇文章中的 createElm 开始添加一个 createComponent 函数,在这个函数中会执行上面提到的 data.hooks.init。

 1function createElm (vnode, parentElm, afterElm = undefined) {
 2  if (createComponent(vnode, parentElm, afterElm)) {
 3    return
 4  }
 5  ...
 6}
 7function createComponent (vnode, parentElm, afterElm) {
 8  let i = vnode.props
 9  if (i) {
10    if (i.hooks&&i.hooks.init) {
11      i.hooks.init(vnode)
12    }
13    if (isDef(vnode.componentInstance)) {
14      vnode.elm = vnode.componentInstance.vnode.elm
15      if(isDef(afterElm)){
16        insertBefore(parentElm,vnode.elm,afterElm)
17      }else if(parentElm){
18        parentElm.appendChild(vnode.elm)
19      }
20      return true
21    }
22  }
23}

再返回来看 hooks.init,其中初始化了 Ctor,并传入两个参数标准 component 和记录父 VNode。最后执行 $mount 函数,生成真实 dom。可以从上面代码 vnode.elm = vnode.componentInstance.vnode.elm 发现,父组件中 hello-world component 渲染的 elm,就是子组件的真实 dom。

1init(vnode){
2    const child = vnode.componentInstance = new vnode.componentOptions.Ctor({
3        _isComponent: true,
4        _parentVnode: vnode
5    })
6    initProps(child, vnode.props.attrs)
7    child.$mount()
8}

initProps(child, vnode.props.attrs) 处理父组件传入子组件的 props,initProps 定义如下。

 1function initProps(vm, propsOptions){
 2    const props = vm._props = {}
 3    for (const key in propsOptions) {
 4        if(key === 'on'){
 5            continue
 6        }
 7        defineReactive(props, key, propsOptions[key])
 8        if (!(key in vm)) {
 9            proxy(vm, `_props`, key)
10        }
11    }
12}

将 propsOptions 传递来的变量通过响应式函数 defineReactive 修改 props 的 get 和 set 方法,实现发布订阅。然后通过 proxy 方法代理,这样就可以直接使用 this 来访问 props 了。

prepatch 钩子是在 patchVnode 中执行。

 1function patchVnode(oldVnode, vnode){
 2  if (oldVnode === vnode) {
 3    return
 4  }
 5  let i
 6  const data = vnode.props
 7  if (isDef(data) && isDef(i = data.hooks) && isDef(i = i.prepatch)) {
 8    i(oldVnode, vnode)
 9  }
10  ...
11}
12
13prepatch (oldVnode, vnode) {
14    const options = vnode.componentOptions
15    const child = vnode.componentInstance = oldVnode.componentInstance
16    const attrs = options.data.attrs;
17    for (const key in attrs) {
18        if(key === 'on'){
19            continue
20        }
21        child._props[key] = attrs[key]
22    }
23}

将 componentInstance 赋给新的 vnode,将父组件传递的 props 最新值赋给 _props,触发双向绑定中的 set 函数。

这样,component 从定义到转换成真实 dom 以及父组件向子组件传递 props 的功能就基本完成了。

本篇代码:https://github.com/buppt/YourVue/tree/master/oldSrc/5.components

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值