vue3渲染器设计

利用响应系统的能力,自动调用渲染器完成页面的渲染和更新

const { ref, effect } = Vue;

function renderer(domString, container) {
	container.innerHTML = domString;
}

const count = ref(0);

effect(() => {
    renderer(`<h1>${count.value}</h1>`, document.getElementById('app'));
});

count.value++;

渲染器renderer: 把虚拟DOM渲染成为特定平台的真是元素。

render函数: 以container为挂载点,将vnode渲染为真实的DOM并添加到挂载点下。

render函数基本实现

function createRenderer() {
    // 挂载
    function mountElement(vnode, container) {
        const el = document.createElement(vnode.type);
        if (typeof vnode.children === 'string') {
            el.textContent = vnode.children
        }
        // 将元素添加到容器中
        conatiner.appendChild(el)
    }
    // 更新打补丁
    function patch(n1, n2, container) {
        if (!n1) {
            // 挂载
            mountElement(n2, container)
        }
        else {
            // 更新,打补丁,暂时忽略
        }
    }
    function render(vnode, container) {
        if (vnode) {
            patch(container._vnode, vnode, container);
        } else {
            if (container._vnode) {
                // 旧vnode存在,新vnode不存在 => 卸载
                container.innerHTML = '';
            }
        }
        container._vnode = vnode;
    }

    return {
        render,
    };
}

此函数中大量依赖了浏览器的API(document.*)如何修改成通用渲染器呢?

抽象通用渲染器

抽离浏览器特有的API

​ 将操作DOM的API作为配置项,作为createRenderer函数的参数传递。这样就可以在函数内部通过配置项来获取操作DOM的API。

const rener = createRenderer({
    // 用于创建元素
    createElement(tag) {
        return document.createElement(tag);
    },
    // 用于设置元素的文本节点
    setElementText(el, text) {
        el.textContent = text;
    },
    // 用于在给定parent下添加指定元素
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor);
    },
});

function createRenderer(options) {
    const { createElement, setElementText, insert } = options;

    function mountElement(vnode, container) {
        const el = createElement(vnode.type);
        if (typeof el.children === 'string') {
            setElementText(el, vnode.children);
        }
        insert(el, container);
    }
    
    // 更新打补丁
    function patch(){}
    function render(){}
    
    return {
        render
    }
}

自定义渲染器通过抽象的手段让核心代码不再依赖平台特有的API,再通过个性化配置的能力来实现跨平台。

挂载与更新

vnode.children定义:

// 虚拟节点: 版本vnode-1
const vnode = {
    type: 'div',
    children: [
        {
            type: 'p',
            children: 'hello'
        }
    ]
}

vnode.children是一个数组,每个元素都是一个独立的虚拟节点对象,这样就形成了树形结构,即: 虚拟DOM树。

// 挂载方法:版本mountElement-1
function mountElement(vnode, container) {
    const el = document.createElement(vnode.type);
    if (typeof vnode.children === 'string') {
        el.textContent = vnode.children
    }
    else if (Array.isArray(vnode.children)) {
        // 遍历每一个节点,调用patch挂载
        vnode.children.forEach(child => {
            paych(null, child, el)
        })
    }
    // 将元素添加到容器中
    insert(el, container);
}

版本mountElement-1中需要注意两点:

  • patch第一个参数为null,因为挂载阶段没有旧vnode。
  • path第三个参数为挂载点,因为正在挂载的子元素是div标签的子节点,需要吧刚创建的div元素作为挂载点。

完成挂载后,看一下vnode如何描述一个标签属性。虚拟节点增加vnode.props字段描述元素属性

// 虚拟节点: 版本vnode-2
const vnode = {
    type: 'div',
    props: {
      id: 'foo'  
    },
    children: [
        {
            type: 'p',
            children: 'hello'
        }
    ]
}

props是一个对象,可以通过遍历对象把属性渲染到对应元素上:

// 挂载方法:版本mountElement-2
function mountElement(vnode, container) {
    const el = document.createElement(vnode.type);
    // ...省略children部分代码
    
    if (vnode.props) {
        for(const key in vnode.props){
            el.setAtrriubute(key, vnode.peops[key])
            // 或者直接设置 el[key] = vnode.props[key]
        }
    }
    
    // 将元素添加到容器中
    insert(el, container);
}

HTML Attributes 与 DOM Properties

HTML Attributes的作用是设置与之对应的 DOM Properties的初始值,一旦值改变,那么 DOM Properties始终存储当前值,而通过getAttribute函数得到的仍然是初始值。

// case:
<input value="foo">
// 如果用户修改了输入框的值为bar,那么通过el.value获取的 DOM Properties值为bar,但是el.getAttribute('value')值还是foo
el.value // bar
el.getAttribute('value') // foo

setAttribute设置的值总会被字符化

el.setAttribute('disabled', false)
el.setAttribute('disabled', 'false')
// 二者等价

事件的处理

// 虚拟节点: 版本vnode-3
const vnode = {
    type: 'div',
    props: {
      id: 'foo',
      onClick: () => {
          alert('alert')
      }
    },
    children: [
        {
            type: 'p',
            children: 'hello'
        }
    ]
}

对事件的处理,通过调用addEventListener函数来绑定事件

// 版本patchProps-1
patchProps(el, key, prevValue, nextValue){
    // 匹配on开头的属性,视为事件
    if (/^on/.test(key)) {
        // onClick => click
        const name = key.slice(2).toLoweCase();
        el.addEventListener(name, nextValue)
    }
}

事件更新如何处理呢?

常规方法先调用removeEventListener移除之前的事件,再绑定新的事件,但这种方式性能不好,有中更优的方式处理:

绑定一个伪造的事件处理函数invoker,然后把真正的事件处理函数设置为invoker.value属性的值。这样当更新事件的时候,我们不再需要调用removeEventListener函数移除上一次的绑定事件,只需要更新invoker.value的值即可。

// 版本patchProps-2
patchProps(el, key, prevValue, nextValue){
    // 匹配on开头的属性,视为事件
    if (/^on/.test(key)) {
        // 获取为该元素伪造的事件处理函数
        let invoker = el._vei;
        const name = key.slice(2).toLoweCase();
        if (nextValue) {
        	if (!invoker) {
                invoker = el._vei = e => {
                    // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
                    invoker.value(e);
                }
                invoker.value = nextValue;
                el.addEventListener(name, invoker)}
            else {
                invoker.value = nextValue;
            }  
        }
         else if (invoker) {
             // 无新的绑定,移除之前的绑定
            el.removeEventListener(name, invoker)}
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值