vue2面试题|[2024-12-5]

发布于:2024-12-18 ⋅ 阅读:(86) ⋅ 点赞:(0)

开题答辩终于结束了,又要开始我的前端面试学习啦!!!

1.v-model双向绑定原理

class Vue{
        constructor(options){
            this.$options = options
            this.$watchEvent = {}
            
            if(typeof options.beforeCreate == 'function'){
                options.beforeCreate.bind(this)()
            }
            // 这是data
            this.$data = options.data
            this.proxyData()
            this.observe()
            if(typeof options.created == 'function'){
                options.beforeCreate.bind(this)()
            }
            if(typeof options.beforeMount == 'function'){
                options.beforeCreate.bind(this)()
            }
            // 这是节点
            this.$el = document.querySelector(options.el)
            // 模板渲染
            this.compile(this.$el)
            if(typeof options.mounted == 'function'){
                options.beforeCreate.bind(this)()
            }
            
        }
        
    // 1.给vue大对象赋属性,来自于data中
    // 2.data中的属性值和vue大对象的属性双向(劫持)
    proxyData(){
        for(let key in this.$data){
            Object.defineProperty(this,key,{
                get(){
                    return this.$data[key]
                },
                set(val){
                    this.$data[key] = val
                }
            })
        }
    }

    // 触发data中的数据发生变化来执行watch中的update
    observe(){
        for(let key in this.$data){
            let value = this.$data[key]
            let that = this
            Object.defineProperty(this.$data,key,{
                get(){
                    return value
                },
                set(val){
                    value = val
                    if(that.$watchEvent[key]){
                        that.$watchEvent[key].forEach((item,index) => {
                            item.update()
                        })
                    }
                }
            })
        }
    }
   compile(node){
       node.childNodes.forEach((item.index) => {
           // 元素节点
           if(item.nodeType == 1){
                // 判断元素节点是否绑定了@click
                if(item.hasAttribute('@click')){
                    // @click后绑定的属性值
                    let vmKey = item.getAttribute('@click').trim()
                    item.addEventListener('click',(event) => {
                        this.eventFn = this.$options.methods[vmKey].bind(this)
                        this.eventFn(event)
                    })
                }
                // 判断元素节点是否添加了v-model
                if(item.hasAttribute('v-model')){
                    let vmKey = item.getAttribute('v-model').trim();
                    if(this.hasOwnProperty(vmKey)){
                        item.value = this[vmKey];
                    }
                    item.addEventListener('input',(event) => {
                        this[vmKey] = item.value;
                    })
                }
                if(item.childNodes.length>0){
                    this.compile(item)
                }
          
           }
           // 这里是文本节点,如果有{{}}就替换成数据
           if(item.nodeType == 3){
              // 正则匹配
              let reg = /\{\{(.*?)\}\}/g
              let text = item.textContent
              // 给节点赋值
              item.textContent = text.replacce(reg,(match,vmKey) => {
                   vmKey = vmKey.trim()

                    if(this.hasOwnProperty(vmKey)){
                        let watch = new Watch(this,vmKey,item,'textContent')
                        if(this.$watchEvent[vmKey]){
                            this.$watchEvent[vmKey].push(watch)
                        }else{
                            this.$watchEvent[vmKey] = []
                            this.$watchEvent[vmKey].push(watch)
                        }
                    }
                   return this.$data[vmKey]
               })
             }
        })
    }
}

class Watch{
    constructor(vm,key,node,attr){
        // 对象
        this.vm = vm
        // 属性名称
        this.key = key
        // 节点
        this.node = node
        // 改变文本节点内容的字符串
        this.attr = attr
    }

    //执行改变(update)操作
    update(){
        this.node[this.attr] = this.vm[this.key]
    }

}

通过Object.defineProperty劫持数据发生的改变,如果数据发生改变了(在set中进行赋值的),触发update方法进行更新节点内容({{ str }}),从而实现了数据的双向绑定原理。

2.diff算法

功能:提升性能

虚拟dom ====>  其实就是数据(把dom数据化)

<script type="text/javascript">
    let box = document.getElementById("box");
    // 第一种:操作dom
    console.time('a');
    for(let i = 0; i <= 10000; i++){
        box.innerHTML = i;
    }
    console.timeEnd('a');

    // 第二种:数据化
    console.time('b');
    let num = 0;
    for(let i = 0; i <= 10000; i++){
        num = i;
    }
    bpx.innerHTMML = num;
    console.timeEnd('b');
</script>

 对于这两种方法,直接操作dom比较费时,将其数据化可以节省很多时间。

操作dom:73ms,但是数据化:0.28ms

主流:snabbdom、virtual-dom

2.1搭建环境:

npm init -y
cnpm install webpack@5 webpack-cli@3 webpack-dev-server@3 -S
cnpm install snabbdom -S
新建webpack.config.js
配置webpack.config.js

 2.2 虚拟节点和真实节点

虚拟节点:

{
    children: undefined
    data:{}
    elm:h1
    key:undefined
    sel:"h1"
    text:"你好h1"
}

        这是对于h1的虚拟节点,其中虚拟节点在表示时用h('h1',{},"你好h1")

        下面就是对于ul的虚拟节点:

{
    children:[
        0:{
            children:undefined
            data:{}
            elm:li
            key:undefined
            sel:"li"
            text:"a"
        }
        1:{...}
        2:{...}
    ]
    data:{}
    elm:ul
    key:undefined
    sel:"ul"
    text:undefined

}

真实节点:

<h1>你好h1</h1>

2.3 新老节点替换的规则

        1、如果新老节点不是同一个节点名称,那么就暴力删除旧的节点,创建插入新的节点

        2、只能同级比较,不能跨层比较。如果跨层那么就暴力删除旧的节点,创建插入新的节点。

        3、如果是相同节点,又分很多情况

                3.1 新节点没有children

                        如果新的节点没有children,那就证明新的节点是文本,那么直接把旧的替换成新的文本

                 3.2 新节点有children

                        新的有children,旧的也有children ===》就是diff算法的核心了

                        新的有children,旧的没有 ===》创建元素添加(把旧的内容删除清空,增加新的)

***注意:如果要提升性能,一定要加入key,key是唯一标识,在更改前后,确认是不是同一个节点。

const container = document.getElementById("container");
const btn = document.getElementById("btn");

const vnode1 = h('h1',{},'你好');
patch(container,vnode1);

const vnode2 = h('div',{},'hi');

btn.onclick = function(){
    patch(vnode1,vnode2);
}

添加key,这样就可以提升性能:

const container = document.getElementById("container");
const btn = document.getElementById("btn");

const vnode1 = h('ul',{},[
    h('li',{key:'a'},'a'),
    h('li',{key:'b'},'b'),
    h('li',{key:'c'},'c')
]);
patch(container,vnode1);

const vnode2 = h('ul',{},[
    h('li',{key:'c'},'c'),
    h('li',{key:'b'},'b'),
    h('li',{key:'a'},'a')
]);

btn.onclick = function(){
    patch(vnode1,vnode2);
}

3.手写diff算法-生成虚拟dom

index.js

import h from './dom/h'
let vnode1 = h('div',{},'你好吖');
console.log(vnode1)

-----运行后,应该得到-----
{
    children:undefined
    data:{}
    elm:undefined
    key:undefined
    sel:"div"
    text:"你好吖"
}

-------------------------------
let vnode2 = h('ul',{},[
    h('li',{},'a'),
    h('li',{},'b'),
    h('li',{},'c')
])
console.log(vnode2)
-----运行后,应该得到-----
{
    children:[
        0:{
            children:undefined
            data:{}
            elm:li
            key:undefined
            sel:"li"
            text:"a"
        }
        1:{...}
        2:{...}
    ]
    data:{}
    elm:ul
    key:undefined
    sel:"ul"
    text:undefined

}

-------------------------------

创建h.js

import vnode from './vnode'
export default function(sel, data, params){
    // h函数的 第三个参数是字符串类型【意味着:他没有子元素】
    if( typeof params == 'string'){
        return vnode(undefined, data,undefined, sel, params);
    }else if(Array.isArray(params)){
        // h函数的 第三个参数是数组类型【意味着:他有子元素】
        let children = [];
        for(let item of params){
            children.push(item);
        }
        return vnode(children, data,undefined, sel, undefined)
    }
}

创建vnode.js

export default function vnode(children, data, elm, sel, text){
    return {
        children, 
        data, 
        elm, 
        sel, 
        text
    }
}

 4.手写diff算法-patch不是同一个节点

旧的节点为真实的节点,要将其转换为真实的虚拟节点

//index.html

<div id="container">
    这里是container
</div>


//index.js

import h from './dom/h'
import patch from './dom/patch'
// 获取真实的dom节点
let container = document.getElementById('container');

// 虚拟节点
let vnode1 = h('h1',{},''你好吖);

patch(container,vnode1);
//patch.js
import vnode from './vnode'
export default function(oldVnode, newVnode){
    // 如果oldVnode 没有sel,就证明是非虚拟节点(就让他变成虚拟节点)
    if(oldVnode.sel == undefined){
        oldVnode = vnode(
            [], //children
            {}, //data
            oldVnode, //elm
            oldVnode.tagName.toLowerCase(), //sel
            undefined // text
        )
    }
    // 判断 旧的虚拟节点 和 新的虚拟节点 是不是同一个节点
    if(oldVnode.sel === newVnode.sel){
        // 判断的条件就复杂了(很多了)
    }else{ // 不是同一个节点,那么就暴力删除旧的节点,创建插入新的节点
        // 把新的虚拟节点 创建为 dom节点
        let newVnodeElm = createElement(newVnode);
        // 获取旧的虚拟节点 .elm就是真正节点
        let oldVnodeElm = oldVnode.elm;
        // 创建新的节点
        if(newVnodeElm){
            oldVnodeElm.parentNode.insertBefore(newVnodeElm,oldVnodeElm);
        }
        // 删除旧节点
        oldVnodeElm.parentNode.removeChild(oldVnode);
    }

}
// createElement.js
// vnode 为新节点,就是要创建的节点
export default function createElement(vnode){
    // 创建dom节点
    let domNode = document.createElement(vnode.sel);
    // 判断有没有子节点 children 是不是为undefined
    if(vnode.children == undefined){
        domNode.innerText = vnode.text;
    } else if(Array.isArray(vnode.children)){

        // 说明内部有子节点,需要递归创建节点
        for(let child of vnode.children){
            let childDom = createElement(child);
            domNode.appendChild(childDom);
        }
    }
    // 补充elm属性
    vnode.elm = domNode;
    return domNode;
}

 5.手写diff算法-相同节点有没有children

//patch.js
import vnode from './vnode'
export default function(oldVnode, newVnode){
    // 如果oldVnode 没有sel,就证明是非虚拟节点(就让他变成虚拟节点)
    if(oldVnode.sel == undefined){
        oldVnode = vnode(
            [], //children
            {}, //data
            oldVnode, //elm
            oldVnode.tagName.toLowerCase(), //sel
            undefined // text
        )
    }
    // 判断 旧的虚拟节点 和 新的虚拟节点 是不是同一个节点
    if(oldVnode.sel === newVnode.sel){
        // 判断的条件就复杂了(很多了)
        patchVnode(oldVnode,newVnode);
    }else{ // 不是同一个节点,那么就暴力删除旧的节点,创建插入新的节点
        // 把新的虚拟节点 创建为 dom节点
        let newVnodeElm = createElement(newVnode);
        // 获取旧的虚拟节点 .elm就是真正节点
        let oldVnodeElm = oldVnode.elm;
        // 创建新的节点
        if(newVnodeElm){
            oldVnodeElm.parentNode.insertBefore(newVnodeElm,oldVnodeElm);
        }
        // 删除旧节点
        oldVnodeElm.parentNode.removeChild(oldVnode);
    }

}
//patchVnode.js
import createElement from './createElement'
export default function patchVnode(oldVnode,newVnode){
    // 判断新节点有没有children
    if(newVnode.children === undefined){// 没有子节点
        // 新的节点的文本 和 旧节点的文本内容是不是一样的
        if(newVnode.text !== oldVnode.text){
            oldVnode.elm.innerText = newVnode.text
        }
    }else{// 新的有子节点
        // 新的虚拟节点有, 旧的虚拟节点有
        if(oldVnode.children !== undefined && oldVnode.children.length > 0){
            // 最复杂的情况了,diff核心了 
            console.log('新旧节点都有children');

        }else{ // 新的虚拟节点有,旧的虚拟节点“没有”

            // 把旧节点的内容 清空
            oldVnode.elm.innerHTML = '';
            // 遍历新的 子节点,创建dom元素,添加到页面
            for( let child of newVnode.children){
                let childDom = createElement(child);
                oldVnode.elm.appendChild(childDom);
            }
        }
    }
}

6.diff算法核心-理论部分

每次都从1开始,不满足就依次往下执行

1.旧前 和新前

        匹配:旧前的指针++、新前的指针++

2.旧后 和新后

        匹配:旧后的指针--、新后的指针--

3.旧前 和 新后

        匹配:旧前的指针++、新后的指针--

4.旧后 和 新前

        匹配:旧后的指针--、新前的指针++

5.以上都不满足条件 ===》 查找

        新的指针++,新的添加到页面上并且新在旧的节点中有,要给旧的复制成undefined

6.创建或删除

        旧的指针指向空,新还有,新就要创建

        旧的指针不指向空,新的指针指向空,旧就要删除

注意:若对于旧的指针加或者减指向的是undefined,直接继续加或者减

 7.手写diff算法-判断前四种情况

// index.js
// 获取到了真实的dom节点
const container = document.getElementById("container");
// 获取到了按钮
const btn = document.getElementById("btn");

// 虚拟节点
const vnode1 = h('ul',{},[
    h('li',{key:'a'},'a'),
    h('li',{key:'b'},'b'),
    h('li',{key:'c'},'c')
]);
patch(container,vnode1);

const vnode2 = h('ul',{},[
    h('li',{key:'c'},'c'),
    h('li',{key:'b'},'b'),
    h('li',{key:'a'},'a')
]);

btn.onclick = function(){
    patch(vnode1,vnode2);
}
// vnode.js
export default function vnode(children, data, elm, sel, text){
    let key = data.key;
    return {
        children, 
        data, 
        elm, 
        key,
        sel, 
        text
    }
}
//patchVnode.js
import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVnode(oldVnode,newVnode){
    // 判断新节点有没有children
    if(newVnode.children === undefined){// 没有子节点
        // 新的节点的文本 和 旧节点的文本内容是不是一样的
        if(newVnode.text !== oldVnode.text){
            oldVnode.elm.innerText = newVnode.text
        }
    }else{// 新的有子节点
        // 新的虚拟节点有, 旧的虚拟节点有
        if(oldVnode.children !== undefined && oldVnode.children.length > 0){
            // 最复杂的情况了,diff核心了 
            console.log('新旧节点都有children');
            updateChildren()

        }else{ // 新的虚拟节点有,旧的虚拟节点“没有”

            // 把旧节点的内容 清空
            oldVnode.elm.innerHTML = '';
            // 遍历新的 子节点,创建dom元素,添加到页面
            for( let child of newVnode.children){
                let childDom = createElement(child);
                oldVnode.elm.appendChild(childDom);
            }
        }
    }
}
// updateChildren.js
import patchVnode from './patchVnode'
// 判断两个虚拟节点是否为同一个节点
function sameVnode(vnode1,vnode2){
    return vnode1.key == vnode2.key;
}
// 参数一:真实的dom节点
// 参数二:旧的虚拟节点
// 参数三:新的虚拟节点
export default (parentElm, oldCh, newCh) => {
    let oldStartIdx = 0;            //旧前的指针
    let oldEndIdx = oldCh.length-1; //旧后的指针
    let newStartIdx = 0;            //新前的指针
    let newEndIdx = newCh.length-1; //新后的指针
    
    let oldStartVnode = oldCh[oldStartIdx]; //旧前虚拟节点
    let oldEndVnode = oldCh[oldEndIdx];     //旧后虚拟节点
    let newStartVnode = newCh[newStartIdx]; //新前虚拟节点
    let newEndVnode = newCh[newEndIdx];     //新后虚拟节点

    while( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
        if( sameVnode(oldStartVnode,newStartVnode)){
            // 第一种情况:旧前 和 新前
            console.log("1")
            patchVnode(oldStartVnode,newStartVnode);
            if(newStartVnode) newStartVnode.elm = oldStartVnode?.elm;
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];

        }else if(sameVnode(oldEndVnode,newEndVnode)){
            // 第二种情况:旧后 和 新后
            console.log("2")
            patchVnode(oldEndVnode,newEndVnode);
            if(newEndVnode) newEndVnode.elm = oldEndVnode?.elm;
            oldEndVnode= oldCh[--oldEndIdx];
            newEndVnode= newCh[--newEndIdx];

        }else if(sameVnode(oldStartVnode,newEndVnode)){
            // 第三种情况:旧前 和 新后
            console.log("3")
            patchVnode(oldStartVnode,newEndVnode);
            if(newEndVnode) newEndVnode.elm = oldStartVnode?.elm;
            // 把旧前指定的节点移动到旧后指向的节点的后面
            parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling);
            oldStartVnode= oldCh[++oldStartIdx];
            newEndVnode= newCh[--newEndIdx];

        }else if(sameVnode(oldEndVnode,newStartVnode)){
            // 第四中情况:旧后 和 新前
            console.log("4")
            patchVnode(oldEndVnode,newStartVnode);
            if(newStartVnode) newStartVnode.elm = newEndVnode?.elm;
            // 把旧后指定的节点移动到旧前指向的节点的前面
            parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm);
            oldEndVnode= oldCh[--oldEndIdx];
            newStartVnode= newCh[++newStartIdx];

        }else{
            // 第五种情况:以上都不满足条件 ===》 查找
        }
    }
}

8.手写diff算法-判断第五种情况

 

// updateChildren.js
import patchVnode from './patchVnode'
import createElement from './createElement'
// 判断两个虚拟节点是否为同一个节点
function sameVnode(vnode1,vnode2){
    return vnode1.key == vnode2.key;
}
// 参数一:真实的dom节点
// 参数二:旧的虚拟节点
// 参数三:新的虚拟节点
export default (parentElm, oldCh, newCh) => {
    let oldStartIdx = 0;            //旧前的指针
    let oldEndIdx = oldCh.length-1; //旧后的指针
    let newStartIdx = 0;            //新前的指针
    let newEndIdx = newCh.length-1; //新后的指针
    
    let oldStartVnode = oldCh[oldStartIdx]; //旧前虚拟节点
    let oldEndVnode = oldCh[oldEndIdx];     //旧后虚拟节点
    let newStartVnode = newCh[newStartIdx]; //新前虚拟节点
    let newEndVnode = newCh[newEndIdx];     //新后虚拟节点

    while( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
        if( oldStartVnode == undefined){
            oldStartVnode = oldCh[++oldStartIdx];
        }if( oldEndVnode == undefined){
            oldEndVnode = oldCh[--oldEndIdx];
        }else if( sameVnode(oldStartVnode,newStartVnode)){
            // 第一种情况:旧前 和 新前
            console.log("1")
            patchVnode(oldStartVnode,newStartVnode);
            if(newStartVnode) newStartVnode.elm = oldStartVnode?.elm;
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];

        }else if(sameVnode(oldEndVnode,newEndVnode)){
            // 第二种情况:旧后 和 新后
            console.log("2")
            patchVnode(oldEndVnode,newEndVnode);
            if(newEndVnode) newEndVnode.elm = oldEndVnode?.elm;
            oldEndVnode= oldCh[--oldEndIdx];
            newEndVnode= newCh[--newEndIdx];

        }else if(sameVnode(oldStartVnode,newEndVnode)){
            // 第三种情况:旧前 和 新后
            console.log("3")
            patchVnode(oldStartVnode,newEndVnode);
            if(newEndVnode) newEndVnode.elm = oldStartVnode?.elm;
            // 把旧前指定的节点移动到旧后指向的节点的后面
            parentElm.insertBefore(oldStartVnode.elm,oldEndVnode.elm.nextSibling);
            oldStartVnode= oldCh[++oldStartIdx];
            newEndVnode= newCh[--newEndIdx];

        }else if(sameVnode(oldEndVnode,newStartVnode)){
            // 第四中情况:旧后 和 新前
            console.log("4")
            patchVnode(oldEndVnode,newStartVnode);
            if(newStartVnode) newStartVnode.elm = newEndVnode?.elm;
            // 把旧后指定的节点移动到旧前指向的节点的前面
            parentElm.insertBefore(oldEndVnode.elm,oldStartVnode.elm);
            oldEndVnode= oldCh[--oldEndIdx];
            newStartVnode= newCh[++newStartIdx];

        }else{
            // 第五种情况:以上都不满足条件 ===》 查找
            console.log('5');
            // 创建一个对象,存虚拟节点的(判断新旧有没有相同节点)
            const keyMap = {};
            for(let i = oldStartIdx;i<=oldEndIdx;i++){
                const key = oldCh[i]?.key;
                if( key ) keyMap[key] = i;
            }

            // 在旧节点中寻找新前指向的节点
            let idxInOld = keyMap[newStartVnode.key];
            // 如果有,说明数据在新旧虚拟节点中都存在
            if(idxInOld){
                const elmMove = oldCh[idxInOld];
                patchVnode(elMove,newStartVnode);
                // 处理过的节点,在旧虚拟节点的数组中,设置为undefined
                oldCh[idxInOld] = undefined;
                parentElm.insertBefore(elMove.elm,oldStartVnode.elm);
            }else{
                // 如果没有找到 ==》 说明是一个新的节点【创建】
                parentElm.insertBefore( createElement(newStartVnode),oldStartVnode.elm);
            }
            // 新数据(指针) +1
            newStartVnode = newCh[++newStartIdx];
        }
    }
    // 结束while 只有两种情况 (新增和删除)
    // 1.oldStartIdx > oldEndIdx
    // 2.newStartIdx > newEndIdx
    if(oldStartIdx > oldEndIdx){
        // 进入新增操作
        const before = newCh[newEndIdx+1] ? newCh[newEndIdx+1].elm : null;
        for( let i=newStartIdx; i<=newEndIdx;i++){
            parentElm.insertBefore(createElement(newCh[i],before));
        } 
    } else {
        // 进入删除操作
        for(let i=oldStartIdx;i<=oldEndIdx;i++){
            parentElm.removeChild(oldCh[i].elm);
        }
    }
}

9.谈一下MVVM框架

 web1.0时代

        文件全在一起,也就是前端和后端的代码全在一起

        问题:

                1.前端和后端都是一个人开发。(技术没有侧重点或者责任不够细分)

                2.项目不好维护

                3.html、css、js页面的静态内容没有,后端是没有办法工作的(没办法套数据)

web2.0时代

        ajax出现了,就可以:前端和后端数据分离了

        解决问题:后端不用等前端页面弄完没,后端做后端的事情(写接口),前端布局、特效、发送请求

        问题:

                1.html、css、js都在一个页面中,单个页面可能内容也比较多的(也会出现不好维护的情况)

出现前端框架MVC、MVVM

        解决问题:可以把一个“特别大”页面,进行拆分(组件化),单个组件进行维护

什么是MVVM

        Model-View-ViewModel的简写

        

view:视图【dom ==》 在页面中展示的内容】

model:模型【数据层:vue中的data数据】

viewModel:视图模型层【就是vue源码】

学了快一周的源码了,煎熬 


网站公告

今日签到

点亮在社区的每一天
去签到