09-组件的实现原理

187 阅读3分钟

渲染器学习之后就应该学习组件化了,渲染器就是为了把组件渲染出来,把虚拟DOM渲染为真实DOM

1、渲染组件

从用户的角度来看,一个有状态的组件就是一个选项对象。但是,从渲染器内部来看,一个组件则是一个特殊类型的虚拟DOM节点。例如,描述普通标签,通过虚拟节点vnode.type属性来存储标签,如下代码所示:

const vnode = {
  type:'div'
  // ...
}

渲染组件需要处理type类型为对象时的情况,因为组件的本质是对象。需要对patch函数更新,代码如下:

function patch(n1, n2, container, anchor) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  const { type } = n2

  if (typeof type === 'string') {
    if (!n1) {
      mountElement(n2, container, anchor)
    } else {
      patchElement(n1, n2)
    }
  } else if (type === Text) {
    if (!n1) {
      const el = n2.el = createText(n2.children)
      insert(el, container)
    } else {
      const el = n2.el = n1.el
      if (n2.children !== n1.children) {
        setText(el, n2.children)
      }
    }
  } else if (type === Fragment) {
    if (!n1) {
      n2.children.forEach(c => patch(null, c, container))
    } else {
      patchChildren(n1, n2, container)
    }
  } else if (typeof type === 'object' || typeof type === 'function') {
    // component
    if (!n1) {
      mountComponent(n2, container, anchor)
    } else {
      patchComponent(n1, n2, anchor)
    }
  }
}

组件本身是对页面内容的封装,用来描述页面内容的一部分,因此一个组件必须包含一个render函数,并且返回值应该是虚拟DOM,代码如下:

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

有基本结构之后就可以完成渲染,代码如下:

// 用来描述组件的 VNode 对象,type 属性值为组件的选项对象
const CompVNode = {
  type: MyComponent
}
// 调用渲染器来渲染组件
renderer.render(CompVNode, document.querySelector('#app'))

渲染器内部执行挂载组件,实现如下

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)
}

2、组件状态与自更新

与用户约定data()表示组件的状态,同时可以在渲染函数中通过this访问由data函数返回状态数据。

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

组件状态初始化:

1.通过组件的选项对象获取data并执行,通过reactive把data函数的返回状态包装为响应式数据

2.在调用render函数时,将this的指向设置为响应式数据state,同时将state作为render函数的第一个参数传递。

代码如下:

function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type
  const { render, data } = componentOptions
  // 调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
  const state = reactive(data())
  // 调用 render 函数时,将其 this 设置为 state,
  // 从而 render 函数内部可以通过 this 访问组件自身状态数据
  const subTree = render.call(state, state)
  patch(null, subTree, container, anchor)
}

3、组件实例与组件的生命周期

组件实例本质上就是一个状态集合(或一个对象),负责维护组件运行过程中的所有信息

如:组件生命周期函数、组件渲染的子树、组件是否已经被挂载、组件自身的状态等

function patchComponent(n1, n2, anchor) {
    const instance = (n2.component = n1.component)
    const { props } = instance
    if (hasPropsChanged(n1.props, n2.props)) {
      const [ nextProps, nextAttrs ] = resolveProps(n2.type.props, n2.props)
      for (const k in nextProps) {
        props[k] = nextProps[k]
      }
      for (const k in props) {
        if (!(k in nextProps)) delete props[k]
      }
    }
  }

  function hasPropsChanged(
    prevProps,
    nextProps
  ) {
    const nextKeys = Object.keys(nextProps)
    if (nextKeys.length !== Object.keys(prevProps).length) {
      return true
    }
    for (let i = 0; i < nextKeys.length; i++) {
      const key = nextKeys[i]
      return nextProps[key] !== prevProps[key]
    }
    return false
  }

  const p = Promise.resolve()
  const queue = new Set()
  let isFlushing = false
  function queueJob(job) {
    queue.add(job)
    if (!isFlushing) {
      isFlushing = true
      p.then(() => {
        try {
          queue.forEach(jon => job())
        } finally {
          isFlushing = false
        }
      })
    }
  }

  function resolveProps(options, propsData) {
    const props = {}
    const attrs = {}
    for (const key in propsData) {
      if ((options && key in options) || key.startsWith('on')) {
        props[key] = propsData[key]
      } else {
        attrs[key] = propsData[key]
      }
    }

    return [ props, attrs ]
  }

  function mountComponent(vnode, container, anchor) {
    const isFunctional = typeof vnode.type === 'function'
    let componentOptions = vnode.type
    if (isFunctional) {
      componentOptions = {
        render: vnode.type,
        props: vnode.type.props
      }
    }
    let { render, data, setup, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, props: propsOption } = componentOptions

    beforeCreate && beforeCreate()

    const state = data ? reactive(data()) : null
    const [props, attrs] = resolveProps(propsOption, vnode.props)
  	//slots
    const slots = vnode.children || {}

    const instance = {
      state,
      props: shallowReactive(props),
      isMounted: false,
      subTree: null,
      slots,
      mounted: []
    }
  	//发送自定义事件部分
    function emit(event, ...payload) {
      const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
      const handler = instance.props[eventName]
      if (handler) {
        handler(...payload)
      } else {
        console.error('事件不存在')
      }
    }

    // setup
    let setupState = null
    if (setup) {
      const setupContext = { attrs, emit, slots }
      const prevInstance = setCurrentInstance(instance)
      const setupResult = setup(shallowReadonly(instance.props), setupContext)
      setCurrentInstance(prevInstance)
      if (typeof setupResult === 'function') {
        if (render) console.error('setup 函数返回渲染函数,render 选项将被忽略')
        render = setupResult
      } else {
        setupState = setupContext
      }
    }

    vnode.component = instance
  	//slots
    const renderContext = new Proxy(instance, {
      get(t, k, r) {
        const { state, props, slots } = t

        if (k === '$slots') return slots

        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.error('不存在')
        }
      },
      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.error('不存在')
        }
      }
    })

    // created
    created && created.call(renderContext)


    effect(() => {
      const subTree = render.call(renderContext, renderContext)
      if (!instance.isMounted) {
        beforeMount && beforeMount.call(renderContext)
        patch(null, subTree, container, anchor)
        instance.isMounted = true
        mounted && mounted.call(renderContext)
        instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))
      } else {
        beforeUpdate && beforeUpdate.call(renderContext)
        patch(instance.subTree, subTree, container, anchor)
        updated && updated.call(renderContext)
      }
      instance.subTree = subTree
    }, {
      scheduler: queueJob
    })
  }

主要三部分:

state:组件自身状态,即data

isMounted:表示组件是否挂载

subTree:存储组件的渲染函数返回的虚拟DOM,即组件的子树(subTree)

4、props与组件被动更新

props重要的两部分:

1、为组件传递的props数据,即vnode.props对象

2、组件选项对象中定义的props选项,即。props对象

这部分代码如上面mountComponent函数所示

其中解析组件在渲染时使用的props和attrs数据的注意事项:

1、Vue.js中,没有定义在组件上的props选项中的props数据存储到attrs对象中

2、上述实现没有包含默认值、类型校验等内容的处理

在props数据发生变化时,会触发父组件重新渲染,渲染器会发现父组件的subtree包含组件类型的虚拟节点,调用patch操作,patchComponent函数进行子组件更新操作。

当子组件发生被动更新时需要:

1、检测子组件是否真的需要更新,因为子组件的props可能是不变的

2、如果需要更新,则更新子组件的props、slots等内容

5、setup函数的作用与实现

setup函数是Vue.js 3中组件选项,主要用于组合式API中,转换响应式、注册生命周期钩子函数

setup函数接收的两个参数:

1.props:取得外部为组件传递的props数据对象

2.setupContext:与组件接口相关的数据与方法

slots:插槽

emit:发送自定义事件

attrs:没显示声明为props的都在这里面

expose:暴露组件数据

setup代码实现,见上面代码中setup注释部分

6、emit的实现

自定义事件会被编译为属性存储到props

emit接收的两个参数:

1.event:事件名称

2.playload:传递给事件处理函数的参数

主要原理,实现emit函数并添加到setupContext中,检测是否以on字符串开头,如果是则认为是自定义事件,具体代码在上面emit注释部分

7、插槽的工作原理与实现

插槽:在组件中预留位置,具体渲染内容由用户决定

实现原理:插槽内容会被编译为插槽函数,放到setupContext中,拦截renderContext的get方法,遇到$slots直接返回slots对象,代码如上注释部分

8、注册生命周期

不同组件,不同的生命周期,需要分配currentIstance去判断当前生命周期属于哪个组件,每当初始化组件并执行setup函数之前,先将currentIstance设置为当前组件,再执行setup函数就可以判断哪个组件正在被初始化,代码如上

总结

1、学习了如何使用虚拟节点描述组件,通过type存储组件对象,通过mountComponent挂载组件,patchComponent更新组件

2、组件自更新,触发副作用函数重新执行,缓冲到微任务队列中

3、组件实例,本质上是对象,包含了组件运行过程中的状态,判断标识确定进行挂载还是打补丁

4、props与组件的被动更新,副作用自更新引起的子组件的更新叫被动更新,渲染上下文

5、setup函数,组合式API写法包含props,setupContext

6、emit函数包含在setupContext中,通过v-on指令为组件编译后以onXX存储到props中

7、slots,会被编译为插槽函数调用,通过执行插槽函数得到外部想槽为填充的部分

8、生命周期部分,通过currentInstance判断是哪个组件处于生命周期的哪个阶段