(四)Vue原理

182 阅读9分钟

@TOC

Vue原理(大厂必考)

面试为何会考察原理

面试中如何考察?以何种方式

考察重点,而不是考察细节,掌握好2/8原则 和使用相关联的原理,例如vdom、模板渲染 整体流程是否全面?热门技术是否有深度?

Vue原理包括哪些?

组件化 响应式 vdom和diff 模板编译 渲染过程 前端路由

如何理解MVVM

组件化基础

“很久以前”就有组件化

asp jsp php已经有组件化了 nodejs中也有类似的组件化 在这里插入图片描述 在这里插入图片描述

数据驱动视图(MVVM,setState)

传统组件,只是静态渲染,更新还要依赖于操作DOM 数据驱动视图--Vue MVVM 数据驱动视图--React setState(暂时先不看)

Vue MVVM

在这里插入图片描述 在这里插入图片描述

总结

组件化 数据驱动视图 MVVM

监听data变化的核心API是什么

Vue响应式

组件data的数据一旦变化,立刻触发视图的更新 实现数据驱动视图的第一步 考察Vue原理的第一题 核心API-Object.defineProperty 如何实现响应式,代码演示 Object.defineProperty的一些缺点(Vue3.0启用Proxy) 在这里插入图片描述

Proxy有兼容性问题

Proxy兼容性不好,且无法polyfill Vue2.x还会存在一段时间,所以都得学 Vue3.0相关知识,下一章将,这里只是先提一下

Object.defineProperty基本用法

在这里插入图片描述

Object.defineProperty实现响应式

监听对象,监听数组 复杂对象,深度监听 几个缺点

如何深度监听data变化、数组变化

Object.defineProperty缺点

深度监听,需要递归到底,一次性计算量大 无法监听新增属性/删除属性(Vue.set Vue.delete) 无法原生监听数组,需要特殊处理

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

总结

基础API-Object.defineProperty 如何监听对象(深度监听),监听数组 Object.defineProperty的缺点

虚拟DOM-面试里的网红

虚拟DOM(Virtual DOM)和diff

vdom是实现vue和React的重要基石 diff算法是vdom中最核心、最关键的部分 vdom是一个热门话题,也是面试中的热门话题 DOM操作非常耗费性能 以前用jQuery,可以自行控制DOM操作的时机,手动调整 Vue和React是数据驱动视图,如何有效控制DOM操作?

解决方案-vdom

有了一定复杂度,想减少计算次数比较难 能不能把计算,更多的转移为JS计算?因为JS执行速度很快 vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM 在这里插入图片描述

通过snabbdom学习vdom

简洁强大的vdom库,易学易用 Vue参考它实现的vdom和diff github.com/snabbdom/sn… Vue3.0重写了vdom的代码,优化了性能 但vdom的基本理念不变,面试考点也不变 React vdom具体实现和Vue也不同,但不妨碍统一学习

用过虚拟DOM吗?

snabbdom重点总结

h函数 vnode数据结构 patch函数

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
    <script src="./demo1.js"></script>
</body>
</html>

const snabbdom = window.snabbdom

// 定义 patch
const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners
])

// 定义 h
const h = snabbdom.h

const container = document.getElementById('container')

// 生成 vnode
const vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item 1'),
    h('li.item', {}, 'Item 2')
])
patch(container, vnode)

document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item B'),
        h('li.item', {}, 'Item 3')
    ])
    patch(vnode, newVnode)
})
//table-without-vdom.html 
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
    <script type="text/javascript">
        const data = [
            {
                name: '张三',
                age: '20',
                address: '北京'
            },
            {
                name: '李四',
                age: '21',
                address: '上海'
            },
            {
                name: '王五',
                age: '22',
                address: '广州'
            }
        ]

        // 渲染函数
        function render(data) {
            const $container = $('#container')

            // 清空容器,重要!!!
            $container.html('')

            // 拼接 table
            const $table = $('<table>')

            $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
            data.forEach(item => {
                $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
            })

            // 渲染到页面
            $container.append($table)
        }

        $('#btn-change').click(() => {
            data[1].age = 30
            data[2].address = '深圳'
            // re-render  再次渲染
            render(data)
        })

        // 页面加载完立刻执行(初次渲染)
        render(data)

    </script>
</body>
</html>
//table-with-vdom.html 
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
    <script type="text/javascript">
        const snabbdom = window.snabbdom
        // 定义关键函数 patch
        const patch = snabbdom.init([
            snabbdom_class,
            snabbdom_props,
            snabbdom_style,
            snabbdom_eventlisteners
        ])

        // 定义关键函数 h
        const h = snabbdom.h

        // 原始数据
        const data = [
            {
                name: '张三',
                age: '20',
                address: '北京'
            },
            {
                name: '李四',
                age: '21',
                address: '上海'
            },
            {
                name: '王五',
                age: '22',
                address: '广州'
            }
        ]
        // 把表头也放在 data 中
        data.unshift({
            name: '姓名',
            age: '年龄',
            address: '地址'
        })

        const container = document.getElementById('container')

        // 渲染函数
        let vnode
        function render(data) {
            const newVnode = h('table', {}, data.map(item => {
                const tds = []
                for (let i in item) {
                    if (item.hasOwnProperty(i)) {
                        tds.push(h('td', {}, item[i] + ''))
                    }
                }
                return h('tr', {}, tds)
            }))

            if (vnode) {
                // re-render
                patch(vnode, newVnode)
            } else {
                // 初次渲染
                patch(container, newVnode)
            }

            // 存储当前的 vnode 结果
            vnode = newVnode
        }

        // 初次渲染
        render(data)


        const btnChange = document.getElementById('btn-change')
        btnChange.addEventListener('click', () => {
            data[1].age = 30
            data[2].address = '深圳'
            // re-render
            render(data)
        })

    </script>
</body>
</html>

vdom总结

用JS模拟DOM结构(vnode) 新旧vnode对比,得出最小的更新范围,最后更新DOM 数据驱动视图的模式下,有效控制DOM操作

虚拟DOM-diff算法概述

diff算法是vdom中最核心、最关键的部分 diff算法能在日常使用vue React中体现出来(如key) diff算法是前端热门话题,面试“宠儿” diff即对比,是一个广泛的感念,如linux diff命令、git diff等 两个js对象也可以做diff,如github.com/cujojs/jiff 两棵树做diff,如这里的vdom diff 在这里插入图片描述

树diff的时间复杂度O(n^3)

第一,遍历tree1;第二,遍历tree2 第三,排序 1000个节点,要计算1亿次,算法不可用

优化时间复杂度到O(n)

只比较同一层级,不跨级比较 tag不相同,则直接删掉重建,不再深度比较 tag和key,两者都相同,则认为是相同节点,不再深度比较 在这里插入图片描述 在这里插入图片描述

深入diff算法源码

生成vnode

h函数

patch函数

执行pre hook 第一个参数不是vnode-创建一个空的vnode(emptyNodeAt),关联到这个DOM元素 判断vnode是否相同(sameVnode)-key和sel都相等 相同执行patchVnode 不相同,直接删掉重建

patchVnode函数(vnode对比)

  • 执行prepatch hook(生命周期的钩子)

  • 设置vnode.elem

  • vnode.text===undefined(vnode.children一般有值)

     新旧都有children,updateChildren;
     新children有,旧children无(旧text有),清空text,添加children(addVnodes);
     旧children有,新children无,移除children(removeVnodes);
     旧text有,清空
    
  • vnode.text!==undefined(vnode.children一般无值)

     新旧text不一样,移除旧children,设置新text
    

updateChildren函数

在这里插入图片描述

  • 开始和开始对比 patchVnode() 累加累减
  • 结束和结束对比 patchVnode() 累加累减
  • 开始和结束对比 patchVnode() 累加累减
  • 结束和开始对比 patchVnode() 累加累减
  • 以上四个都未命中 拿新节点key,能否对应上oldCh中的某个节点的key 没对应上,New element 对应上,拿到对应上key的节点,判断sel是否相等,不相等New element,相等patchVnode()

在这里插入图片描述 在这里插入图片描述 不使用key全部删掉然后插入,使用key直接移动过来,不用做销毁然后重新渲染的过程

虚拟DOM-考点总结和复习

diff算法总结

patchVnode addVnodes removeVnodes updateChildren(key的重要性)

vdom和diff-总结

细节不重要,updateChildren的过程也不重要,不要深究 vdom核心概念很重要:h、vnode、patch、diff、key等 vdom存在的价值更加重要:数据驱动视图,控制DOM操作

模板编译前置知识点-with语法

模板编译

模板是vue开发中最常用的部分,即与使用相关联的原理 它不是html,有指令、插值、JS表达式,到底是什么 面试不会直接问,但会通过“组件渲染和更新过程”考察 前置知识:JS的with语法 vue template complier将模板编译为render函数 执行render函数生成vnode

with语法

改变{}内自由变量的查找规则,当做obj属性来查找 如果找不到匹配的obj属性,就会报错 with要慎用,它打破了作用域规则,易读性变差

const obj = {a:100,b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) //undefined

//使用with,能改变{}内自由变量的查找方式
//使用{}内自由变量,当做obj的属性来查找
with(obj) {
	console.log(a)
	console.log(b)
	console.log(c) //会报错!!!
}

vue模板被编译成什么?

模板不是html,有指令、插值JS表达式,能实现判断、循环 html是标签语言,只有JS才能实现判断、循环(图灵完备的:能实现顺序执行、判断、循环) 因此,模板一定是转换为某种JS代码,即编译模板

const compiler = require('vue-template-compiler')

// 插值
// const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return createElement('p',[createTextVNode(toString(message))])}
//this->new Vue({....})
// h -> vnode
// createElement -> vnode

// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // 属性和动态属性
// const template = `
//     <div id="div1" class="container">
//         <img :src="imgUrl"/>
//     </div>
// `
// with(this){return _c('div',
//      {staticClass:"container",attrs:{"id":"div1"}},
//      [
//          _c('img',{attrs:{"src":imgUrl}})])}

// // 条件
// const template = `
//     <div>
//         <p v-if="flag === 'a'">A</p>
//         <p v-else>B</p>
//     </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
// const template = `
//     <ul>
//         <li v-for="item in list" :key="item.id">{{item.title}}</li>
//     </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
// const template = `
//     <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }

编译模板

模板编译为render函数,执行render函数返回vnode 基于vnode再执行patch和diff(后面会讲) 使用webpack vue-loader,会在开发环境下编译模板(重要)

vue组件中使用render代替template

在这里插入图片描述

讲完模板编译,再讲这个render,就比较好理解了 在有些复杂情况中,不能用template,可以考虑用render React一直都用render(没有模板),和这里一样

总结

with语法 模板到render函数,再到vnode,再到渲染和更新 vue组件可以用render代替template

回顾和复习已学的知识点

总结组件 渲染/更新过程

一个组件渲染到页面,修改data触发更新(数据驱动视图) 其背后原理是什么,需要掌握哪些要点 考察对流程了解的全面程度

回顾学过的知识

响应式:监听data属性getter setter(包括数组) 模板编译:模板到render函数,再到vnode vdom:patch(elem,vnode)和patch(vnode,newVnode)

组件 渲染/更新过程

初次渲染过程 更新过程 异步渲染

vue组件是如何渲染和更新的

初次渲染过程

解析模板为render函数(或在开发环境已完成,vue-loader) 触发响应式,监听data属性getter setter 执行render函数,生成vnode,patch(elem,vnode)

执行render函数会触发getter

<p>{{message}}</p>
<script>
export default {
	data(){
		return {
			message:'hello',//会触发get
			city:'北京'//不会触发get,因为模板没用到,即和视图没关系
		}
	}
}
</script>

更新过程

修改data,触发setter(此前在getter中已被监听) 重新执行render函数,生成newVnode patch(vnode,newVnode)

完成流程图

在这里插入图片描述

异步渲染

回顾$nextTick 汇总data的修改,一次性更新视图 减少DOM操作次数,提高性能 在这里插入图片描述

总结1

渲染和响应式的关系 渲染和模板编译的关系 渲染和vdom的关系

总结2

初次渲染过程 更新过程 异步渲染

如何用JS实现hash路由

前端路由原理

稍微复杂一点的SPA,都需要路由 vue-router也是vue全家桶的标配之一 属于“和日常使用相关联的原理”,面试常考 回顾vue-router的路由模式 hash H5 history 在这里插入图片描述

hash的特点

hash变化会触发网页跳转,即浏览器的前进、后退 hash变化不会刷新页面,SPA必须的特点 hash永远不会提交到server端(前端自生自灭)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>hash test</title>
</head>
<body>
    <p>hash test</p>
    <button id="btn1">修改 hash</button>

    <script>
        // hash 变化,包括:
        // a. JS 修改 url
        // b. 手动修改 url 的 hash
        // c. 浏览器前进、后退
        window.onhashchange = (event) => {
            console.log('old url', event.oldURL)
            console.log('new url', event.newURL)

            console.log('hash:', location.hash)
        }

        // 页面初次加载,获取 hash
        document.addEventListener('DOMContentLoaded', () => {
            console.log('hash:', location.hash)
        })

        // JS 修改 url
        document.getElementById('btn1').addEventListener('click', () => {
            location.href = '#/user'
        })
    </script>
</body>
</html>

如何用JS实现H5 history路由

H5 history

用url规范的路由,但跳转不刷新页面 history.pushState window.onpopstate

正常页面浏览

github.com/xxx 刷新页面 github.com/xxx/yyy 刷新页面 github.com/xxx/yyy/zzz 刷新页面

改造成H5 history模式

github.com/xxx 刷新页面 github.com/xxx/yyy 前端跳转,不刷新页面 github.com/xxx/yyy/zzz 前端跳转,不刷新页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>history API test</title>
</head>
<body>
    <p>history API test</p>
    <button id="btn1">修改 url</button>

    <script>
        // 页面初次加载,获取 path
        document.addEventListener('DOMContentLoaded', () => {
            console.log('load', location.pathname)
        })

        // 打开一个新的路由
        // 【注意】用 pushState 方式,浏览器不会刷新页面
        document.getElementById('btn1').addEventListener('click', () => {
            const state = { name: 'page1' }
            console.log('切换路由到', 'page1')
            history.pushState(state, '', 'page1') // 重要!!
        })

        // 监听浏览器前进、后退
        window.onpopstate = (event) => { // 重要!!
            console.log('onpopstate', event.state, location.pathname)
        }

        // 需要 server 端配合,可参考
        // https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
    </script>
</body>
</html>

总结

hash--window.onhashchange H5 history--history.pushState和window.onpopstate H5 history需要后端支持

两者选择

to B的系统推荐用hash,简单易用,对url规范不敏感 to C的系统,可以考虑选择H5 history,但需要服务端支持 能选择简单的,就别用复杂的,要考虑成本和收益

vue原理-考点总结和复习

组件化

组件化的历史 数据驱动视图 MVVM

响应式

Object.defineProperty 监听对象(深度),监听数组 Object.defineProperty的缺点(Vue3用Proxy,后面会讲)

vdom和diff

应用背景 vnode结构 snabbdom使用:vnode h patch

模板编译

with语法 模板编译为render函数 执行render函数生成vnode

渲染过程

初次渲染过程 更新过程 异步渲染

前端路由

hash H5 history 两者对比