67 Vue面试考察的高频原理

响应式原理

响应式

  • 组件data数据一旦变化,立刻触发视图的更新
  • 实现数据驱动视图的第一步
  • 核心API:Object.defineProperty
    • 缺点
      • 深度监听,需要递归到底,一次计算量大
      • 无法监听新增属性、删除属性(使用Vue.setVue.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: 'shenzhen' // 需要深度监听
        },
        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) // 监听数组
    // proxy-demo
    
    // const data = {
    //     name: 'zhangsan',
    //     age: 20,
    // }
    const data = ['a', 'b', 'c']
    
    const proxyData = new Proxy(data, {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key) // 监听
            }
    
            const result = Reflect.get(target, key, receiver)
            return result // 返回结果
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true
            }
    
            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            // console.log('result', result) // true
            return result // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            // console.log('result', result) // true
            return result // 是否删除成功
        }
    })

vdom和diff算法

1. vdom

  • 背景

    • DOM操作非常耗时
    • 以前用jQuery,可以自行控制DOM操作时机,手动调整
    • VueReact是数据驱动视图,如何有效控制DOM操作
  • 解决方案VDOM

    • 有了一定的复杂度,想减少计算次数比较难
    • 能不能把计算,更多的转移为JS计算?因为JS执行速度很快
    • vdomJS模拟DOM结构,计算出最小的变更,操作DOM
  • 用JS模拟DOM结构

  • 通过snabbdom学习vdom

    • 简洁强大的vdom
    • vue2参考它实现的vdomdiff
    • snabbdom
      • h函数
      • vnode数据结构
      • patch函数
  • vdom总结

    • JS模拟DOM结构(vnode
    • 新旧vnode对比,得出最小的更新范围,有效控制DOM操作
    • 数据驱动视图模式下,有效控制DOM操作
    <!-- snabbdom-demo -->
    
    <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>
        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)
    
            // vnode = newVnode // patch 之后,应该用新的覆盖现有的 vnode ,否则每次 change 都是新旧对比
        })
    
    </script>
    <!-- table-without-vdom.html -->
    <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>
    <!-- table-with-vdom -->
    
    <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>

2. diff算法

  • diff算法是vdom中最核心、最关键的部分
  • diff算法能在日常使用vue react中提现出来(如key

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

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

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

  • 只比较同一层级,不跨级比较
  • tag不想同,则直接删掉重建,不再深度比较
  • tagkey相同,则认为是相同节点,不再深度比较

diff过程细节

  • 新旧节点都有children,执行updateChildren diff对比
    • 开始和开始对比--头头
    • 结束和结束对比--尾尾
    • 开始和结束对比--头尾
    • 结束和开始对比--尾头
    • 以上四个都未命中:拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
  • children有,旧children无:清空旧text节点,新增新children节点
  • children有,新children无:移除旧children
  • 否则旧text有,设置text为空

vdom和diff算法总结

  • 细节不重要,updateChildren的过程也不重要,不要深究
  • vdom的核心概念很重要:hvnodepatchdiffkey
  • vdom存在的价值更重要,数据驱动视图,控制dom操作
    // snabbdom源码位于 src/snabbdom.ts
    /* global module, document, Node */
    import { Module } from './modules/module';
    import vnode, { VNode } from './vnode';
    import * as is from './is';
    import htmlDomApi, { DOMAPI } from './htmldomapi';
    
    type NonUndefined<T> = T extends undefined ? never : T;
    
    function isUndef (s: any): boolean { return s === undefined; }
    function isDef<A> (s: A): s is NonUndefined<A> { return s !== undefined; }
    
    type VNodeQueue = VNode[];
    
    const emptyNode = vnode('', {}, [], undefined, undefined);
    
    function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
      // key 和 sel 都相等
      // undefined === undefined // true
      return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
    }
    
    function isVnode (vnode: any): vnode is VNode {
      return vnode.sel !== undefined;
    }
    
    type KeyToIndexMap = {[key: string]: number};
    
    type ArraysOf<T> = {
      [K in keyof T]: Array<T[K]>;
    }
    
    type ModuleHooks = ArraysOf<Module>;
    
    function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
      const map: KeyToIndexMap = {};
      for (let i = beginIdx; i <= endIdx; ++i) {
        const key = children[i]?.key;
        if (key !== undefined) {
          map[key] = i;
        }
      }
      return map;
    }
    
    const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
    
    export { h } from './h';
    export { thunk } from './thunk';
    
    export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
      let i: number, j: number, cbs = ({} as ModuleHooks);
    
      const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
    
      for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
          const hook = modules[j][hooks[i]];
          if (hook !== undefined) {
            (cbs[hooks[i]] as any[]).push(hook);
          }
        }
      }
    
      function emptyNodeAt (elm: Element) {
        const id = elm.id ? '#' + elm.id : '';
        const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
        return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
      }
    
      function createRmCb (childElm: Node, listeners: number) {
        return function rmCb () {
          if (--listeners === 0) {
            const parent = api.parentNode(childElm);
            api.removeChild(parent, childElm);
          }
        };
      }
    
      function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        let i: any, data = vnode.data;
        if (data !== undefined) {
          const init = data.hook?.init;
          if (isDef(init)) {
            init(vnode);
            data = vnode.data;
          }
        }
        let children = vnode.children, sel = vnode.sel;
        if (sel === '!') {
          if (isUndef(vnode.text)) {
            vnode.text = '';
          }
          vnode.elm = api.createComment(vnode.text!);
        } else if (sel !== undefined) {
          // Parse selector
          const hashIdx = sel.indexOf('#');
          const dotIdx = sel.indexOf('.', hashIdx);
          const hash = hashIdx > 0 ? hashIdx : sel.length;
          const dot = dotIdx > 0 ? dotIdx : sel.length;
          const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
          const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
            ? api.createElementNS(i, tag)
            : api.createElement(tag);
          if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
          if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
          for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
          if (is.array(children)) {
            for (i = 0; i < children.length; ++i) {
              const ch = children[i];
              if (ch != null) {
                api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
              }
            }
          } else if (is.primitive(vnode.text)) {
            api.appendChild(elm, api.createTextNode(vnode.text));
          }
          const hook = vnode.data!.hook;
          if (isDef(hook)) {
            hook.create?.(emptyNode, vnode);
            if (hook.insert) {
              insertedVnodeQueue.push(vnode);
            }
          }
        } else {
          vnode.elm = api.createTextNode(vnode.text!);
        }
        return vnode.elm;
      }
    
      function addVnodes (
        parentElm: Node,
        before: Node | null,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number,
        insertedVnodeQueue: VNodeQueue
      ) {
        for (; startIdx <= endIdx; ++startIdx) {
          const ch = vnodes[startIdx];
          if (ch != null) {
            api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
          }
        }
      }
    
      function invokeDestroyHook (vnode: VNode) {
        const data = vnode.data;
        if (data !== undefined) {
          data?.hook?.destroy?.(vnode);
          for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
          if (vnode.children !== undefined) {
            for (let j = 0; j < vnode.children.length; ++j) {
              const child = vnode.children[j];
              if (child != null && typeof child !== "string") {
                invokeDestroyHook(child);
              }
            }
          }
        }
      }
    
      function removeVnodes (parentElm: Node,
        vnodes: VNode[],
        startIdx: number,
        endIdx: number): void {
        for (; startIdx <= endIdx; ++startIdx) {
          let listeners: number, rm: () => void, ch = vnodes[startIdx];
          if (ch != null) {
            if (isDef(ch.sel)) {
              invokeDestroyHook(ch); // hook 操作
    
              // 移除 DOM 元素
              listeners = cbs.remove.length + 1;
              rm = createRmCb(ch.elm!, listeners);
              for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
              const removeHook = ch?.data?.hook?.remove;
              if (isDef(removeHook)) {
                removeHook(ch, rm);
              } else {
                rm();
              }
            } else { // Text node
              api.removeChild(parentElm, ch.elm!);
            }
          }
        }
      }
    
      // diff算法核心
      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];
    
          // 开始和开始对比--头头
          } 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];
    
          // 以上四个都未命中
          } else {
            if (oldKeyToIdx === undefined) {
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
            idxInOld = oldKeyToIdx[newStartVnode.key as string];
      
            // 没对应上
            if (isUndef(idxInOld)) { // New element
              api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
              newStartVnode = newCh[++newStartIdx];
            
            // 对应上了
            } 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);
          }
        }
      }
    
      function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
        // 执行 prepatch hook
        const hook = vnode.data?.hook;
        hook?.prepatch?.(oldVnode, vnode);
    
        // 设置 vnode.elem
        const elm = vnode.elm = oldVnode.elm!;
      
        // 旧 children
        let oldCh = oldVnode.children as VNode[];
        // 新 children
        let ch = vnode.children as VNode[];
    
        if (oldVnode === vnode) return;
      
        // hook 相关
        if (vnode.data !== undefined) {
          for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
          vnode.data.hook?.update?.(oldVnode, vnode);
        }
    
        // vnode.text === undefined (vnode.children 一般有值)
        if (isUndef(vnode.text)) {
          // 新旧都有 children
          if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
          // 新 children 有,旧 children 无 (旧 text 有)
          } else if (isDef(ch)) {
            // 清空 text
            if (isDef(oldVnode.text)) api.setTextContent(elm, '');
            // 添加 children
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
          // 旧 child 有,新 child 无
          } else if (isDef(oldCh)) {
            // 移除 children
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          // 旧 text 有
          } else if (isDef(oldVnode.text)) {
            api.setTextContent(elm, '');
          }
    
        // else : vnode.text !== undefined (vnode.children 无值)
        } else if (oldVnode.text !== vnode.text) {
          // 移除旧 children
          if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          }
          // 设置新 text
          api.setTextContent(elm, vnode.text!);
        }
        hook?.postpatch?.(oldVnode, vnode);
      }
    
      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 元素
          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;
      };
    }

模板编译

前置知识

  • 模板是vue开发中最常用的,即与使用相关联的原理
  • 它不是HTML,有指令、插值、JS表达式,能实现循环、判断,因此模板一定转为JS代码,即模板编译
  • 面试不会直接问,但会通过组件渲染和更新过程考察

模板编译

  • vue template compiler将模板编译为render函数
  • 执行render函数,生成vnode
  • 基于vnode在执行patchdiff
  • 使用webpack vue loader,会在开发环境下编译模板

with语法

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

vue组件中使用render代替template

    // 执行 node index.js
    
    const compiler = require('vue-template-compiler')
    
    // 插值
    const template = `<p>{{message}}</p>`
    with(this){return _c('p', [_v(_s(message))])}
    // this就是vm的实例, message等变量会从vm上读取,触发getter
    // _c => createElement 也就是h函数 => 返回vnode
    // _v => createTextVNode 
    // _s => toString 
    // 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}
    
    // 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;
    }

组件渲染更新过程(重点掌握)

前言

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

回顾三大核心知识点

  • 响应式 :监听data属性gettersetter(包括数组)
  • 模板编译 :模板到render函数,再到vnode
  • vdom :两种用法
    • patch(elem,vnode) 首次渲染vnodecontainer
    • patch(vnode、newVnode) 新的vnode取更新旧的vnode
  • 搞定这三点核心原理,vue原理不是问题

组件渲染更新过程

  • 1. 初次渲染过程
    • 解析模板为render函数(或在开发环境已经完成vue-loader
    • 触发响应式,监听data属性gettersetter
    • 执行render函数(执行render函数过程中,会获取data的属性触发getter),生成vnode,在执行patch(elem,vnode) elem组件对应的dom节点
      • const template = <p></p>
      • 编译为render函数 with(this){return _c('p', [_v(_s(message))])}
      • this就是vm的实例, message等变量会从vm上读取,触发getter进行依赖收集
                export default {
            data() {
                return {
                    message: 'hello' // render函数执行过程中会获取message变量值,触发getter
                }
            }
        }
  • 2. 更新过程
    • 修改data,触发setter(此前在getter中已被监听)
    • 重新执行render函数,生成newVnode
    • 在调用patch(oldVnode, newVnode)算出最小差异,进行更新
  • 3. 完成流程图

异步渲染

  • 汇总data的修改,一次更新视图
  • 减少DOM操作次数,提高性能

    methods: {
        addItem() {
            this.list.push(`${Date.now()}`)
            this.list.push(`${Date.now()}`)
            this.list.push(`${Date.now()}`)
    
            // 1.页面渲染是异步的,$nextTick待渲染完在回调
            // 2.页面渲染时会将data的修改做整合,多次data修改也只会渲染一次
            this.$nextTick(()=>{
                const ulElem = this.$refs.ul
                console.log(ulElem.childNotes.length)
            })
        }
    }

总结

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

前端路由原理

hash的特点

  • hash变化会触发网页跳转,即浏览器的前进和后退
  • hash变化不会刷新页面,SPA必须的特点
  • hash永远不会提交到server
  • 通过onhashchange监听

H5 History

  • url规范的路由,但跳转时不刷新页面
  • 通过history.pushStatehistory.onpopstate监听
  • H5 History需要后端支持
    • 当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404
    • 所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理
    • nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;
        server {
      listen  80;
      server_name  www.xxx.com;
    
      location / {
        index  /data/dist/index.html;
        try_files $uri $uri/ /index.html;
      }
    }

两者选择

  • to B系统推荐使用hash,简单易用,对url规范不敏感
  • to C系统,可以考虑使用H5 History,但需要服务端支持
  • 能选择简单的,就别用复杂的,要考虑成本和收益
    // 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'
    })
    // history API
    
    // 页面初次加载,获取 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
Last Updated:
Contributors: leeguooooo