vuejs设计与实现-组件的实现原理

渲染组件

  • 从用户角度来看,组件是一个选项对象.
  • 从渲染器的内部实现来看, 组件是一个特殊类型的虚拟dom节点.

组件是对页面的一部分进行封装, 这样多个组件就可以组成一个完整的页面.

// MyComponent是一个组件
const MyComponent = {
    name: 'MyComponent',
    data(){
        return { foo: 1 }
    }
}
// vnode 描述一个组件
const vnode = {
    type: MyComponent
    // ....
}
// 增加对组件类型的虚拟节点处理
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(type === Text) {
        // 文本节点
    } else if(type === Fragment) {
        // 片段
    } else if(typeof type === 'object') {
        if(!n1){
            mountComponent(n2, container, anchor)
        } else {
            patchComponent(n1, n2, container)
        }
    }
}
// 挂载组件
function mountComponent(vnode, container, anchor){
    const componentOptions = vnode.type
    const { render } = componentOptions
    // 调用组件的渲染函数
    const subTree = render()
    patch(null, subTree, container, anchor)
}

组件状态与自更新

  1. 使用data函数定义组件自身的状态, 在渲染函数中可以通过this访问由data函数返回的状态数据.
  2. 将渲染任务放在effect中执行, 这样当组件自身状态发生变化时, 可以自动触发组件的更新.
  3. 实现异步更新. 使用调度器控制渲染函数的执行, 避免不必要的执行浪费性能.
// 在render函数中访问data
const MyComponent = {
    name: 'MyComponent',
    data(){
        return { foo: 1 }
    },
    render(){
        return {
            type: 'div',
            children: `foo 的值是: ${this.foo}`
        }
    }
}

//  增加组件自身状态与更新
function mountComponent(vnode, container, anchor){
    const componentOptions = vnode.type
    const { render, data } = componentOptions
    // 将data返回的原始数据包装成响应式数据
    const state = reactive(data())
    
    // 将组件的render函数调用包装在effect内
    effect(() => {
        // 改变this指向, 使可以访问data中的数据
        const subTree = render.call(state, state)
        patch(null, subTree, container, anchor)
    }, {
        scheduler: queueJob
    })
}

// queueJob
const queue = new Set()
let isFlushing = false
const p = Promise.resolve()
// 调度器
function queueJob(job){
    queue.add(job)
    if(!isFlushing) {
        isFlushing = true
        p.then(() => {
            try {
               queue.forEach(job => job()) 
            } finally {
                // 重置状态
               isFlushing = false
               queue.clear = ()
               // 书中  queue.clear = 0 没看懂 ? 
            }
        })
    }
}

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

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

function mountComponent(vnode, container, anchor){
    const componentOptions = vnode.type
    const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions
    // 组件创建之前, 调用beforeCreate钩子
    beforeCreate && beforeCreate()
    
    const state = reactive(data())
    // 组件实例, 包含与组件相关的信息
    const instance = {
        state,
        isMounted: false,
        subTree: null
    }
    vnode.component = instance
    // 组件创建完成, 调用created钩子 
    // 所以在 beforeCreate 中无法访问data
    created && created.call(state)
    
    effect(() => {
        const subTree = render.call(state, state)
        if(!instance.isMounted){
            // 挂载组件
            beforeMount && beforeMount.call(state)
            patch(null, subTree, container, anchor)
            mounted && mounted.call(state)
        } else {
            // 更新组件
            beforeUpdate && beforeUpdate.call(state)
            patch(instance.subTree, subTree, container, anchor)
            updated && updated.call(state)
        }
        instance.subTree = subTree
    }, {
        scheduler: queueJob
    })
}

props与组件的被动更新

对于组件的props, 我们需要关心:

  • 为组件传递的props数据, 即组件的vndoe.props对象.
  • 组件选项对象中定义的props选项, 即MyComponent.props对象.

结合以上两点解析出组件在渲染时需要用到的props数据. props本是来源于父组件的数据, 其变化会引起父组件的自更新. 过程中渲染器发现父组件的subTree包含组件类型的虚拟节点, 会调用patchComponent函数完成子组件更新. 当子组件发生被动更新时, 需要:

  1. 检测是否真的要更新, 因为子组件的props可能是不变的
  2. 如果需要更新, 则更新子组件的propsslots等内容

由于props数据与组件自身的状态数据都需要暴露到渲染函数当中(在渲染函数当中, 通过this进行访问), 需要封装一个渲染上下文对象renderContext. 并将其作为渲染函数以及生命周期钩子的this值.

// 增加对props的解析
function mountComponent(vnode, container, anchor){
    const componentOptions = vnode.type
    const { render, data, props: propsOption } = componentOptions
    
    beforeCreate && beforeCreate()
    
    const state = reactive(data())
    const [props, attrs] = resolveProps(propsOption, node.props)
    const instance = {
        state,
        props: shallowReactive(props), 
        isMounted: false,
        subTree: null
    }
    vnode.component = instance
    // 创建渲染上下文对象 --  组件实例的代理
    // 拦截数据状态的读取与设置操作, 尝试从组件自身状态及props中读取
    const renderContext = new Proxy(instance, {
        get(t, k, r){
            const { state, props } = t
            if(state && k in state) {
                return state[key]
            } else if (k in props){
                return props[key]
            } else {
                console.error('不存在')
            }
        },
        set(t, k, v, r){
            const { state, props } = t
            if(state && k in state) {
                state[key] = v
            } else if (k in props){
                console.error(`Attempting to mutate prop "${k}". Props are readonly.`)
            } else {
                console.error('不存在')
            }
            return true
        }
    })
    // created 绑定渲染上下文对象
    created && created.call(renderContext)
    
    // ...
}

// 解析组件props与attrs
function resolveProps(options, propsData){
    const props = {}, attrs = {}
    for(const key in propsData) {
        if(key in options) {
            // 定义存在则是合法的props
            props[key] = propsData[key]
        } else {
            attrs[key] = propsData[key]
        }
    }
    return [props, attrs]
}

// patchComponent函数完成子组件更新
// 由父组件自更新引起的子组件更新叫做子组件的被动更新
function patchComponent(n1, n2, container){
    // 获取组件实例, 同时赋值给新的组件vnode对象. 否则下次更新无法获取组件实例
    const instance = (n2.component = n1.component)
    const { props } = instance
    // 检查props是否发生变化
    if(hasPropsChanged(n1.props, n2.props)){
        const [ nextProps ] = resolveProps(n2.type.props, n2.props)
        // instance.props 本身是浅响应的
        // 更新props时设置其属性值即可触发组件重新渲染 
        for(const k in nextProps){
            props[k] = nextProps[k]
        }
        // 删除不存在的props
        for(const k in props){
            if(!(k in nextProps)) delete props[k]
        }
    }
}

// 验证props是否发生变化
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]
        if(nextProps[key] !== prevProps[key]) return true
    }
    return false
}

除了组件自身的状态及props数据之外, 完整的组件还包括methodscomputed等选项中定义的数据和方法, 这些内容都应在渲染上下文对象中处理.

setup函数的作用与实现

setup函数是vue3中的组件选项, 用于配合组合式api, 提供两个参数:

  1. props数据对象
  2. setupContext对象, 包括slotsemitattrsexpose

在组件的生命周期中, setup函数只会在被挂载时执行一次, 返回值可以作为组件的渲染函数(返回一个函数) 或者 提供给渲染函数使用(返回一个对象暴露给渲染函数, 可以通过this访问)

// 增加setup选项, 提供组合式API能力
function mountComponent(vnode, container, anchor){
    const componentOptions = vnode.type
    const { 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
    }
    // 暂时省略 emit 和 slots
    const setupContext = { attrs }
    // setup函数提供 props 和 setupContext 两个参数
    const setupResult = setup(shallowReadonly(instance.props), setupContext)
    let setupState = null
    // 根据返回值类型, 直接作为render函数 或 提供给渲染函数使用
    if(typeof setupResult === 'function') {
        if(render) console.error('setup函数返回渲染函数, render选项被忽略!')
        render = setupResult
    } else {
        setupState = setupResult
    }
    
    vnode.component = instance
     // 渲染上下文 增加对setupState的支持
    const renderContext = new Proxy(instance, {
        get(t, k, r){
            const { state, props } = t
            if(state && k in state) {
                return state[key]
            } else if (k in props){
                return props[key]
            } else if (setupState && k in setupState){
                return setupState[key]
            } else {
                console.error('不存在')
            }
        },
        set(t, k, v, r){
            const { state, props } = t
            if(state && k in state) {
                state[key] = v
            } else if (k in props){
                console.error(`Attempting to mutate prop "${k}". Props are readonly.`)
            } else if (setupState && k in setupState){
                setupState[k] = v
            } else {
                console.error('不存在')
            }
            return true
        }
    })
    
    // ...
}

组件事件与emit的实现

emit用来发射组件的自定义事件, 根据事件名称xxx去props数据对象中寻找对应的事件处理函数onXxx.

// 增加setupContext对象的emit选项
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)}`
        const handler = instance.props[eventName]
        // 其实还需改变this指向
        handler && handler(...payload)
    }
    const setupContext = { attrs, emit }
    
    // ...
}

// 处理 onXxx 格式的props
function resolveProps(options, propsData){
    const props = {}, attrs = {}
    for(const key in propsData) {
        if(key in options || key.startWith('on')) {
            // 定义存在则是合法的props
            props[key] = propsData[key]
        } else {
            attrs[key] = propsData[key]
        }
    }
    return [props, attrs]
}

插槽的工作原理与实现

组件会预留一个槽位, 具体要渲染的内容由用户插入. 组件模板的插槽内容会被编译为插槽函数, 返回值就是具体的插槽内容. 渲染插槽的过程, 就是调用插槽函数(this.$slots.xxx())并渲染由其返回的内容的过程.

// 增加setupContext对象的emit选项
function mountComponent(vnode, container, anchor){
    // ...
    // 将编译好的 vnode.children 作为slots对象
    const slots = vnode.children || {}
    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(key === '$slots') return slots
            // ...
        }
    })
    
    // ...
}

对渲染上下文进一步处理, 当读取$slots时, 返回组件实例上的slots对象. 通过this.$slots就可以访问插槽内容.

注册生命周期

onMounted为例, 可以多次调用, 注册多个钩子函数. 为了将钩子函数准确注册至组件上, 需要维护一个currentInstance变量, 存储当前组件实例.

function mountComponent(vnode, container, anchor){
    // ...
        const instance = {
        state,
        props: shallowReactive(props),
        isMounted: false,
        subTree: null,
        slots,
        // 存储 onMounted 注册的诸多钩子
        mounted: []
    }
    
    const setupContext = { attrs, emit, slots }
    // 设置当前组件实例
    setCurrentInstance(instance)
    // 执行setup函数 其中的生命周期注册函数将读取当前组件实例
    const setupResult = setup(shallowReadonly(instance.props), setupContext)
    setCurrentInstance(null)
    
    effect(() => {
        const subTree = render.call(renderContext, renderContext)
        if(instance.isMounted) {
            // ...
            // 逐个执行
            instance.mounted && instance.mounted.forEach(hook => {
                hook.call(renderContext)
            }) 
        } else {
            // ...
        }
    })
}
// 提供注册生命周期钩子函数的方法
function onMounted(fn){
    if(currentInstance){
        currentInstance.mounted.push(fn)
    } else {
        console.error('onMounted 只能在 setup 中调用')
    }
}

对其他生命周期狗子函数, 其原理一样.

总结

  • 虚拟节点的vnode.type属性存储组件对象, 渲染器根据虚拟节点的type属性判断是否为组件, 并通过mountComponentpatchComponent完成组件的挂载与更新.
  • 组件挂载阶段, 会创建一个用于渲染其内容的副作用函数. 组件自更新是组件自身的响应式数据与组件的渲染函数建立响应式联系, 并通过调度器实现异步更新.
  • 组件实例本质是一个包含了组件运行状态的对象. 渲染副作用函数内, 通过实例上的状态标识, 判断进行全新的挂载或更新.
  • 副作用自更新所引起的子组件更新叫做子组件的被动更新.
  • 渲染上下文renderContext是组件实例的代理对象.
  • setup为组合式而生. 其返回值根据类型有不同用处.

原文链接:https://juejin.cn/post/7214458935171743804 作者:NidusP

(0)
上一篇 2023年3月26日 下午4:31
下一篇 2023年3月26日 下午4:41

相关推荐

发表回复

登录后才能评论