新聞中心
1.寫在前面
2.props與組件的被動更新
props
在虛擬DOM中,組件的props和普通html標簽上的屬性差別并不大。

創(chuàng)新互聯(lián)建站專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務,包含不限于網(wǎng)站設計制作、做網(wǎng)站、鄂倫春網(wǎng)絡推廣、小程序開發(fā)、鄂倫春網(wǎng)絡營銷、鄂倫春企業(yè)策劃、鄂倫春品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運營等,從售前售中售后,我們都將竭誠為您服務,您的肯定,是我們最大的嘉獎;創(chuàng)新互聯(lián)建站為所有大學生創(chuàng)業(yè)者提供鄂倫春建站搭建服務,24小時服務熱線:18982081108,官方網(wǎng)址:www.cdcxhl.com
對應的虛擬DOM是:
const vnode = {
type: MyComponent,
props: {
name:"pingping",
age:18
}
}
對于組件而言:
const MyComponent = {
name:"MyComponent",
props:{
name:String,
age: Number
},
render(){
return {
type:"div",
children:`my name is ${this.name}, my age is: ${this.age}`
}
}
}
對于組件而言,需要關(guān)心的props內(nèi)容有兩部分:
- 為組件傳遞數(shù)據(jù)的props,即vnode.props對象
- 組件內(nèi)部選項自定義的props,即MyComponent.props
組件在渲染時解析props數(shù)據(jù)需要結(jié)合這兩個選項,最終解析出組件在渲染時需要使用到的props和attrs。
function mountComponent(vnode, container, anchor){
const componentOptions = vnode.type;
// 從組件選項中獲取到的props對象即propsOption
const { render, data,props: propsOption } = componentOptions;
// 在數(shù)據(jù)初始化前
beforeCreate && beforeCreate();
// 將原始數(shù)據(jù)對象data封裝成響應式數(shù)據(jù)
const state = reactive(data());
// 調(diào)用resolveProps 函數(shù)解析最終的props和attrs數(shù)據(jù)
const [props, attrs] = resolveProps(propsOptions, vnode.props);
// 組件實例
const instance = {
// 組件狀態(tài)數(shù)據(jù)
state,
// 組件掛載狀態(tài)
isMounted: false,
// 組件渲染內(nèi)容
subTree: null,
props: shallowReactive(props);
}
// 將組件實例設置在vnode上,方便后續(xù)更新
vnode.component = instance;
//... 代碼省略
}
再看看將props解析成最終的props和attrs的resolveProps函數(shù):
在上面代碼中,沒有定義在組件的props選項中的props數(shù)據(jù)將會被存儲在attrs對象中,實際上還需要對其進行默認值處理。
function resolveProps(options, propsData){
const props = {};
const attrs = {};
//遍歷為組件傳遞的props數(shù)據(jù)
for(const key in propsData){
// 鑒別是否為組件約定的props
if(key in options){
props[key] = propsData[key];
}else{
attrs[key] = propsData[key];
}
}
return [props, attrs]
}
組件的被動更新
其實,子組件的props數(shù)據(jù)本質(zhì)上就是來自于父組件傳遞的,在props發(fā)生變化時,會觸發(fā)父組件的重新渲染。
假定父組件初次要渲染的虛擬DOM:
const vnode = {
type: MyComponent,
props:{
name:"pingping",
age:18
}
}
在name或age的數(shù)據(jù)發(fā)生變化時,父組件的渲染函數(shù)會重新執(zhí)行,從而產(chǎn)生新的虛擬DOM:
const vnode = {
type: MyComponent,
props:{
name:"onechuan",
age:18
}
}
由于父組件要渲染的虛擬DOM內(nèi)容發(fā)生變化,此時就需要進行自更新,在更新時會使用patchComponent函數(shù)進行子組件的更新。
function patch(n1, n2, container, anchor){
if(n1 && n1.type !== n2.type){
unmount(n1);
n1 = null;
}
const {type} = n2;
if(typeof type === "string"){
//...普通元素
}else if(typeof type === Text){
//...文本節(jié)點
}else if(typeof type === Fragement){
//...片段
}else if(typeof type === "object"){
// vnode.type的值是選項對象,作為組件處理
if(!n1){
//掛載組件
mountComponent(n2, container, anchor);
}else{
//更新組件
patchComponent(n1, n2, anchor);
}
}
}
由父組件更新引起的子組件更新叫做子組件的被動更新,在子組件更新時需要檢測子組件是否真的需要更新,如果需要更新則更新子組件的props和slots等內(nèi)容。具體的patchComponent代碼如下所示:
function patchComponent(n1, n2, anchor){
//獲取組件實例,新舊組件實例是一樣的
const instance = (n2.component = n1.component);
const {props} = instance;
if(hasPropsChanged(n1.props, n2.props)){
const [nextProps] = resovleProps(n1.props, n2.props);
// 更新props
for(const k in nextProps){
props[k] = nextProps[k]
}
// 刪除不存在的props
for(const k in props){
if(!(k in nextProps)) delete props[k];
}
}
}
hasPropsChanged函數(shù)用于判斷新舊props內(nèi)容是否有改動,有改動則進行組件的更新。
function hasPropsChanged(prevProps, nextProps){
const nextKeys = Object.keys(nextProps);
cosnt prevKeys = Object.keys(prevProps);
// 新舊數(shù)量是否改變
if(nextKeys.length !== prevKeys.length){
return true
}
// 是否有不相等的props
for(let i = 0; i < nextKeys.length; i++){
const key = nextKeys[i];
if(nextProps[key] !== prevProps[key]) return true
}
return false
}
props和attrs本質(zhì)上都是根據(jù)組件的props選項定義和給組件傳遞的props數(shù)據(jù)進行處理的。但是由于props數(shù)據(jù)與組件本身的狀態(tài)數(shù)據(jù)都需要暴露到渲染函數(shù)中,渲染函數(shù)中可以通過this進行訪問,對此需要封裝一個渲染上下文對象。
function mountComponent(vnode, container, anchor){
// 省略代碼...
// 組件實例
const instance = {
state,
isMounted: false,
subTree: null,
props: shallowReactive(props);
}
// 創(chuàng)建渲染上下文對象,本質(zhì)上是組件實例的代理
const renderContext = new Proxy(instance, {
get(t, k, r){
// 獲取組件自身狀態(tài)和props數(shù)據(jù)
const {state, props} = t;
// 先嘗試讀取自身數(shù)據(jù)
if(state && k in state){
return state[k]
}else if(k in props){
return props[k]
}else{
console.log("不存在");
}
},
set(t, k, v, r){
const {state, props} = t;
if(state && k in state){
state[k] = v
}else if(k in props){
props[k] = v
}else{
console.log("不存在");
}
}
})
created && created.call(renderCOntext
//代碼省略...
}
在上面代碼中,通過為組件實例創(chuàng)建一個代理對象,即渲染上下文對象,對數(shù)據(jù)狀態(tài)攔截實現(xiàn)讀取和設置操作。在渲染函數(shù)或生命周期鉤子中可以通過this讀取數(shù)據(jù)時,會優(yōu)先從組件自身狀態(tài)中獲取,倘若組件自身沒有對應數(shù)據(jù),則從props數(shù)據(jù)中進行讀取。渲染上下文對象其實就是作為渲染函數(shù)和生命周期鉤子的this值。
當然,渲染上下文對象處理的不僅僅是組件自身的數(shù)據(jù)和props數(shù)據(jù),還包括:methods、computed等選項的數(shù)據(jù)和方法。
3.setup函數(shù)的作用與實現(xiàn)
組件的setup函數(shù)是Vue.js3新增的組件選項,主要用于配合組合式api進行建立組合邏輯、創(chuàng)建響應式數(shù)據(jù)、創(chuàng)建通用函數(shù)、注冊生命周期鉤子等能力。在組件的整個生命周期中,setup函數(shù)只會在被掛載時執(zhí)行一次,返回值可以是組件的渲染函數(shù)也可以是暴露出的響應式數(shù)據(jù)到渲染函數(shù)中。
const Comp = {
//setup函數(shù)可以返回一個函數(shù)作為組件的渲染函數(shù)
setup(){
return ()=>{
return {
type:"div",
children:"pingping"
}
}
}
}
但是,這種方式通常用于不是以模板來渲染內(nèi)容,如果組件是模板來渲染內(nèi)容,那么setup函數(shù)就不可以返回函數(shù),否則會與模板編譯的渲染函數(shù)沖突。
返回對象的情況,是將對象的數(shù)據(jù)暴露給模板使用,setup函數(shù)暴露的數(shù)據(jù)可以通過this進行訪問。
const Comp = {
props:{
name:String
},
//setup函數(shù)可以返回一個函數(shù)作為組件的渲染函數(shù)
setup(props, setupContext){
console.log(`my name is ${props.name}`);
const age = ref(18);
// setupContex包含與組件接口相關(guān)的重要數(shù)據(jù)
const {slots, emit, attrs} = setupContext;
return {
age
}
},
render(){
return {
type:"div",
children:`my age is ${this.age}`
}
}
}
那么setup函數(shù)是如何設計與實現(xiàn)的呢?
function mountComponent(vnode, container, anchor){
const componentOptions = vnode.type;
//從選項組件中取出setup函數(shù)
let {render, data, setup, /*...*/} = componentOptions;
beforeCreate && beforeCreate();
const state = data ? reactive(data()) : null;
const [props, attrs] = resolveProps(propsOption, vnode.props);
const instance = {
state,
props:shallowReactive(props),
isMounted:false,
subTree:null
}
const setupContext = { attrs };
const setupResult = setup(shallowReadonly(instance.props), setupContext);
// 存儲setup返回的數(shù)據(jù)
let setupState = null;
// 判斷setup返回的是函數(shù)還是數(shù)據(jù)對象
if(typeof setupResult === "function"){
// 報告沖突
if(render) console.error("setup函數(shù)返回渲染函數(shù),render選項可以忽略");
render = setupResult;
}else{
setupState = setupContext;
}
vnode.component = instance;
const renderContext = new Proxy(instance,{
get(t, k, r){
const {state, props} = t;
if(state && k in state){
return state[k];
}else if(k in props){
return props[k]
}else if(setupState && k in setupState){
return setupState[k]
}else{
console.log("不存在");
}
},
set(t, k, v, r){
const {state, props} = t;
if(state && k in state){
state[k] = v;
}else if(k in props){
props[k] = v;
}else if(setupState && k in setupState){
setupState[k] = v;
}else{
console.log("不存在");
}
}
})
//省略部分代碼...
}
4.組件事件與emit的實現(xiàn)
emit是用于父組件傳遞方法到子組件,是一個發(fā)射事件的自定義事件。
上面組件的虛擬DOM:
const CompVNode = {
type:MyComponent,
props:{
onChange:handler
}
}
const MyComponent = {
name:"MyComponent",
setup(props, {emit}){
emit("change", 1, 1)
return ()=>{
return //...
}
}
}
emit發(fā)射事件的本質(zhì)是:通過事件名稱去props對象數(shù)據(jù)中尋找對應的事件處理函數(shù)并執(zhí)行。
function mountComponent(vnode, container, anchor){
// 省略部分代碼
const instance = {
state,
props:shallowReactive(props),
isMounted:false,
subTree:null
}
function emit(event, ...payload){
// 如change -> onChange
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`;
// 根據(jù)處理后的事件名稱去props中尋找對應的事件處理函數(shù)
const handler = instance.props[eventName];
if(handler){
handler(...payload);
}else{
console.error("事件不存在")
}
}
const setupContext = { attrs, emit };
//省略部分代碼...
}
在上面代碼中,其實就是在setupContext對象中添加emit方法,在emit函數(shù)被調(diào)用時,根據(jù)約定對事件名稱便于在props數(shù)據(jù)對象中找到對應的事件處理函數(shù)。最終調(diào)用函數(shù)和傳遞參數(shù),在解析props數(shù)據(jù)時需要對事件類型的props進行處理。
function resolveProps(options, propsData){
const props = {};
const attrs = {};
for(const key in propsData){
if(key in options || key.startWith("on")){
props[key] = propsData[key]
}else{
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
5.插槽的工作原理與實現(xiàn)
插槽就是在組件中預留槽位,具體渲染內(nèi)容由用戶插入:
父組件中使用組件,通過插槽傳入自定義內(nèi)容:
我是標題
我是內(nèi)容
我是底部內(nèi)容
父組件的模板編譯成渲染函數(shù):
function render(){
return {
type:MyComponent,
children:{
hader(){
return {
type:"h1",
chidlren:"我是標題"
}
},
body(){
return {
type:"section",
chidlren:"我是內(nèi)容"
}
},
footer(){
return {
type:"p",
chidlren:"我是底部"
}
}
}
}
}
組件MyComponent模板編譯成渲染函數(shù):
function render(){
return [
{
type:"header",
chidlren:[this.$slots.header()]
},{
type:"body",
chidlren:[this.$slots.body()]
},{
type:"footer",
chidlren:[this.$slots.footer()]
}
]
}
在上面代碼中,看到渲染插槽內(nèi)容的過程,就是調(diào)用插槽函數(shù)比不過渲染返回內(nèi)容的過程。
function mountComponent(vnode, container, anchor){
// 省略代碼
const slots = vnode.children || {}
const setupContext = {attrs, emit, slots};
const instance = {
state,
props:shallowReactive(props),
isMounted:false,
subTree:null,
slots
}
const renderContext = new Proxy(instance,{
get(t, k, r){
const {state, props, slots} = t;
if(k === "$slots") return slots;
//省略部分代碼
},
set(t,k,v,r){
//省略部分代碼
}
//省略部分代碼
}
其實,slots的實現(xiàn)就是將編譯好的vnode.children作為slots對象,然后將slots對象添加到setupContext對象中。
6.注冊生命周期
在Vue.js3中,部分組合式api是用來注冊生命周期鉤子函數(shù)的,
新聞名稱:Vue.js設計與實現(xiàn)之組件的實現(xiàn)原理
文章分享:http://m.5511xx.com/article/cdiccpd.html


咨詢
建站咨詢
