Vue设计与实现:组件的实现原理

组件标识

vnode的type是一个对象就说明是组件,组件是对页面内容的封装,是用来描述页面内容的一部分。因此,一个组件必须包含一个render函数,并且渲染函数的返回值应该是vnode

js 复制代码
const MyComponent = {
 // 组件名称,可选
 name: 'MyComponent',
 // 组件的渲染函数,其返回值必须为虚拟 DOM
 render() {
   // 返回虚拟 DOM
   return {
     type: 'div',
     children: `我是文本内容`
   }
 }
}

// 该 vnode 用来描述组件,type 属性存储组件的选项对象
const CompVNode = {
 type: MyComponent
 // ...
}

组件渲染

在patch添加判断type是组件的逻辑,如果没有oldN就挂载组件

js 复制代码
function patch(oldN, newN, container, anchor) {
  if (oldN && oldN.type !== newN.type) {
    unmount(oldN)
    oldN = null
  }

  const { type } = newN

  if (typeof type === 'string') {
    // 作为普通元素处理
  } else if (type === Text) {
    // 作为文本节点处理
  } else if (type === Fragment) {
    // 作为片段处理
  } else if (typeof type === 'object') {
    // vnode.type 的值是选项对象,作为组件来处理
    if (!oldN) {
      // 挂载组件
      mountComponent(newN, container, anchor)
    } 
  }

思路:

  1. 通过vnode.type拿到组件的选项
  2. 因为组件的render是返回页面的vdom,调用patch挂载真实dom
js 复制代码
function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type
  // 获取组件的渲染函数 render
  const { render } = componentOptions
  // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
  const subTree = render()
  // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
  patch(null, subTree, container, anchor)
}

结果:

组件data与更新

在组件定义data函数,同时在render函数中通过this访问由data函数返回的状态数据

js 复制代码
const MyComponent = {
  name: 'MyComponent',
  // 用 data 函数来定义组件自身的状态
  data() {
    return {
      foo: 'hello world'
    }
  },
  render() {
    return {
      type: 'div',
      children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态
    }
  }
}

思路:

  1. 通过componentOptions拿到组件的data函数并执行拿到数据
  2. 把拿到的数据传入reactive得到响应数据
  3. 因为需要修改页面数据后重新执行render函数,所以将render跟patch传入effect,effect一开始会自执行一次,render再通过call将this执行state响应数据,这样render的this.foo就变成state.foo
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type
  // 获取组件的渲染函数 render
-  const { render } = componentOptions
+  const { render, data } = componentOptions
+  // 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
+  const state = reactive(data())
-  // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
-  const subTree = render()
-  // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
-  patch(null, subTree, container, anchor)
+  // 将组件的 render 函数调用包装到 effect 内
+  effect(() => {
+    // 调用 render 函数时,将其 this 设置为 state,
+    // 从而 render 函数内部可以通过 this 访问组件自身状态数据
+    const subTree = render.call(state, state)
+    // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
+    patch(null, subTree, container, anchor)
+  })
}

异步更新

由于effect的执行是同步的,因此当响应式数据发生变化时,与之关联的副作用函数会同步执行,所以需要无论对响应式数据进行多少次修改,副作用函数都只会重新执行一次

思路:

  1. 定义存放任务set队列、是否正在刷新队列标识、微任务变量
  2. queueJob函数作为调度器,当修改响应数据时,如果effect函数第二个参数有传scheduler参数就会将effect队列中的effect依次传给scheduler
  3. 当多个effect传给queueJob会保存到任务set队列,isFlushing第一次为false,把isFlushing设置为true、执行微任务的then函数,后续执行queueJob只会执行queue.add(job),当数据修改完成后,执行微任务队列中的 queue.forEach(job => job()),此时queue是多次添加后的数据
js 复制代码
// 任务缓存队列,用一个 Set 数据结构来表示,这样就可以自动对任务进行去重
const queue = new Set()
// 一个标志,代表是否正在刷新任务队列
let isFlushing = false
// 创建一个立即 resolve 的 Promise 实例
const p = Promise.resolve()

// 调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
function queueJob(job) {
  // 将 job 添加到任务队列 queue 中
  queue.add(job)
  // 如果还没有开始刷新队列,则刷新之
  if (!isFlushing) {
    // 将该标志设置为 true 以避免重复刷新
    isFlushing = true
    // 在微任务中刷新缓冲队列
    p.then(() => {
      try {
        // 执行任务队列中的任务
        queue.forEach(job => job())
      } finally {
        // 重置状态
        isFlushing = false
        queue.clear = 0
      }
    })

  }
}
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type
  // 获取组件的渲染函数 render
  const { render, data } = componentOptions
  // 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
  const state = reactive(data())
  // 将组件的 render 函数调用包装到 effect 内
  effect(() => {
    // 调用 render 函数时,将其 this 设置为 state,
    // 从而 render 函数内部可以通过 this 访问组件自身状态数据
    const subTree = render.call(state, state)
    // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
    patch(null, subTree, container, anchor)
  },
+  {
+    // 指定该副作用函数的调度器为 queueJob 即可
+    scheduler: queueJob
+  })
}

组件实例

组件实例本质上就是一个状态集合(或一个对象),它维护着组件运行过程中的所有信息,例如注册到组件的生命周期函数、组件渲染的子树(subTree)、组件是否已经被挂载、组件自身的状态 (data)

思路:

  1. 在挂载组件时定义instance对象,instance中有state(组件自身的状态数据)、isMounted(组件是否已经被挂载)、subTree(组件所渲染的内容)
  2. 将instance设置在vnode的component上
  3. 在effect中判断isMounted,为false时,patch函数第一个参数传null,并将isMounted设置为true,这样当更新发生时就不会再次进行挂载操作
  4. 为true时,patch函数第一个参数传instance.subTree也就是上次vnode,与新的vnode进行比较更新
  5. 最终将subTree赋值给instance.subTree
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type
  // 获取组件的渲染函数 render
  const { render, data } = componentOptions
  // 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
  const state = reactive(data())

+  // 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
+  const instance = {
+    // 组件自身的状态数据,即 data
+    state,
+    // 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
+    isMounted: false,
+    // 组件所渲染的内容,即子树(subTree)
+    subTree: null
+  }

+  // 将组件实例设置到 vnode 上,用于后续更新
+  vnode.component = instance

  // 将组件的 render 函数调用包装到 effect 内
  effect(() => {
    // 调用 render 函数时,将其 this 设置为 state,
    // 从而 render 函数内部可以通过 this 访问组件自身状态数据
    const subTree = render.call(state, state)
+    if (!instance.isMounted) {
+      // 初次挂载,调用 patch 函数第一个参数传递 null
+      patch(null, subTree, container, anchor)
+      // 重点:将组件实例的 isMounted 设置为 true,这样当更新发生时就不会再次进行挂载操作,
+      // 而是会执行更新
+      instance.isMounted = true
+    } else {
+      // 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
+      // 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
+      // 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
+      patch(instance.subTree, subTree, container, anchor)
+    }
+    // 更新组件实例的子树
+    instance.subTree = subTree
  }, {
    // 指定该副作用函数的调度器为 queueJob 即可
    scheduler: queueJob
  })
}

组件生命周期

思路:

  1. 在组件选项拿到生命周期函数
  2. 在data、instance之前调用beforeCreate,所以beforeCreate拿不到data的数据与组件实例
  3. 在data、instance后,执行effect函数前,执行created并将this指向state,因为create函数会用到this
  4. instance.isMounted为false时说明是要挂载,所以在patch之前执行beforeMount,并将this指向state
  5. instance.isMounted为true时说明是要更新,所以在patch之前执行updated,并将this指向state
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type
+  // 从组件选项对象中取得组件的生命周期函数
+  const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions

+  // 在这里调用 beforeCreate 钩子
+  beforeCreate && beforeCreate()

  // 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
  const state = reactive(data())

  // 定义组件实例,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
  const instance = {
    // 组件自身的状态数据,即 data
    state,
    // 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
    isMounted: false,
    // 组件所渲染的内容,即子树(subTree)
    subTree: null
  }

  // 将组件实例设置到 vnode 上,用于后续更新
  vnode.component = instance

+  // 在这里调用 created 钩子
+  created && created.call(state)

  // 将组件的 render 函数调用包装到 effect 内
  effect(() => {
    // 调用 render 函数时,将其 this 设置为 state,
    // 从而 render 函数内部可以通过 this 访问组件自身状态数据
    const subTree = render.call(state, state)
    console.log(subTree)
    if (!instance.isMounted) {
+      // 在这里调用 beforeMount 钩子
+      beforeMount && beforeMount.call(state)

      // 初次挂载,调用 patch 函数第一个参数传递 null
      patch(null, subTree, container, anchor)
      // 重点:将组件实例的 isMounted 设置为 true,这样当更新发生时就不会再次进行挂载操作,
      // 而是会执行更新
      instance.isMounted = true
+      // 在这里调用 mounted 钩子
+      mounted && mounted.call(state)
    } else {
+      // 在这里调用 beforeUpdate 钩子
+      beforeUpdate && beforeUpdate.call(state)
      // 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
      // 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
      // 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
      patch(instance.subTree, subTree, container, anchor)
+      // 在这里调用 updated 钩子
+      updated && updated.call(state)
    }
    // 更新组件实例的子树
    instance.subTree = subTree
  }, {
    // 指定该副作用函数的调度器为 queueJob 即可
    scheduler: queueJob
  })
}

结果:

组件props

对于一个组件来说,有两部分关于 props 的内容我们需要关心:

  1. 为组件传递的 props 数据,即组件的 vnode.props 对象;
  2. 组件选项对象中定义的 props 选项,即 MyComponent.props 对象。
js 复制代码
const MyComponent = {
  name: 'MyComponent',
  // 组件接收名为 title 的 props,并且该 props 的类型为 String
  props: {
    title: String
  },
  render() {
    return {
      type: 'div',
      children: `count is: ${this.title}` // 访问 props 数据
    }
  }
}

const vnode = {
  type: MyComponent,
  props: {
    title: 'A big Title',
    other: this.val
  }
}

思路:

  1. 拿到props,转换成浅响应数据定义在组件实例上
diff 复制代码
function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type
+  // 从组件选项对象中取出 props 定义,即 propsOption
+  const { render, data, props: propsOption /* 其他省略 */ } = componentOptions

  beforeCreate && beforeCreate()

  const state = reactive(data())
+  // 调用 resolveProps 函数解析出最终的 props 数据与 attrs 数据
+  const [props, attrs] = resolveProps(propsOption, vnode.props)

  const instance = {
    state,
+    // 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
+    props: shallowReactive(props),
    isMounted: false,
    subTree: null
  }
  vnode.component = instance

  // 省略部分代码
}
  1. 遍历vnode的prop,再判断vnode中的props是否在组件的props中
  2. 如果在组件的props中就说明是合法的props,存到props对象中
  3. 否则就存到attrs对象中
js 复制代码
// resolveProps 函数用于解析组件 props 和 attrs 数据
function resolveProps(options, propsData) {
  const props = {}
  const attrs = {}
  // 遍历为组件传递的 props 数据
  for (const key in propsData) {
    if (key in options) {
      // 如果为组件传递的 props 数据在组件自身的 props 选项中有定义,则将其视为合法的 props
      props[key] = propsData[key]
    } else {
      // 否则将其作为 attrs
      attrs[key] = propsData[key]
    }
  }
  // 最后返回 props 与 attrs 数据
  return [props, attrs]
}

父组件props修改

当vnode的props发生改变时,需要做的是:

  1. 检测子组件是否真的需要更新,因为子组件的 props 可能是不变的
  2. 如果需要更新,则更新子组件的 props、slots 等内容
js 复制代码
// 父组件要渲染的内容
const vnode = {
  type: MyComponent,
  props: {
    title: 'A Big Title'
  }
}
// 父组件要渲染的内容
const vnode = {
  type: MyComponent,
  props: {
    title: 'A Small Title'
  }
}

思路:

当组件发生改变会执行patchComponent

diff 复制代码
function patch(oldN, newN, container, anchor) {
    else if (typeof type === 'object') {
      // 如果 newN.type 的值的类型是对象,则它描述的是组件
      if (!oldN) {
        mountComponent(newN, container, anchor)
+      } else {
+        patchComponent(oldN, newN, anchor)
+      }
    } 
  }
  1. 将旧vnode的实例赋值给新vnode
  2. 比较新旧vnode的props,如果需要更新就resolveProps,比较新组件定义的props跟父组件传入的props,获取到新的合法props
  3. 遍历新的合法props更新旧vnode的props
  4. 遍历旧vnode的props,判断是否在新vnode的props中,不存在说明不存在,需要删除
js 复制代码
function patchComponent(oldN, newN, anchor) {
    // 获取组件实例,即 oldN.component,同时让新的组件虚拟节点 newN.component也指向组件实例
    const instance = newN.component = oldN.component
    // 获取当前的 props 数据
    const { props } = instance
    // 调用 hasPropsChanged 检测为子组件传递的 props 是否发生变化,如果没有变化,则不需要更新
    if (hasPropsChanged(oldN.props, newN.props)) {
      // 调用 resolveProps 函数重新获取 props 数据
      const [nextProps] = resolveProps(newN.type.props, newN.props)
      // 更新 props
      for (const k in nextProps) {
        props[k] = nextProps[k]
      }
      // 删除不存在的 props
      for (const k in props) {
        if (!(k in nextProps)) delete props[k]
      }
    }
  }
  1. 比较组件新props与组件旧props的长度,不一样说明需要更新props,返回true
  2. 组件新旧props长度一样,但值不一样说明需要更新,返回true
  3. 最终返回false,不需要更新
js 复制代码
function hasPropsChanged(prevProps, nextProps) {
    const nextKeys = Object.keys(nextProps)
    const prevKeys = Object.keys(prevProps)
    // 如果新旧 props 的数量变了,则说明有变化
    if (nextKeys.length !== prevKeys.length) return true
    for (let i = 0; i < nextKeys.length; i++) {
      const key = nextKeys[i]
      if (nextProps[key] !== prevProps[key]) return true
    }
    return false
  }
  1. 给instance做一层proxy代理renderContext
  2. 生命周期的this指向renderContext
  3. 当访问的数据不在state中,就尝试访问props
  4. 当修改数据在state中,就修改state,如果在props中就报错,props是只读的
js 复制代码
function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null
  }

  vnode.component = instance

  // 创建渲染上下文对象,本质上是组件实例的代理
  const renderContext = new Proxy(instance, {
      get(target, key, receiver) {
        // 取得组件自身状态与 props 数据
        const { state, props } = target
        if (state && key in state) {
          return state[key]
        } else if (key in props) { //// 如果组件自身没有该数据,则尝试从props 中读取
          return props[key]
        } else {
          console.error('不存在')
        }
      },
      set(target, key, value, receiver) {
        const { state, props } = target
        if (state && key in state) {
          state[key] = value
        } else if (key in props) {
          console.warn(`Attempting to mutate prop "${k}". Propsare readonly.`)
        } else {
          console.error('不存在')
        }

      }
    })

  // 生命周期函数调用时要绑定渲染上下文对象
  created && created.call(renderContext)

  // 省略部分代码
}

结果:

js 复制代码
const vnode = {
  type: MyComponent,
  props: {
    title: 'A Big Title'
  }
}
const vnode1 = {
  type: MyComponent,
  props: {
    title: 'A Small Title'
  }
}

renderer.render(vnode, document.querySelector('#app'))
setTimeout(() => {
  renderer.render(vnode1, document.querySelector('#app'))
},1000)
  1. 组件挂载执行mountComponent
  2. 将render的this指向renderContext,在render中数据都从renderContext访问
  3. 组件更新执行patchComponent
  4. instance的props是代理对象,当props的key被修改会触发effect,此时effect中instance.isMounted为true执行patch重新渲染

setup 函数的作用与实现

返回一个函数

该函数将作为组件的 render 函数

js 复制代码
const Comp = {
  setup() {
    // setup 函数可以返回一个函数,该函数将作为组件的渲染函数
    return () => {
      return { type: 'div', children: 'hello' }
    }
  }
}

返回一个对象

该对象中包含的数据将暴露给模板使用

js 复制代码
const Comp = {
  setup() {
    const count = ref(0)
    // 返回一个对象,对象中的数据会暴露到渲染函数中
    return {
      count
    }
  },
  render() {
    // 通过 this 可以访问 setup 暴露出来的响应式数据
    return { type: 'div', children: `count is: ${this.count}` }
  }
}

setup 函数接收两个参数

第一个参数取得外部为组件传递的 props 数据对象,第二个参数是组件接口相关的数据和方法

js 复制代码
const Comp = {
  props: {
    foo: String
  },
  setup(props, setupContext) {
    props.foo // 访问传入的 props 数据
    // setupContext 中包含与组件接口相关的重要数据
    const { slots, emit, attrs, expose } = setupContext
    // ...
  }
}

思路:

  1. 首先从组件选项拿到setup
  2. setup第一个参数是通过resolveProps函数解析出最终的props并且只读,第二个参数是setupContext对象,暂时只有attrs
  3. setup函数返回值有对象与函数两种情况,如果是函数并且组件选项的render不存在就赋值给render作为渲染函数,如果是对象就作为数据
  4. 因为setup函数的返回值是对象作为数据的情况,需要在renderContext的get中判断,如果setupState存在并且key在setupState上,就返回setupState[k],在set中判断,如果setupState存在并且key在setupState上,就将最新的值赋值给setupState[key]
diff 复制代码
function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type
+  // 从组件选项中取出 setup 函数
+  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
  }

+  // setupContext,由于我们还没有讲解 emit 和 slots,所以暂时只需要attrs
+  const setupContext = { attrs }
+  // 调用 setup 函数,将只读版本的 props 作为第一个参数传递,避免用户意外地修改 props 的值,
+  // 将 setupContext 作为第二个参数传递
+  const setupResult = setup(shallowReadonly(instance.props), setupContext)
+  // setupState 用来存储由 setup 返回的数据
+  let setupState = null
+  // 如果 setup 函数的返回值是函数,则将其作为渲染函数
+  if (typeof setupResult === 'function') {
+    // 报告冲突
+    if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
+    // 将 setupResult 作为渲染函数
+    render = setupResult
+  } else {
+    // 如果 setup 的返回值不是函数,则作为数据状态赋值给 setupState
+    setupState = setupResult
+  }

  vnode.component = instance

  const renderContext = new Proxy(instance, {
    get(target, key, receiver) {
      // 取得组件自身状态与 props 数据
      const { state, props } = target
      if (state && key in state) {
        return state[key]
      } else if (key in props) { //// 如果组件自身没有该数据,则尝试从props 中读取
        return props[key]
+      } else if (setupState && key in setupState) {
+        // 渲染上下文需要增加对 setupState 的支持
+        return setupState[key]
+      } else {
        console.error('不存在')
      }
    },
    set(target, key, value, receiver) {
      const { state, props } = target
      if (state && key in state) {
        state[key] = value
      } else if (key in props) {
        console.warn(`Attempting to mutate prop "${key}". Propsare readonly.`)
+      } else if (setupState && key in setupState) {
+        // 渲染上下文需要增加对 setupState 的支持
+        setupState[key] = value
+      } else {
        console.error('不存在')
      }

    }
  })

  // 省略部分代码
}

结果:

  1. setup返回一个函数
js 复制代码
const Comp = {
  setup() {
    // setup 函数可以返回一个函数,该函数将作为组件的渲染函数
    return () => {
      return { type: 'div', children: 'hello' }
    }
  }
}
// 父组件要渲染的内容
const vnode = {
  type: Comp,
}

renderer.render(vnode, document.querySelector('#app'))

2. setup返回一个对象

js 复制代码
const Comp = {
  setup() {
    // 返回一个对象,对象中的数据会暴露到渲染函数中
    return {
      count: 1
    }
  },
  render() {
    // 通过 this 可以访问 setup 暴露出来的响应式数据
    return { type: 'div', children: `count is: ${this.count}` }
  }
}
// 父组件要渲染的内容
const vnode = {
  type: Comp,
}

renderer.render(vnode, document.querySelector('#app'))

3. setup函数参数

js 复制代码
const Comp = {
  props: {
    title: String
  },
  setup(props, setupContext) {
    return () => ({ type: 'div', children: `title is: ${props.title}` })
  }
}
// 父组件要渲染的内容
const vnode = {
  type: Comp,
  props: {
    title: 'A Big Title'
  }
}

renderer.render(vnode, document.querySelector('#app'))

组件事件与 emit 的实现

在组件通过@注入自定义事件

html 复制代码
<MyComponent @change="handler" />

对应的vnode

js 复制代码
const CompVNode = {
  type: MyComponent,
  props: {
    onChange: handler
  }
}

在组件setup用emit发射事件

js 复制代码
const MyComponent = {
  name: 'MyComponent',
  setup(props, { emit }) {
    // 发射 change 事件,并传递给事件处理函数两个参数
    emit('change', 1, 2)

    return () => {
      return // ...
    }
  }
}

思路:

  1. emit函数第一个参数是自定义事件的名称字符串,将名称转换成原生的事件名称格式
  2. 第二个参数是传递的参数使用...收集,根据转换后的事件名称去props中寻找对应的事件处理函数,事件处理函数存在就将payload传入
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null
  }

+  // 定义 emit 函数,它接收两个参数
+  // event: 事件名称
+  // payload: 传递给事件处理函数的参数
+  function emit(event, ...payload) {
+    // 根据约定对事件名称进行处理,例如 change --> onChange
+    const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
+    // 根据处理后的事件名称去 props 中寻找对应的事件处理函数
+    const handler = instance.props[eventName]
+    if (handler) {
+      // 调用事件处理函数并传递参数
+      handler(...payload)
+    } else {
+      console.error('事件不存在')
+    }
+  }

+  // 将 emit 函数添加到 setupContext 中,用户可以通过 setupContext 取得 emit 函数
-  const setupContext = { attrs }
+  const setupContext = { attrs, emit }

  // 省略部分代码
}

将on开头的自定义事件也放入props对象中

diff 复制代码
function resolveProps(options, propsData) {
  const props = {}
  const attrs = {}
  for (const key in propsData) {
-    if (key in options) {
+    // 以字符串 on 开头的 props,无论是否显式地声明,都将其添加到 props数据中,而不是添加到 attrs 中
+    if (key in options || key.startsWith('on')) {
      props[key] = propsData[key]
    } else {
      attrs[key] = propsData[key]
    }
  }

  return [props, attrs]
}

结果:

js 复制代码
const MyComponent = {
  name: 'MyComponent',
  props: {
    title: String
  },
  setup(props, { emit }) {
    // 发射 change 事件,并传递给事件处理函数两个参数
    emit('input', 1, 2)

    return () => {
      return {
        type: 'div',
        children: `count is: ${props.title}` // 访问 props 数据
      }
    }
  }
}

function handler(...num) {
  console.log(num)
}
// 父组件要渲染的内容
const vnode = {
  type: MyComponent,
  props: {
    title: 'A Big Title',
    onInput: handler
  }
}

renderer.render(vnode, document.querySelector('#app'))

插槽的工作原理与实现

MyComponent 组件

html 复制代码
<template>
  <header><slot name="header" /></header>
  <div>
  <slot name="body" />
  </div>
  <footer><slot name="footer" /></footer>
</template>

MyComponent 组件对应的render函数

js 复制代码
function render() {
  return [
    {
      type: 'header',
      children: [this.$slots.header()]
    },
    {
      type: 'body',
      children: [this.$slots.body()]
    },
    {
      type: 'footer',
      children: [this.$slots.footer()]
    }
  ]
}

父组件

html 复制代码
<MyComponent>
  <template #header>
  <h1>我是标题</h1>
  </template>
  <template #body>
  <section>我是内容</section>
  </template>
  <template #footer>
  <p>我是注脚</p>
  </template>
</MyComponent>

父组件对应的render函数,父组件的children是一个对象,key为插槽名称,value为插槽渲染内容,方便后续子组件通过this.$slots访问到

js 复制代码
// 父组件的渲染函数
function render() {
  return {
    type: MyComponent,
    // 组件的 children 会被编译成一个对象
    children: {
      header() {
        return { type: 'h1', children: '我是标题' }
      },
      body() {
        return { type: 'section', children: '我是内容' }
      },
      footer() {
        return { type: 'p', children: '我是注脚' }
      }
    }
  }
}

思路:

  1. 父组件插槽的内容放在children,通过vnode.children获得插槽内容
  2. 将插槽内容添加setupContext,方便在setup函数拿到
  3. 在执行组件render时,通过访问this.$slots指向父组件的children
diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 省略部分代码
+  // 直接使用编译好的 vnode.children 对象作为 slots 对象即可
+  const slots = vnode.children || {}

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null,
    // 将插槽添加到组件实例上
    slots
  }

+  // 将 slots 对象添加到 setupContext 中
+  const setupContext = { attrs, emit, slots }

  const renderContext = new Proxy(instance, {
    get(t, k, r) {
      const { state, props, slots } = t
      // 当 k 的值为 $slots 时,直接返回组件实例上的 slots
+      if (k === '$slots') return slots

      // 省略部分代码
    },
    set(t, k, v, r) {
      // 省略部分代码
    }
  })

  // 省略部分代码
}

结果:

  1. 在setup函数成功拿到slots对象
js 复制代码
const MyComponent = {
  name: 'MyComponent',
  props: {
    title: String
  },
  setup(props, { emit, slots }) {
    return () => {
      return {
        type: Fragment,
        children: [
          {
            type: 'header',
            children: [slots.header()]
          },
          {
            type: 'body',
            children: [slots.body()]
          },
          {
            type: 'footer',
            children: [slots.footer()]
          }
        ]
      }
    }
  },
}

// 父组件要渲染的内容
const vnode = {
  type: MyComponent,
  // 组件的 children 会被编译成一个对象
  children: {
    header() {
      return { type: 'h1', children: '我是标题' }
    },
    body() {
      return { type: 'section', children: '我是内容' }
    },
    footer() {
      return { type: 'p', children: '我是注脚' }
    }
  }
}

renderer.render(vnode, document.querySelector('#app'))

2. 在子组件的render成功拿到this.$slots

js 复制代码
const MyComponent = {
  name: 'MyComponent',
  props: {
    title: String
  },
  render() {
    return {
      type:Fragment,
      children:[
      {
        type: 'header',
        children: [this.$slots.header()]
      },
      {
        type: 'body',
        children: [this.$slots.body()]
      },
      {
        type: 'footer',
        children: [this.$slots.footer()]
      }
    ]
    }
  }
}

// 父组件要渲染的内容
const vnode = {
  type: MyComponent,
  // 组件的 children 会被编译成一个对象
  children: {
    header() {
      return { type: 'h1', children: '我是标题' }
    },
    body() {
      return { type: 'section', children: '我是内容' }
    },
    footer() {
      return { type: 'p', children: '我是注脚' }
    }
  }
}

renderer.render(vnode, document.querySelector('#app'))

注册生命周期

setup函数中可以注册多个生命周期函数

js 复制代码
const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted 1')
    })
    // 可以注册多个
    onMounted(() => {
      console.log('mounted 2')
    })
    // ...
  }
}

思路:

生命周期是外部定义,在setup函数中调用的函数,如果多个组件一起调用生命周期函数会分不清,需要定义当前组件实例,在调用setup前保存当前组件,调用后重置null

js 复制代码
// 全局变量,存储当前正在被初始化的组件实例
let currentInstance = null
// 该方法接收组件实例作为参数,并将该实例设置为 currentInstance
function setCurrentInstance(instance) {
  currentInstance = instance
}

onMounted接受一个函数参数,调用onMounted就将函数参数传入当前组件实例的mounted数组

js 复制代码
function onMounted(fn) {
  if (currentInstance) {
    // 将生命周期函数添加到 instance.mounted 数组中
    currentInstance.mounted.push(fn)
  } else {
    console.error('onMounted 函数只能在 setup 中调用')
  }
}

在effect中遍历instance的mounted

diff 复制代码
function mountComponent(vnode, container, anchor) {
  // 省略部分代码

  const instance = {
    state,
    props: shallowReactive(props),
    isMounted: false,
    subTree: null,
    slots,
+    // 在组件实例中添加 mounted 数组,用来存储通过 onMounted 函数注册的生命周期钩子函数
+    mounted: []
  }

  // 省略部分代码

  // setup
  const setupContext = { attrs, emit, slots }

+  // 在调用 setup 函数之前,设置当前组件实例
+  setCurrentInstance(instance)
  // 执行 setup 函数
  const setupResult = setup(shallowReadonly(instance.props), setupContext)
+  // 在 setup 函数执行完毕之后,重置当前组件实例
+  setCurrentInstance(null)

    // 省略部分代码
    effect(() => {
    const subTree = render.call(renderContext, renderContext)
    if (!instance.isMounted) {
      // 省略部分代码
+    // 遍历 instance.mounted 数组并逐个执行即可
+    instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))
    } else {
      // 省略部分代码
    }
    instance.subTree = subTree
  }, {
    scheduler: queueJob
  })
}

结果:

js 复制代码
const MyComponent = {
  name: 'MyComponent',
  props: {
    title: String
  },
  setup(props, { emit, slots }) {
    onMounted(() => {
      console.log('mounted 1')
    })
    // 可以注册多个
    onMounted(() => {
      console.log('mounted 2')
    })
  },
  render() {
    return {
      type: Fragment,
      children: [
        {
          type: 'header',
          children: [this.$slots.header()]
        },
        {
          type: 'body',
          children: [this.$slots.body()]
        },
        {
          type: 'footer',
          children: [this.$slots.footer()]
        }
      ]
    }
  }
}

// 父组件要渲染的内容
const vnode = {
  type: MyComponent,
  // 组件的 children 会被编译成一个对象
  children: {
    header() {
      return { type: 'h1', children: '我是标题' }
    },
    body() {
      return { type: 'section', children: '我是内容' }
    },
    footer() {
      return { type: 'p', children: '我是注脚' }
    }
  }
}

renderer.render(vnode, document.querySelector('#app'))
相关推荐
猿饵块18 分钟前
cmake--get_filename_component
java·前端·c++
大表哥629 分钟前
在react中 使用redux
前端·react.js·前端框架
十月ooOO34 分钟前
【解决】chrome 谷歌浏览器,鼠标点击任何区域都是 Input 输入框的状态,能看到输入的光标
前端·chrome·计算机外设
qq_3391911434 分钟前
spring boot admin集成,springboot2.x集成监控
java·前端·spring boot
pan_junbiao42 分钟前
Vue使用代理方式解决跨域问题
前端·javascript·vue.js
明天…ling1 小时前
Web前端开发
前端·css·网络·前端框架·html·web
ROCKY_8171 小时前
web前端-HTML常用标签-综合案例
前端·html
海石1 小时前
从0到1搭建一个属于自己的工作流站点——羽翼渐丰(bpmn-js、Next.js)
前端·javascript·源码
Q186000000001 小时前
在HTML中添加图片
前端·html
傻虎贼头贼脑1 小时前
day21JS-npm中的部分插件使用方法
前端·npm·node.js