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