Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】

Vue 原理

其他扩展:vue面试中常见的面试题

原理的意义

  • 知其然知其所以然——各行业通用的道理
  • 了解原理,才能应用的更好
  • 大厂造轮子(业务定制、技术 KPI)

如何考察

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

重要的原理

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

原理题

  • 为何 v-for 中要用 key
  • 描述组件渲染和更新的过程
  • 双向数据绑定 v-model 的实现原理

如何理解 MVVM

  • “很久以前” 的组件化
    • asp、jsp、php 已经有组件化了
    • nodejs 中也有类似的组件化
  • 数据驱动视图(MVVM、setState)
    • 传统组件,只是静态渲染,更新还要依赖于操作 DOM
    • 数据驱动视图——Vue MVVM
    • 数据驱动视图——React setState

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第1张图片

Vue 响应式(数据拦截)

推荐文章:简单手写实现Vue2.x

  • 组件 data 数据一旦变化,立刻触发视图的更新
  • 实现数据驱动视图的第一步

数据响应式原理

源码级别可以参考: Vue.js 技术揭秘——深入响应式原理

在 JavaScript 的对象 Object 中有一个属性叫访问器属性,其中有 [[Get]][[Set]] 特性,它们分别是获取函数和设置函数

核心 APIObject.defineProperty

  • 存在一些问题,Vue 3.0 启动 Proxy

    Proxy 可以原生支持监听数组变化

    但是 Proxy 兼容性不好,且无法 polyfill

问题

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性(Vue.setVue.delete
  • 不能监听数组变化(重新定义原型,重写 pushpop 等方法)

Object.defineProperty 实现响应式

  • observe 的功能:给非 VNode 的对象类型数据添加一个 Observer ,用来监听数据的变化
  • Observer 的作用:给对象的属性添加 getter 和 setter,用来依赖收集和派发更新。对于数组会调用 observeArray 方法,对于对象会对对象的 key 调用 defineReactive 方法
  • defineReactive 的功能:定义一个响应式对象,给对象动态添加 getter 和 setter。对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter
  • setter 的时候,会通知所有的订阅者
  • arrayMethods 首先继承了 Array,然后对数组中的所有能改变数组自身的方法进行重写,重写后的方法会先执行它们本身原有的逻辑,然后把新添加的值

简单实现 Vue 中的 defineReactive

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

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

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

  // 不可枚举的不用监听
  const property = Object.getOwnPropertyDescriptor(target, key)
  if (property && property.configurable === false) return
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        observer(newValue) // 值修改后进行监听

        // value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
        value = newValue
        updateView() // 触发更新视图
      }
    },
  })
}

// 监测数据的变化
function observer(target) {
  // 不是对象或数组(vue 是判断是否是数组、纯粹对象、可扩展对象)
  if (typeof target !== 'object' || target === null) {
    return target
  }
  // 不要这样写,污染全局的 Array 原型
  /* Array.prototype.push = function () {
      updateView()
  } */
  if (Array.isArray(target)) {
    target.__proto__ = arrayMethods
  }
  // 重新定义各个属性(for in 也可以遍历数组)
  for (let key in target) {
    if (!Object.hasOwnProperty.call(target, key)) return
    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
data.info.address = '上海' // 深度监听
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
data.nums.push(4) // 监听数组

虚拟 DOM

  • DOM 操作非常耗费性能
  • 以前用 jQuery,可以自行控制 DOM 操作的时机,手动调整
  • Vue 和 React 是数据驱动视图,如何有效控制 DOM 操作?

vdom

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

用 JS 模拟 DOM 结构

  • DOM 结构
<div id="div1" class="container">
  <p>vdomp>
  <ul style="font-size: 20px;">
    <li>ali>
  ul>
div>
  • JS 模拟
{
  tag: 'div',
  props: {
    className: 'container',
    id: 'div1',
  },
  children: [
    {
      tag: 'p',
      children: 'dom',
    },
    {
      tag: 'ul',
      props: { style: 'font-size: 20px' },
      children: [{ tag: 'li', children: 'a' }],
    },
  ],
}
  • Vue 中的真实 DOM
<div id="app">
  <p class="text">HelloWorldp>
div>
  • Vue 中的虚拟 DOM
{
  "tag": "div",                     // 标签名
  "key": undefined,                 // key值
  "elm": div#app,                   // 真实DOM节点
  "text": undefined,                // 文本信息
  "data": {attrs: {id:"app"}},      // 节点属性
  "children": [{    	            // 子节点属性
    "tag": "p",
    "key": undefined,
    "elm": p.text,
    "text": undefined,
    "data": {attrs: {class: "text"}},
    "children": [{
      "tag": undefined,
      "key": undefined,
      "elm": text,
      "text": "helloWorld",
      "data": undefined,
      "children": []
    }]
  }]
}

snabbdom 使用

  • 通过 snabbdom 学习 vdom

  • 核心概念:h、vnode、patch、diff、key 等

  • vdom 价值:数据驱动视图,控制 DOM 操作

    patch(elem, vnode)patch(vnode, newVnode)

DOCTYPE html>
<html>
  <body>
    <div id="container">div>
    <button id="btn-change">changebutton>
  body>
html>
<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)
  })
script>
  • jQuery 版本
DOCTYPE html>
<html>
  <body>
    <div id="container">div>
    <button id="btn-change">changebutton>
  body>
html>
<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.append($('/tr>'))
    data.forEach(item=>{
      $table.append($('/tr>'))})// 渲染到页面
    $container.append($table)}$('#btn-change').click(()=>{
    data[1].age =30
    data[2].address ='深圳'// re-render  再次渲染render(data)})// 页面加载完立刻执行(初次渲染)render(data)script>

  • vdom 版本
DOCTYPE html>
<html>
  <body>
    <div id="container">div>
    <button id="btn-change">changebutton>
  body>
html>

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

diff 算法

推荐文章:图文并茂地来详细讲讲Vue Diff算法

  • diff 算法是 vdom 中最核心、最关键的部分
  • diff 算法能在日常使用 Vue、React 中体现出来(如:key)

diff 算法概述:

  • diff 即对比。是一个广泛的概念,如 linux diff 命令、git diff 等
  • 两个 js 对象也可以做 diff,jiff
  • 两棵树做 diff,如这里的 vdom diff

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第2张图片

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

  • 对于旧树上的点 E 来说,它要和新树上的所有点比较,复杂度为 O(n)

  • 点 E 在新树上没有找到,点 E 会被删除,然后遍历新树上的所有点找到对应点(X)去填空,复杂度增加到 O(n^2)

  • 这样的操作会在旧树的每个点进行,最终复杂度为 O(n^3)

    1000 个节点,要计算 1 亿次,算法不可用

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

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

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第3张图片

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第4张图片

snabbdom 源码

snabbdom

  • src\h.ts

    最后返回 vnode

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 {
  // ...
  return vnode(sel, data, children, text, undefined);
}
  • src\vnode.ts

    最后返回一个 JS 对象

export function vnode(
  sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | DocumentFragment | Text | undefined
): VNode {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}
  • src\init.ts

    触发页面更新就会调用 patch 方法,之后我们看一下 patch 的源码

return function patch(
  oldVnode: VNode | Element | DocumentFragment,
  vnode: VNode
): VNode {
  let i: number, elm: Node, parent: Node;
  const insertedVnodeQueue: VNodeQueue = [];
  // 执行 pre hooks
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

  // 第一个参数不是 vnode
  if (isElement(api, oldVnode)) {
    // 创建一个空的 vnode,关联到这个 DOM 元素
    oldVnode = emptyNodeAt(oldVnode);
  } else if (isDocumentFragment(api, oldVnode)) {
    oldVnode = emptyDocumentFragmentAt(oldVnode);
  }

  // 相同的 vnode(key 和 sel 都相同)
  if (sameVnode(oldVnode, vnode)) {
    // vnode 对比
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  // 不同的 vnode,直接删掉重建
  } else {
    elm = oldVnode.elm!;
    parent = api.parentNode(elm) as Node;

    // 重建
    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;
};

如果 patch 第一个参数不是 vnode,创建一个空的 vnode

function emptyNodeAt(elm: Element) {
  const id = elm.id ? "#" + elm.id : "";
  const classes = elm.getAttribute("class");
  const c = classes ? "." + classes.split(" ").join(".") : "";
  return vnode(
    api.tagName(elm).toLowerCase() + id + c,
    {},
    [],
    undefined,
    elm
  );
}

比较 vnode 是否相同(比较 key 和 selector)

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 或 data 都相等就是相等
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;

  return isSameSel && isSameKey && isSameIs;
}

patchVnode

vnode 相同的话执行 patchVnode

  • 两者都有 children -> 进行 children 对比,之后更新 children(updateChildren)
  • 新 children 有,旧 children 无 -> 清空旧 text,添加 children(addVnodes)
  • 新 children 无,旧 children 有 -> 移除旧 children(removeVnodes)
  • 新 children 无,旧 children 无,旧 text 有 -> 清空旧 text
  • 新旧 text 不一样 -> 移除旧 children(removeVnodes),设置 text
function patchVnode(
  oldVnode: VNode,
  vnode: VNode,
  insertedVnodeQueue: VNodeQueue
) {
  // 执行 prepatch hook,生命周期钩子
  const hook = vnode.data?.hook;
  hook?.prepatch?.(oldVnode, vnode);
  // 设置 vnode.elm
  const elm = (vnode.elm = oldVnode.elm)!;
  // 旧的 children
  const oldCh = oldVnode.children as VNode[];
  // 新的 children
  const 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);
    // 旧 children 有,新 children 无
    } else if (isDef(oldCh)) {
      // 移除 children
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    // 旧 text 有
    } else if (isDef(oldVnode.text)) {
      api.setTextContent(elm, "");
    }
  // vnode.text!==undefined(vnode.children 无值)
  } else if (oldVnode.text !== vnode.text) {
    // 移除旧 children
    if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    api.setTextContent(elm, vnode.text!);
  }
  hook?.postpatch?.(oldVnode, vnode);
}

addVnodes removeVnodes

  • 新增 vnode
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);
    }
  }
}
  • 删除 vnode
function removeVnodes(
  parentElm: Node,
  vnodes: VNode[],
  startIdx: number,
  endIdx: number
): void {
  for (; startIdx <= endIdx; ++startIdx) {
    let listeners: number;
    let rm: () => void;
    const 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!);
      }
    }
  }
}

updateChildren

新旧都有 children,会对 children 进行 updateChildren

function updateChildren(
  parentElm: Node,
  oldCh: VNode[],
  newCh: VNode[],
  insertedVnodeQueue: VNodeQueue
) {
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVnode == null) { /* ... */ }
    // 开始和开始对比
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
    // 结束和结束对比
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
    // 开始和结束对比
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
    // 结束和开始对比
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
    // 以上4个都未命中
    } else {
      // 拿新节点的 key,能否对应上 oldCh 中的某个节点的 key
      idxInOld = oldKeyToIdx[newStartVnode.key as string];
      // 没对应上
      if (isUndef(idxInOld)) {
        // New element
        api.insertBefore(
          parentElm,
          createElm(newStartVnode, insertedVnodeQueue),
          oldStartVnode.elm!
        );
      } 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];
    }
  }
}

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第5张图片

不使用 key 就会全部删掉然后插入;使用 key ,key 相同会直接移动过来,不用做销毁然后重新渲染的过程

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第6张图片

总结

在 Vue 中,主要是 patch()patchVnode()updateChildren 这三个方法来实现 Diff

  • 当 Vue 中的响应式数据发生变化时,就会触发 updateCompoent()

  • updateComponent() 会调用 patch() 方法,在该方法中进行比较,调用 sameVnode 判断是否为相同节点(判断 keytag 等静态属性),如果是相同节点的话执行 patchVnode 方法,开始比较节点差异,如果不是相同节点的话,则进行替换操作

    patch() 接收新旧虚拟 DOM,即 oldVnodevnode

    • 首先判断 vnode 是否存在,如果不存在,删除旧节点
    • 如果 vnode 存在,再判断 oldVnode,如果不存在,只需新增整个 vnode 即可
    • 如果 vnodeoldVnode 都存在,判断两者是不是相同节点,如果是,调用 patchVnode() ,对两个节点进行详细比较
    • 如果两者不是相同节点,只需将 vnode 转换为真实 DOM 替换 oldVnode
  • patchVnode 同样接收新旧虚拟 DOM,即 oldVnodevnode

    • 首先判断两个虚拟 DOM 是不是全等,即没有任何变动,是的话直接结束函数,否者继续执行
    • 其次更新节点的属性,接着判断 vnode.text 是否存在,存在的话只需更新节点文本即可,否则继续执行
    • 判断 vnodeoldVnode 是否有孩子节点
      • 如果两者都有孩子节点,执行 updateChildren() ,进行比较更新
      • 如果 vnode 有孩子,oldVnode 没有,则直接删除所有孩子节点,并将该文本属性设为空
      • 如果 oldVnode 有孩子,vnode 没有,则直接删除所有孩子节点
      • 如果两者都没有孩子节点,就判断 oldVnode.text 是否有内容,有的话情况内容即可
  • updateChildren 接收三个参数:parentElm 父级真实节点、oldCholdVnode 的孩子节点、newChVnode 的孩子节点

    oldChnewCh 都是一个数组。正常我们想到的方法就是对这两个数组一一比较,时间复杂度为 O(NM)。Vue 中是通过四个指针实现的

    • 首先是 oldStartVnodenewStartVnode 进行比较(两头比较),如果比较相同的话,就可以执行 patchVnode
    • 如果 oldStartVnodenewStartVnode 匹配不上的话,接下来就是 oldEndVnodenewEndVnode 做比较了(两尾比较)
    • 如果两头和两尾比较都不是相同节点的话,就开始交叉比较,首先是 oldStartVnodenewEndVnode 做比较(头尾比较)
    • 如果 oldStartVnodenewEndVnode 匹配不上的话,就 oldEndVnodenewStartVnode 进行比较(尾头比较)

    如果这四种比较方法都匹配不到相同节点,才是用暴力解法,针对 newStartVnode 去遍历 oldCh 中剩余的节点,一一匹配

sameVnode 中,比较两个节点是否相同时,第一个判断条件就是 vnode.key ,并且在后面是用暴力解法时,第一选择也是通过 key 去匹配


<div>
  <p>Ap>
  <p>Bp>
  <p>Cp>
div>


<div>
  <p>Bp>
  <p>Cp>
  <p>Ap>
div>

如果没有设置 key 值,通过 diff 需要操作 DOM 次数会很多,因为 keyundefined,即使每个标签都是相同节点,也会一一进行替换,需要操作 3 次 DOM

如果分别给对应添加了 key 值,通过 diff 只需操作 1 次 DOM

图片来源:图文并茂地来详细讲讲Vue Diff算法

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第7张图片

模板编译

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

with 语法

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

console.log(obj.a)
console.log(obj.b) // undefined

with (obj) {
  console.log(a)
  console.log(b) // 报错
}

vue 模板被编译成什么

  • html 是标签语言,只有 JS 才能实现判断、循环,因此模板一定是转换为某种 JS 代码,即编译模板
  • 使用 webpack vue-loader ,会在开发环境下编译模板
const compiler = require('vue-template-compiler')

// *插值
const template = `

{{message}}

`
/* with (this) { return createElement('p', [createTextVNode(toString(message))]) } */ // 编译 const res = compiler.compile(template) console.log(res.render)
  • 三元运算符
// *三元运算符
const template = `

{{flag ? message : 'no message found'}}

`
with (this) { return createElement('p', [createTextVNode(toString(flag ? message : 'no message found'))]) }
  • class、id 属性和动态属性
// *属性和动态属性
const template = `
  
`
with (this) { return createElement('div', { staticClass: 'container', attrs: { id: 'div1' } }, [ createElement('img', { attrs: { src: imgUrl } }), ]) }
  • v-if 条件
// *条件
const template = `
  

A

B

`
with (this) { return createElement('div', [ flag === 'a' ? createElement('p', [createTextVNode('A')]) : createElement('p', [createTextVNode('B')]), ]) }
  • v-show
// *v-show
const template = `

A

`
with (this) { return createElement( 'p', { directives: [ { name: 'show', rawName: 'v-show', value: flag === 'a', expression: "flag === 'a'" }, ], }, [createTextVNode('A')] ) } // v-show 操作的是样式 https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/directives/show.js bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) { vnode = locateNode(vnode) const transition = vnode.data && vnode.data.transition const originalDisplay = el.__vOriginalDisplay = el.style.display === 'none' ? '' : el.style.display if (value && transition) { vnode.data.show = true enter(vnode, () => { el.style.display = originalDisplay }) } else { el.style.display = value ? originalDisplay : 'none' } }
  • v-for 循环
// *循环
const template = `
  
  • {{item.title}}
`
with (this) { return createElement( 'ul', renderList(list, function(item) { return createElement('li', { key: item.id }, [createTextVNode(toString(item.title))]) }), 0 ) }
  • event 事件
// *事件
const template = ``

with (this) {
  return createElement('button', { on: { click: clickHandler } }, [createTextVNode('submit')])
}
  • v-model
// *v-model
const template = ``

with (this) {
  return createElement('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
      },
    },
  })
}

vue 组件使用 render 函数

  • 有些复杂情况中,不能用 template,可以考虑用 render
Vue.component('heading', {
  render: function(createElement) {
    return createElement('h' + this.level, [
      createElement(
        'a',
        {
          attrs: {
            name: 'headerId',
            href: '#' + 'headerId',
          },
        },
        'this is a tag'
      ),
    ])
  },
})

Vue 组件渲染和更新

初次渲染过程:

  • 解析模板为 render 函数(或在开发环境已完成,vue-loader)
  • 触发响应式,监听 data 属性 gettersetter
  • 执行 render 函数,生成 vnodepatch(elem, vnode)
<template>
  <p>{{ message }}p>
template>

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

更新过程:

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

Vue 原理【响应式、虚拟DOM和Diff算法、模板编译】_第8张图片

  • 生成 render 函数,其生成一个 vnode,它会 touch 触发 getter 进行收集依赖
  • 在模板中哪个被引用了就会将其用 Watcher 观察起来,发生了 setter 也会将其 Watcher 起来
  • 如果之前已经被 Watcher 观察起来,发生更新进行重新渲染

异步渲染

源码级别可以参考:Vue.js 技术揭秘——nextTick

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

前端路由原理

hash 路由

hash 的特点:

  • hash 变化会触发网页跳转,即浏览器的前进、后退
  • hash 变化不会刷新页面,SPA 必需的特点
  • hash 永远不会提交到 server 端(前端自生自灭)
http://127.0.0.1:5500/src/1.html?a=100&b=20#/aaa/bbb/
location.protocol // 'http:'
location.hostname // '127.0.0.1'
location.host // '127.0.0.1:5500'
location.port // '5500'
location.pathname // '/src/1.html'
location.search // '?a=100&b=20'
location.hash // '#/aaa/bbb/'

hash 变化:

  1. JS 修改 url
  2. 手动修改 urlhash
  3. 浏览器前进、后退
DOCTYPE html>
<html>
  <body>
    <p>hash testp>
    <button id="btn1">修改 hashbutton>
  body>
html>
<script>
  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>

history 路由

  • url 规范的路由,但跳转时不刷新页面
  • 需要 server 端配合,可参考:后端配置例子
  • pushState 不会触发 hashchange 事件,popstate 事件只会在浏览器某些行为下触发,比如点击后退、前进按钮
DOCTYPE html>
<html lang="en">
  <body>
    <p>history API testp>
    <button id="btn1">修改 urlbutton>
  body>
html>
<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)
  }
script>

两者选择

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

你可能感兴趣的:(Vue框架,vue.js,算法,前端)

nameageaddress
' + item.name + '' + item.age + '' + item.address + '