写在前面
本篇是从零实现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