引言
本期來實(shí)現(xiàn),setup里面使用props,父子組件通信props和emit等,所有的源碼請(qǐng)查看
本期的內(nèi)容與上一期的代碼具有聯(lián)動(dòng)性,所以需要明白本期的內(nèi)容,最后是先看下上期的內(nèi)容哦!
實(shí)現(xiàn)render中的this
在render函數(shù)中,可以通過this,來訪問setup返回的內(nèi)容,還可以訪問this.$el等
測(cè)試用例
由于是測(cè)試dom,jest需要提前注入下面的內(nèi)容,讓document里面有app節(jié)點(diǎn),下面測(cè)試用例類似在html中定義一個(gè)app節(jié)點(diǎn)哦
let appElement: Element; beforeEach(() => { appElement = document.createElement(‘p’); appElement.id = ‘app’; document.body.appendChild(appElement); }); afterEach(() => { document.body.innerHTML = ”; })復(fù)制代碼
本功能的測(cè)試用例正式開始
test(‘實(shí)現(xiàn)代理對(duì)象,通過this來訪問’, () => { let that; const app = createApp({ render() { // 在這里可以通過this來訪問 that = this; return h(‘p’, { class: ‘container’ }, this.name); }, setup() { return { name: ‘123’ } } }); const appDoc = document.querySelector(‘#app’) app.mount(appDoc); // 綁定值后的html expect(document.body.innerHTML).toBe(‘123’); const elDom = document.querySelector(‘#container’) // el就是當(dāng)前組件的真實(shí)dom expect(that.$el).toBe(elDom); })復(fù)制代碼
分析
上面的測(cè)試用例
解決這兩個(gè)需求:
編碼
針對(duì)上面的分析,需要在setupStatefulComponent中來創(chuàng)建proxy并且綁定到instance當(dāng)中,并且setup的執(zhí)行結(jié)果如果是對(duì)象,也已經(jīng)存在instance中了,可以通過instance.setupState來進(jìn)行獲取
function setupStatefulComponent(instance: any) { instance.proxy = new Proxy({}, { get(target, key){ // 判斷當(dāng)前的key是否存在于instance.setupState當(dāng)中 if(key in instance.setupState){ return instance.setupState[key] } } }) // …省略其他}// 然后在setupRenderEffect調(diào)用render的時(shí)候,改變當(dāng)前的this執(zhí)行,執(zhí)行為instance.proxyfunction setupRenderEffect(instance: any, vnode: any, container: any) { // 獲取到vnode的子組件,傳入proxy進(jìn)去 const { proxy } = instance const subtree = instance.render.call(proxy) // …省略其他}復(fù)制代碼
通過上面的操作,從render中this.xxx獲取setup返回對(duì)象的內(nèi)容就ok了,接下來處理el
需要在mountElement中,創(chuàng)建節(jié)點(diǎn)的時(shí)候,在vnode中綁定下,el,并且在setupStatefulComponent 中的代理對(duì)象中判斷當(dāng)前的key
// 代理對(duì)象進(jìn)行修改 instance.proxy = new Proxy({}, { get(target, key){ // 判斷當(dāng)前的key是否存在于instance.setupState當(dāng)中 if(key in instance.setupState){ return instance.setupState[key] }else if(key === ‘$el’){ return instance.vnode.el } } }) // mount中需要在vnode中綁定el function mountElement(vnode: any, container: any) { // 創(chuàng)建元素 const el = document.createElement(vnode.type) // 設(shè)置vnode的el vnode.el = el //…… 省略其他 }復(fù)制代碼
看似沒有問題吧,但是實(shí)際上是有問題的,請(qǐng)仔細(xì)思考一下,mountElement是不是比setupStatefulComponent 后執(zhí)行,setupStatefulComponent執(zhí)行的時(shí)候,vnode.el不存在,后續(xù)mountelement的時(shí)候,vnode就會(huì)有值,那么上面的測(cè)試用例肯定是報(bào)錯(cuò)的,$el為null
解決這個(gè)問題的關(guān)鍵,mountElement的加載順序是 render -> patch -> mountElement,并且render函數(shù)返回的subtree是一個(gè)vnode,改vnode中上面是mount的時(shí)候,已經(jīng)賦值好了el,所以在patch后執(zhí)行下操作
function setupRenderEffect(instance: any, vnode: any, container: any) { // 獲取到vnode的子組件,傳入proxy進(jìn)去 const { proxy } = instance const subtree = instance.render.call(proxy) patch(subtree, container) // 賦值vnode.el,上面執(zhí)行render的時(shí)候,vnode.el是null vnode.el = subtree.el}復(fù)制代碼
至此,上面的測(cè)試用例就能ok通過啦!
實(shí)現(xiàn)on+Event注冊(cè)事件
在vue中,可以使用onEvent來寫事件,那么這個(gè)功能是怎么實(shí)現(xiàn)的呢,咋們一起來看看
測(cè)試用例
test(‘測(cè)試on綁定事件’, () => { let count = 0 console.log = jest.fn() const app = createApp({ render() { return h(‘p’, { class: ‘container’, onClick() { console.log(‘click’) count++ }, onFocus() { count– console.log(1) } }, ‘123’); } }); const appDoc = document.querySelector(‘#app’) app.mount(appDoc); const container = document.querySelector(‘.container’) as HTMLElement; // 調(diào)用click事件 container.click(); expect(console.log).toHaveBeenCalledTimes(1) // 調(diào)用focus事件 container.focus(); expect(count).toBe(0) expect(console.log).toHaveBeenCalledTimes(2) })復(fù)制代碼
分析
在本功能的測(cè)試用例中,可以分析以下內(nèi)容:
解決問題:
這個(gè)功能比較簡(jiǎn)單,在處理prop中做個(gè)判斷, 屬性是否滿足 /^on[A-Z]/i這個(gè)格式,如果是這個(gè)格式,則進(jìn)行事件注冊(cè),但是vue3會(huì)做事件緩存,這個(gè)是怎么做到?
緩存也好實(shí)現(xiàn),在傳入當(dāng)前的el中增加一個(gè)屬性 el._vei || (el._vei = {}) 存在這里,則直接使用,不能存在則創(chuàng)建并且存入緩存
編碼
在mountElement中增加處理事件的邏輯 const { props } = vnode for (let key in props) { // 判斷key是否是on + 事件命,滿足條件需要注冊(cè)事件 const isOn = (p: string) => p.match(/^on[A-Z]/i) if (isOn(key)) { // 注冊(cè)事件 el.addEventListener(key.slice(2).toLowerCase(), props[key]) } // … 其他邏輯 el.setAttribute(key, props[key]) }復(fù)制代碼
事件處理就ok啦
父子組件通信——props
父子組件通信,在vue中是非常常見的,這里主要實(shí)現(xiàn)props與emit
測(cè)試用例
test(‘測(cè)試組件傳遞props’, () => { let tempProps; console.warn = jest.fn() const Foo = { name: ‘Foo’, render() { // 2. 組件render里面可以直接使用props里面的值 return h(‘p’, { class: ‘foo’ }, this.count); }, setup(props) { // 1. 此處可以拿到props tempProps = props; // 3. readonly props props.count++ } } const app = createApp({ name: ‘App’, render() { return h(‘p’, { class: ‘container’, }, [ h(Foo, { count: 1 }), h(‘span’, { class: ‘span’ }, ‘123’) ]); } }); const appDoc = document.querySelector(‘#app’) app.mount(appDoc); // 驗(yàn)證功能1 expect(tempProps.count).toBe(1) // 驗(yàn)證功能3,修改setup內(nèi)部的props需要報(bào)錯(cuò) expect(console.warn).toBeCalled() expect(tempProps.count).toBe(1) // 驗(yàn)證功能2,在render中可以直接使用this來訪問props里面的內(nèi)部屬性 expect(document.body.innerHTML).toBe(`1123`) })復(fù)制代碼
分析
根據(jù)上面的測(cè)試用例,分析props的以下內(nèi)容:
解決問題:
問題1: 想要在子組件的setup函數(shù)中第一個(gè)參數(shù),使用props,那么在setup函數(shù)調(diào)用的時(shí)候,把當(dāng)前組件的props傳入到setup函數(shù)中即可 問題2: render中this想要問題,則在上面的那個(gè)代理中,在加入一個(gè)判斷,key是否在當(dāng)前instance的props中 問題3: 修改報(bào)錯(cuò),那就是只能讀,可以使用以前實(shí)現(xiàn)的api shallowReadonly來包裹一下既可
編碼
1. 在setup函數(shù)調(diào)用的時(shí)候,傳入instance.props之前,需要在實(shí)例上掛載propsexport function setupComponent(instance) { // 獲取props和children const { props } = instance.vnode // 處理props instance.props = props || {} // ……省略其他 } //2. 在setup中進(jìn)行調(diào)用時(shí)作為參數(shù)賦值 function setupStatefulComponent(instance: any) { // ……省略其他 // 獲取組件的setup const { setup } = Component; if (setup) { // 執(zhí)行setup,并且獲取到setup的結(jié)果,把props使用shallowReadonly進(jìn)行包裹,則是只讀,不能修改 const setupResult = setup(shallowReadonly(instance.props)); // …… 省略其他 }}// 3. 在propxy中在加入判斷 instance.proxy = new Proxy({}, { get(target, key){ // 判斷當(dāng)前的key是否存在于instance.setupState當(dāng)中 if(key in instance.setupState){ return instance.setupState[key] }else if(key in instance.props){ return instance.props[key] }else if(key === ‘$el’){ return instance.vnode.el } } })復(fù)制代碼
做完之后,可以發(fā)現(xiàn)咋們的測(cè)試用例是運(yùn)行沒有毛病的
組件通信——emit
上面實(shí)現(xiàn)了props,那么emit也是少不了的,那么接下來就來實(shí)現(xiàn)下emit
測(cè)試用例
test(‘測(cè)試組件emit’, () => { let count; const Foo = { name: ‘Foo’, render() { return h(‘p’, { class: ‘foo’ }, this.count); }, setup(props, { emit }) { // 1. setup對(duì)象的第二個(gè)參數(shù)里面,可以結(jié)構(gòu)出emit,并且是一個(gè)函數(shù) // 2. emit 函數(shù)可以父組件傳過來的事件 emit(‘click’) // 驗(yàn)證emit1,可以執(zhí)行父組件的函數(shù) expect(count.value).toBe(2) // 3 emit 可以傳遞參數(shù) emit(‘clickNum’, 5) // 驗(yàn)證emit傳入?yún)?shù) expect(count.value).toBe(7) // 4 emit 可以使用—的模式 emit(‘click-num’, -5) expect(count.value).toBe(2) } } const app = createApp({ name: ‘App’, render() { return h(‘p’, {}, [ h(Foo, { onClick: this.click, onClickNum: this.clickNum, count: this.count }) ]) }, setup() { const click = () => { count.value++ } count = ref(1) const clickNum = (num) => { count.value = Number(count.value) + Number(num) } return { click, clickNum, count } } }) const appDoc = document.querySelector(‘#app’) app.mount(appDoc); // 驗(yàn)證掛載 expect(document.body.innerHTML).toBe(`1`) })復(fù)制代碼
分析
根據(jù)上面的測(cè)試用例,可以分析出:
解決辦法: 問題1: emit 是setup的第二個(gè)參數(shù),那么可以在setup函數(shù)調(diào)用的時(shí)候,傳入第二個(gè)參數(shù) 問題2: 關(guān)于emit的第一個(gè)參數(shù),可以做條件判斷,把xxx-xxx的形式轉(zhuǎn)成xxxXxx的形式,然后加入on,最后在props中取找,存在則調(diào)用,不存在則不調(diào)用 問題3:emit的第二個(gè)參數(shù),則使用剩余參數(shù)即可
編碼
// 1. 在setup函數(shù)執(zhí)行的時(shí)候,傳入第二個(gè)參數(shù) const setupResult = setup(shallowReadonly(instance.props), { emit: instance.emit });// 2. 在setup中傳入第二個(gè)參數(shù)的時(shí)候,還需要在實(shí)例上添加emit屬性哦export function createComponentInstance(vnode) { const instance = { // ……其他屬性 // emit函數(shù) emit: () => { }, } instance.emit = emit.bind(null, instance); function emit(instance, event, …args) { const { props } = instance // 判斷props里面是否有對(duì)應(yīng)的事件,有的話執(zhí)行,沒有就不執(zhí)行,處理emit的內(nèi)容,詳情請(qǐng)查看源碼 const key = handlerName(capitalize(camize(event))) const handler = props[key] handler && handler(…args) } return instance}復(fù)制代碼
到此就圓滿成功啦!