likes
comments
collection
share

探索Vue渲染器的内部机制-挂载元素属性

作者站长头像
站长
· 阅读数 38

回顾

前面聊完渲染器的概念后再来看看虚拟节点中的属性是如何被挂载到真实dom上去的,再来回顾一下虚拟节点的结构。

const vnode = {
    type:'div',
    props:{
        id:'box',
    },
    children:'hello'
}

其中props属性就是要被挂载到元素中的属性,那还是本着从简的原则来实现一下。

function moutEelement(vnode,container){
       const el = document.createElement(vnode.type);
       // 如果给元素设置了props
       if(vnode.props){
           for(let key in vnode.props){
               el.setAttribute(key,props[key])
           }
       }
       container.append(el);
}

如果为元素设置了属性会全部被收集到props对象中,然后依次遍历属性并使用setAttribute函数将属性设置到元素上。除了使用setAttribute函数外还可以使用DOM对象直接设置即:

el[key] = props[key];

这两种方式都存在缺陷,那这两种方式有什么区别呢?

HTML Attributes与DOM Properties

理解这两种方式之间的差异和关联是正确为元素设置属性的关键。HTML Attributes指的是定义在HTML标签上的属性,比如:

<input id="ipt" value="xxx" />

与之对应的还有其DOM对象:

const ipt = document.getElementById("ipt");
ipt.value;

DOM Properties其实就可以理解为是在js中获取/设置的属性。但它们不都是一一对应的关系,比如class属性:

<div class="box" id="foo"></div>

const div=document.getElementById("foo")
console.log(div.className)

class="box"对应的DOM Properties是div.className

那么它们两者之间有没有什么关联呢?下面来看一个比较奇怪的现象:

  <input id="foo" value="foo"/>

const ipt = document.querySelector("#foo");
ipt.value = "bar";
console.log(ipt.value); // "bar"
console.log(ipt.getAttribute('value')); // "foo" 

从上面的代码中可以看出对文本框内容的修改不会影响ipt.getAttribute('value')的返回值,这说明HTML Attributes的作用是设置与之对应的DOM Properties的初始值。 也就是说当值发生改变时,DOM Properties中始终存储的是当前变化的值,而HTML Attributes中存储的是默认值。

如何正确设置元素属性

对于普通的HTML文件来说浏览器会自动分析HTMLAttributes并设置对应的DOM Properties,但如果是一个vue文件的话并不会被浏览器所解析,下面就来看看在vue中是如何进行处理的。 以button为例:

<button disabled>Button</button>

编译为vnode就变成:

const vnode = {
    type:'button';
    props:{
        disabled:""
    },
    children:"Button"
}

接着使用setAttribute('disabled','')方法会将按钮禁用,但是考虑这种情况:

<button :disabled="false">Button</button>

const vnode = {
    type:'button';
    props:{
        disabled:false
    },
    children:"Button"
}

这个时候明显是想不对按钮禁用,但是按钮依然会被禁用,这是因为使用setAttribute函数设置的值总是会被字符串化。对于按钮来说只要disabled属性值为true就会生效,并不会关心HTML Attributes的值是什么,到这里我们发现不应该总是使用setAttribute函数为元素设置属性,这时候可以很自然的想到使用el.disabled = falseDOM Properties的方式,这样确实可以解决这种情况下的问题,但又不适用于第一个情况,再把第一个案例搬过来

<button disabled>Button</button>

const vnode = {
    type:'button';
    props:{
        disabled:""
    },
    children:"Button"
}

在vue中这种情况下我们希望的是禁用该按钮,但如果使用DOM.properties的方式则会变成

el.disabled = '';

由于disabled属性是一个布尔值,在js中一个空的字符串会被认为是false即等价于el.disabled = flase,也就是不禁用,这就违背了我们的本意因为我们希望禁用该按钮,所以我们就需要针对这一问题进行特殊处理。

function mountElement(vnode,container){
    const el =document.createElement(vnode.type);
    // ... 省略children的处理
    if(vnode.props){
        for(let key in vnode.props){
            // key是否有对应的DOM Properties
            if(key in el){
                const type =typeof el[key];
                const value = vnode.props[key];
                if(type == "boolean" && value === ''){
                    el[key] = true;
                }else{
                    el[key] = false;
                }
            }else{
                el.setAttribute(key,vnode.props[key]);
            }
        }
    }
    container.append(el);
}

上面代码中首先判断了key是否有其对应的DOM Properties,如果有则判断DOM Properties的属性值是否是一个布尔值并且传递的属性值是否为空,若满足该条件则将DOM Properties的属性值设为true,如果没有则依然使用setAttribute方法为元素设置属性。

class的处理

在vue中对class属性做了增强,比如我们经常这样为元素设置类名:

// 值为字符串
<div class="foo"></div>

// 值为对象
<div :class='{foo:true,bar:false}'></div>

// 值为数组
<div :class='['foo','bar',{test:true,home:false}]'></div>

可以看到class的值可以是多种类型的,所以在设置元素的类名之前必须统一对值进行序列化操作,下面来实现一个简单的用来序列化类名的normalizeClass函数。

// 序列化函數
function normalizeClass(value){
    if(!value) return '';
    if(typeof value == 'string') return value;
    if(Array.isArray(value)){
        let str = '';
        for(let i = 0;i < value.length;i++){
            let item = value[i];
            if(typeof item == 'string'){
                str += item +' ';
            }else{
                // 走对象的处理逻辑
                str += normalizeClass(item)
            }
        }
        return str;
    }
    if(typeof value == 'object'){
        let str = '';
        for(let key in value){
            // 当属性值为true时才添加
            str += value[key] ? key + ' ' : '';
        }
        return str.trim();
    }
}

 console.log(normalizeClass('foo bar')) // foo bar
 console.log(normalizeClass({ foo: true, bar: true })) // foo bar
 console.log(normalizeClass(['foo', 'bar', { test: true, home: false }])) // foo bar test

对应的vnode结构就应该是这样:

const vnode = {
    type:'div',
    props:{
        class:normalizeClass(...)
    }
}

现在class就可以正常使用了, 下一步就是要把它设置到元素上,设置class类名的方式有三种分别是setAttribute、className、classList,其中className的性能最优,所以需要再修改一下mounteElement函数,设置class类名时直接使用className来设置。

function mountElement(vnode,container){
    const el =document.createElement(vnode.type);
    // ... 省略children的处理
    if(vnode.props){
        for(let key in vnode.props){
            // 针对class的处理
            if(key == 'class'){
              el.className = vnode.props[key];
            }
            // key是否有对应的DOM Properties
            if(key in el){
                const type =typeof el[key];
                const value = vnode.props[key];
                if(type == "boolean" && value === ''){
                    el[key] = true;
                }else{
                    el[key] = false;
                }
            }else{
                el.setAttribute(key,vnode.props[key]);
            }
        }
    }
    container.append(el);
}

除了class属性外,vue也对style属性进行了增强,类似的处理也是一样的。

小结

这篇文章主要讲解了在vue中如何给元素添加属性,相对复杂了一点主要有一些需要特殊处理的情况,当然还是那句话,掌握处理问题的思路更加重要!

转载自:https://juejin.cn/post/7374055681890107427
评论
请登录