大厂面试必考之vue原理!纯干货3万4千多字符!面试为何会考原理? 面试中如何考察?以何种方式? vue原理包括哪些? 面试为何考察原理,又用不到?

275 阅读18分钟

u=2553087719,3107156948&fm=253&fmt=auto.jpeg

vue原理(大厂必考)

面试为何会考原理?

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

vue原理包括哪些?

面试为何考察原理,又用不到?

  • 知其然知其所以然--各行业通用的道理

  • 了解原理,才能应用的更好(竞争激烈,择优录取)

    比如:一个API用的熟但不知道原理,一个不熟但知道原理,会优先录用后者

  • 大厂造轮子(有钱有资源,业务定制,技术KPI)

    大厂有时需要造轮子,只会用API肯定不行

面试如何考察Vue原理?

1、我们讲的是面试时的考察重点,而不是考察细节。掌握好2 8原则。

因为面试是个非常现实的问题,不是为了考你而考你,而是为了筛选和区分候选者。筛选也有成本,因此会用20%时间了解你80%重要的东西。

2、和使用相关联的原理,例如vdom、模版渲染

3、会考察整体流程是否全面?

是否闭环,很多工作时需要你闭环的。

4、热门技术是否有深度?

是否对热门技术有关注。广度差不多,优先考虑深度的候选人。

组件化

响应式

vdom和diff

模版编译

渲染过程

前端路由

组件化基础

很久以前就有组件化

数据驱动视图(vue:MVVM,react:setState)

对比Vue React的组件化

image-20220520020017312.png

数据驱动视图

image-20220520020112792.png Mvvm:数据驱动视图。通过修改视图就能改变数据,反过来也可以。像现在很多管理系统,如果是操作dom的方式改变视图,复杂程度是没办法想象的,有了mvvm,就可以直接修改数据就能改变视图。

官网图:

image-20220520020720571.png 具体到代码中是怎么样的?

image-20220520021726461.png

vue响应式(重点中的重点)

  • 也就是组件data的数据一旦变化,立刻触发视图的更新
  • 是实现数据驱动视图的第一步
  • 会是考察vue原理的第一题
  • 核心API-Object.defineProperty

  • 如何实现响应式,代码演示

  • Object.defineProperty的一些缺点()

    但也不意味着proxy就一定好,不能100%使用,

  • proxy存在兼容性问题,且无法polifill

Object.defineProperty基本用法

image-20220520022814459.png

Object.defineProperty实现响应式

监听对象,监听数组

复杂对象,深度监听

几个缺点

代码演示

安装并启动服务:

image-20220520023140823.png observe-demo文件夹

observe.js文件展示

// 触发更新视图
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()
    //     ...
    // }// 所以需要重新定义个原型
    // data.nums.push(4) 走的是这里
    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) // 21 说明defineReactive的参数value一直存在内存中 
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所以有 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

所以,我们可以提炼出

Object.defineProperty()缺点:

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

那如何完善?

完善方式:

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);

控制台看效果: image.png

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // 相当于Array.prototype.push.call(this, ...arguments)
    }
})

总结:

image-20220521013809635.png

虚拟DOM(Virtual DOM)和diff

image-20220521013945733.png image-20220521014043831.png 这么多dom,怎么知道操作正确

vdom最早是react提出来的,vue2引入了

解决方案-vdom(Virtual DOM)

image-20220521014328443.png

请用js模拟以下dom

image-20220521014625892.png 答题时,起码要有标签,属性,事件。

以上只是个基础,不同框架规范不一样。

模拟之后怎么去用?

通过snabbdom这个工具学习vdom(面试官:用过虚拟dom么)

snobby:[ˈsnɒbi],adj. 势利的;

image-20220521014756684.png snabbdom部分源码展示: image-20220521045839062.png

可以看出,vnode和我们上图的结构挺像的。用代码演示下用法。

代码演示

目录snabbdom-demo文件夹

image-20220521204908622.png patch一个用法是,直接渲染到dom当中,一个用法是更新dom。

更新前:

image-20220521205054235.png 更新后:

image-20220521205553166.png

只有2,3元素闪动了,说明只有2,3更新。

当它特别复杂的时候,我们就很难对比出哪些需要更新哪些不需要更新。

Vdom核心:只有改变的才更新。

用jq呢?

image-20220521210842383.png 不使用snabbdom代码:

<!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>

那能不能在jq里实现,只更新那两条数据?可以自己写,写到最后发现自己造了个跟snabbdom差不多的轮子/库。但是不现实,复杂之后你是很难宅出来的。

用了snabbdom这个库:

<!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>

结果展示: image-20220521212659459.png 现在就只更新了两条。

这就是使用了vdom的价值,我们不用关心哪些变,哪些没变,往vdom上面一扔就好,自动会跟我们对比出来。

image-20220521015006058.png 关注整个流程的全面度,不要陷入细节,较真,不然精力是不够的。

snabbdom的重点总结

  • h函数
  • vnode数据结构
  • patch函数

vdom总结

  • 用js模拟dom结构(vnode)

  • 新旧vdnode对比,得出最小的更新范围,最后更新dom

    不管你需求多复杂,多少人操作,都根据这个机制,得出最小范围,然后更新,这样数据量再大,性能也能不错。

  • 数据驱动视图的模式下,有效控制dom操作

Vue react 作为最流行的俩框架,有成千上万人操作dom,你无法控制每个人的操作,怎么数据驱动视图,你也没办法控制,那怎么去满足所有的需求呢?那就是使用vdom。

diff(对比)算法

新旧vdnode对比,得出最小的更新范围,这个算法就是diff算法。

image-20220522000611449.png

面试题:为何要在v-for中要有key?

学完就有答案了。

diff算法细节知道了,应对大厂框架的问题,在深度上是够了的,到顶了。

diff算法是什么

  • diff(对比)算法没有特殊描述,常指vdom的diff算法。但它不是vdom独创的,是一个广泛的概念,如linux diff命令、git diff等。

    所以你要是跟后端说diff算法,他的理解可能和你不一样。

  • 两个js对象也可以做diff, 如:github.com/cujojs/jiff

  • 两颗树做diff,如这里的vdom diff。

image-20220522001905660.png

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

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

结论,不可用。

除此,O(n^2)也不常用,比如冒泡,数据一大还是不行的。

这就设计之初,最大的难题。

最终大神们优化后,时间复杂度只有O(n)

image-20220522003518503.png

diff算法概述:图示讲解

image-20220522003911544.png

image-20220522004407334.png

Snabbdom源码解读

会过一下,千万不要较真,面试也不会考这些很细的地方。

不要认为自己看不懂,跟着来就好。

地址:github.com/snabbdom/sn…

源码克隆下来。目录snabbdom-source文件夹。

重点看h函数,patch,vnode函数

h函数:s r c >h.ts

import { vnode, VNode, VNodeData } from './vnode';
export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
import * as is from './is';
​
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg';
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      let childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
      }
    }
  }
}
​
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode;
export function h (sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {}, children: any, text: any, i: number;
  if (c !== undefined) {
    if (b !== null) { data = b; }
    if (is.array(c)) {
      children = c;
    } else if (is.primitive(c)) {
      text = c;
    } else if (c && c.sel) {
      children = [c];
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b;
    } else if (is.primitive(b)) {
      text = b;
    } else if (b && b.sel) {
      children = [b];
    } else { data = b; }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    addNS(data, children, sel);
  }
​
  // 返回 vnode
  return vnode(sel, data, children, text, undefined);
};
export default h;

vnode函数:

vnode.ts

import { Hooks } from './hooks';
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'export type Key = string | number;
​
export interface VNode {
  sel: string | undefined;
  data: VNodeData | undefined;
  children: Array<VNode | string> | undefined;
  elm: Node | undefined;
  text: string | undefined;
  key: Key | undefined;
}
​
export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: any[]; // for thunks
  [key: string]: any; // for any other 3rd party module
}
​
export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  let key = data === undefined ? undefined : data.key;
  
  return { sel, data, children, text, elm, key };
}
​
export default vnode;
​

重点提炼:

  • 返回值是一个对象
  • children, text是不能共存的,子元素要么是个文本,要么是个dom节点。
  • elm是渲染到哪个元素上去的

init函数

看看怎么来的:init返回出来的。

image-20220522013016796.png snabbdom.ts源码中找init。

init()中的patch函数
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  function emptyNodeAt (elm: Element) {
    const id = elm.id ? '#' + elm.id : '';
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    // 注意:最后返回值中传入的是elm,就是说把空的vnode跟elm绑定。
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [],     undefined, elm);
  }
  xxx省略xxx
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行 pre hook
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
​
    // 第一个参数不是 vnode
    if (!isVnode(oldVnode)) {
      // 创建一个空的 vnode ,关联到这个 DOM 元素
      // 一个vnode必须要有一个dom元素做关联,不然不知道更新到哪里去
      oldVnode = emptyNodeAt(oldVnode);
    }
​
    // 相同的 vnode(key 和 sel 都相等)
    if (sameVnode(oldVnode, vnode)) {
      // vnode 对比
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    
    // 不同的 vnode ,直接删掉重建
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm);
​
      // 重建
      createElm(vnode, insertedVnodeQueue);
​
      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
​
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}

cbs:callback

源码中的hook是什么意思

image-20220522014029213.png

image-20220522014506416.png 调用的就是这个生命周期。

如果都没有传key呢?

第一个参数是sel,都相同。

image-20220522022258741.png

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等 
  // 如果都没有传key呢
  // 如果都没传,结果是true:undefined === undefined // true
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

如果都没传,结果是true。

当然,这是单独直接写出来的,可以不传key;如果在循环体里面,就必须要传key。

第一次patch ,因为传入的不是一个vnode,就会先创建一个空的 vnode ,关联到这个 DOM 元素,然后可能会命中重建。

其他函数见源码解读注释。

updateChildren()函数

最复杂的一个

image-20220523191855626.png

function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
		// 当旧开始小于等于旧结束,且,新开始小于新结束时
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 做为空处理
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];

      // 开始和开始对比 ,判断是否是相同节点(key 和 sel 都相等 就说明是相同的元素)
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      
      // 结束和结束对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

      // 开始和结束对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

      // 结束和开始对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];

      // 以上四个都未命中(也就是没有旧开始等于新开始,...四种特殊情况)
      // 就会看新节点的key值,是否跟旧的某个节点的key相等
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
  
        // 没对应上,比如上图中的e,和旧的节点没一个对应上,
        // 就会重建或插入节点
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];
        
        // 对应上了 但也不能说明是相同节点 还要咋样?比对sel是否相等
        } else {
          // 对应上 key 的节点 
          elmToMove = oldCh[idxInOld];

          // sel 是否相等(参考sameVnode 的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // 不相等就会重建或插入节点
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          
          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

while干的活儿图解

image-20220523193644924.png 开始/结束,和,开始结束 进行排列组合对比 (2*2=4种情况),通过sameVnode()判断是否是相同节点(key 和 sel 都相等 就说明是相同的元素)。

新旧节点如何对比?用sameVnode()函数。

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等 就说明是相同的元素
  // undefined === undefined // true
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
key在源码中的作用

有了key,会导致updateChildren算法更高效

为什么高效?

image-20220524042745156.png 因为:在以上四个都未命中(也就是没有旧开始等于新开始,...四种特殊情况) 的情况下,就会看新节点的key值,是否跟旧的某个节点的key相等;不想等,就会新建节点;相等,再看sel是否相等;不相等,就会新建节点;相等,就patchVnode。

总结不使用keys和使用keys区别

不使用keys,老节点就会全部删掉,然后插入;使用了keys,就可以知道哪个是相同的,就可以直接移动到新的节点里去,避免旧的节点全部销毁,然后重建新的节点。

是不是使用了keys就万事大吉了?

也不是,假如你使用的是随机数,,每次不一样,也白搭;假如使用的是index值,你之前的节点有了顺序变化,就也会出现问题;

diff算法总结

patchVnode

addVnodes 和 removeVnodes

updateChildren(key的重要性)

vdom和diff算法-总结

细节不重要,updateChildren的过程也不重要,不要深究

vdom核心概念很重要:h\vnode\patch\diff\key等。

vdom存在的价值更加重要:数据驱动视图 ,控制dom操作。

模版编译

老师用的是剧情推动式的讲解方式。

  • 模版是vue开发中常用部分,即与使用相关联的原理。
  • 它不是html,有指令、插值、JS表达式,到底是什么?
  • 面试不会直接问,一般会通过问“组件渲染和更新过程”来考察对vue整个流程的全面性。

可能相关面试题:浏览器如何渲染页面?

前置知识:

  • js的with语法
  • Vue template complier将模版编译为render函数
  • 执行render函数生成vnode

js的with语法

image-20220524053148453.png

image-20220524053302042.png 要用,移动要和组内,或者高工,打招呼。

编译模版

1、模版不是html,有指令、插值、js表达式,能实现判断、循环

html只是个标签语言,配合css能实现显示隐藏,但实现不了判断、循环,所以模版编译一定是某种js。

2、html、css都不是图灵完备语言,只有js才能是。

什么是图灵完备的语言?就是能实现数据循环,判断,执行逻辑这三种的。

3、因此,模版一定是转换为某种js代码,即编译模版

编译模版-代码演示

安装

image-20220526000405808.png 安装完之后

image-20220526000456563.png 新建index.js文件

插值表达式编译成啥?
// 导入
const compiler = require('vue-template-compiler')
// 插值
const template = `<p>{{message}}</p>`
// 编译
const res = compiler.compile(template)
console.log(res.render)

image-20220526000924077.png 打印结果:

image-20220526001421736.png

this是什么?
const vm = new Vue({...}) // this是vue实例
下划线(_xxx)是什么?

指this,会从this里查找,也就是从vue的实例里找。

其中

image-20220526001803774.png 这个挺像h函数

image-20220526001859343.png 为什么没{},因为p标签里没有元素,只有一个字符串。

“_ c”是什么?

扒vue的没压缩的源码,找到“_ c”、“_ v”等,是什么?

// 从 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;
}

“_ c”不在此处,在别处,其意思是:“createElement”,比叫h函数,更具语义。

现在替换下打印的结果:

// with(this){return createElement('p',[createTextVNode(toString(message))])}

对比下之前打印的:

image-20220526001421736.png 之前:执行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'))])}

还是js代码,没什么神奇魔术之类的,执行这个js,还是返回vnode。

属性和动态属性编译成?
// // 属性和动态属性
 const template = `
     <div id="div1" class="container">
         <img :src="imgUrl"/>
     </div>
 `

image-20220526005757842.png

条件编译成?
// 条件
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")])])}

// target._v = createTextVNode;
// v-if,v-else变成了三元表达式
循环编译成?
// 循环
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)}

// target._l = renderList; 这个实现也不难,返回若干个li标签
v-model原理?

面试题:请说一下v-model实现双向绑定的原理?

// 面试题:请说一下v-model实现双向绑定的原理
// 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}}})}

看 input 事件:

根据模版去渲染input的时候,就已经给他挂了个input事件,所以说它每次去input的时候,都能去把变量,进行更新,这样一更新,就能触发我们重新渲染,就能把新的name放到value上。

input渲染的时候,有几个要点:

  • 1,value赋值的是个变量,变量的值取自this,也就是vue实例。
  • 2,给input绑定了个on监听事件,将监听到的value值,赋值给实例的name (name=$event.target.value)。
总结:

以上函数都叫render 函数。

返回的都是 vnode 有了这个,就可以去做渲染,怎么渲染,之前讲过。至此,就可以了。

就不要去深挖'vue-template-compiler'包又做了什么了,违反了之前讲过的“和使用相关”的原则。

image-20220526015645358.png

使用webpack的vue-loader优化

如果用webpack或者vue-cli的话,上面这部分工作,都是在开发环境下做的,所以产出的代码里就没有模版,全部变成了render函数的形式。

什么情况下不是开发环境下编译?

比如,你就是新建了个html,引入了vue.js,写个简单例子,那它就是在浏览器运行时编译的,而编译模版开销还是比较大的,导致这样比较慢。

所以,做项目时,一定要集成webpack的vue-loader,这样就可以在开发环境下去做编译模版。

vue组件中使用render代替template

image-20220526060136685.png 要是没有学模版编译,就不知道以上时干嘛,或变一下也不知道。

image-20220526060246564.png

总结

image-20220526060511927.png

总结组件的渲染/更新过程

image-20220526060842015.png

回顾vue三大核心知识点

1、响应式:监听data 属性getter setter(包括数组)

之前写过一个observe.js,在observe-demo文件夹下

核心代码

// 重新定义属性,监听起来
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()
            }
        }
    })
}

核心 API:Object.defineProperty

深度监听:observer(value)

// 监听对象属性
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 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)
    }
})

2、模板编译:模板到render函数,再到vnode

见模版编译

3、vnode:patch(elem,vnode)和patch(vnode,newVnode)

image-20220526062915144.png

组件渲染/更新过程

初次渲染过程

image-20220526074220314.png

image-20220526075012147.png 即:city修改的时候,不会被监听到,即触发不到setter。

更新过程

image-20220526075221042.png

完成流程图

面试的时候能把这个图,边画边讲出来,就89十分了。

image-20220526075500574.png

异步渲染(这个概念非常重要)

回顾$nextTick

image-20220526080056138.png

image-20220526080140723.png 如果不加$nextTick,每次获取的都是错误的。

因为vue的组件是异步渲染的, $nextTick待dom渲染完再回调

不太理解:老师说,虽然我们修改了三次,但是,页面渲染的时候,会将data的修改做整合,多次data的修改,会整合起来渲染一次。所以说,虽然我门修改了三次,但是nextTick只调用了一次。

所以vue异步渲染的作用:

1、汇总data的修改,一次性更新视图。

2、减少dom操作次数,提高性能。

如果说修改一次数据,就渲染一次,浏览器是受不了的。

不光是vue,react也是异步渲染的。

总结1

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

vue最核心的三个概念,串起来,就是模版的渲染和更新过程。

也就是那张图,建议把图手动画一遍。

总结2

初次渲染过程

更新过程

异步渲染(一定要知道)

前端路由原理

通用的,不局限于vue的,react也是。

  • 稍微复杂点的spa,都需要路由(前端的路由,如果是后端的路由,就不叫spa了,就叫多页应用)。
  • Vue-router也是vue全家桶的标配之一
  • 属于“和日常使用相关联的原理”,面试常考

回顾vue-router的路由模式

  • 哈希(hash)
  • H5 history

mode=history,同时也需要后端的支持。

网页url组成部分

image-20220527013454499.png

hash的特点

  • hash的变化会触发网页跳转,即浏览器的前进后退

  • hash的变化不会刷新页面,spa必需的特点。

    像app样,前进后退了

  • 永远不会提交到server端(前端自生自灭,后端不会参与)

代码演示(看看就好)

进入router-demo文件夹,打开终端,使用http-server启动。

image-20220527030342669.png

<!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>

更改hash 的变化

有很多种,包括:

  1. JS 修改 url
  2. 手动修改 url 的 hash,不会触发刷新,会触发前进后退。要是修改的其他部分,就会触发刷新。
  3. 浏览器前进、后退,也可能导致hash的变化。

点击之前

image-20220527031910437.png 点击之后

image-20220527032500593.png 只要知道监听、触发哈希的变化,就好,至于怎么触发的哈希变化,就不要去深究了,违背了“常使用”的原则。

哈希总结

1,监听哈希:window.onhashchange

window.onhashchange = (event) => {
  console.log('old url', event.oldURL) // 完整的url
  console.log('new url', event.newURL)

  console.log('hash:', location.hash) // 哈希
}

2,获取哈希:location.hash

3,改变哈希:三种,手动修改地址栏哈希,浏览器前进后退,js

j s:触发监听,在监听回调里 使用location.href 。

eg:location.href = '#/user'

H5 history

哈希是跟后端没交互的,属于前端路由。

H5 history:

1、是用使用url规范的路由 。

2、跳转时也不刷新页面(当然啊,不然我们spa应用还能用它! )。

3、使用history.pushState和 window.onpopstate实现。

正常页面:

地址后面输入啥,都会刷新页面

image-20220527034941886.png

改造成H5 history模式

image-20220527035200994.png 表现跟哈希是一样的,但是是符合地址规范的。

代码演示

<!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')
            // pushState(对象,空就好,路由的path)
            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>

前进或后退的时候,path都会打印出来。

两个核心API:

1、history.pushState(对象,空就好,路由的path)

【注意】用 pushState 方式,浏览器不会刷新页面

2、window.onpopstate:监听浏览器前进、后退

window.onpopstate = (event) => { // 重要!!
    console.log('onpopstate', event.state, location.pathname)
  // event.state就是我们之前定义的state对象,可以用来传值
}

后端要做的事

image-20220527044127408.png 不管什么地址/路由,统统给我返回到主页面地址,完了,全部通过前端自己的方式去跳转页面,后端不要管。

例如:

image-20220527044416552.png 做了兼容处理,即每次不管什么样的路由,都给我返回主页面,然后,通过

history.pushState(对象,空就好,路由的path)的方式,去做页面的切换,去访问其他路由。

总结

image-20220527044936875.png

两者如何选择

当你的项目,需要做SEO搜索优化的时候,可以去选择复杂的history模式。

总的来说:

image-20220527045226494.png 能简单实现就简单点,别用复杂的,要考虑成本。

vue原理-总结

image-20220527045456378.png

组件化:

image-20220527045617145.png vue实现的方式是mvvm,react实现的方式是setState(没记错的话)

响应式:

image-20220527045834583.png Object.defineProperty缺点有三个。

vdom和diff

首先要用js模拟dom的结构,才能实现虚拟dom(vdom)和diff算法的过程。

image-20220527050321988.png

模版编译

image-20220527050404900.png

组件渲染/更新过程

image-20220527050441279.png 那个图要能画出来!!!

前端路由原理

image-20220527050504799.png

下一篇,开大餐!真题演练!

完结,撒花撒花!

累死宝宝了,3万4千多字符~累趴

image.png