Vue 响应式原理 与 diff 算法

2022-09-13  本文已影响0人  A_走在冷风中

一、简答题

1、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如何把新增成员设置成响应式数据,它的内部原理是什么。

let vm = new Vue({
 el: '#el'
 data: {
  o: 'object',
  dog: {}
 },
 method: {
  clickHandler () {
   // 该 name 属性是否是响应式的
   this.dog.name = 'Trump'
  }
 }
})

this.dog.name = 'Trump' 不是响应式的
正确写法

Vue.$set(this.dog,'name','Trump')

原理:vue2.X 通过 Object.defindProperty 挟持 obj 并将 obj 中的属性转换为 get 和 set 实现响应式
通过 this.dog.name = 'Trump' 向 obj 中添加属性, 所添加的 name 属性并没有转换为 get 和 set,所以不是响应式的

2、请简述 Diff 算法的执行过程

由于操作 dom 比较耗费性能, 所以当 dom 改变时,不能将整个 dom 全部更新, 需要进行比较,找出 dom 中被修改的节点进行更新
这个过程就叫 diff 算法
diff 算法的主要执行过程:

二、编程题

1、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #nav a {
        margin-right: 10px;
      }

      #nav a.act {
        color: #ff0000;
      }
    </style>
  </head>
  <body>
    <nav id="nav"></nav>
    <main id="app"></main>
    <script>
      class Router {
        constructor() {
          this.navs = [
            {
              path: '#index',
              title: '首页',
              content: '首页-内容',
            },
            {
              path: '#news',
              title: '新闻',
              content: '新闻-内容',
            },
            {
              path: '#about',
              title: '关于',
              content: '关于-内容',
            },
          ]

          this.navNode = document.getElementById('nav')
          this.el = document.getElementById('app')
        }
        init() {
          this.createNav()
          this.haddleHashChage()
          //监听hash值变动
          window.addEventListener('hashchange', this.haddleHashChage.bind(this))
        }
        createNav() {
          //创建导航
          let fragment = document.createDocumentFragment()
          this.navs.forEach((nav) => {
            let tagA = document.createElement('a')
            tagA.href = nav.path
            tagA.innerText = nav.title
            fragment.appendChild(tagA)
          })
          this.navNode.appendChild(fragment)
        }
        haddleHashChage() {
          //根据hash值,变动内容
          const hashVal = window.location.hash || this.navs[0].path
          this.navs.forEach((nav, index) => {
            let curNodes = this.navNode.childNodes[index]
            curNodes.className = ''
            if (nav.path == hashVal) {
              curNodes.className = 'act'
              this.el.innerHTML = nav.content
            }
          })
        }
      }

      const router = new Router()
      router.init()
    </script>
  </body>
</html>

2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。

class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    this.compile(this.el);
  }
  // 编译模板,处理文本节点和元素节点
  compile(el) {
    let childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      // 处理文本节点
      if (this.isTextNode(node)) {
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node);
      }

      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }
  // 编译元素节点,处理指令
  compileElement(node) {
    // console.log(node.attributes)
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach((attr) => {
      // 判断是否是指令
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        if(attrName.startsWith("v-on"))
           attrName = attrName.substr(4);
        else
        // v-text --> text
          attrName = attrName.substr(2);
        let key = attr.value;
        this.update(node, key, attrName);
      }
    });
  }

  update(node, key, attrName) {
    let updateFn = this[attrName + "Updater"];
    updateFn && updateFn.call(this, node, this.vm[key], key);
  }
  // 处理 v-on 指令
  onUpdater (node, value,eventType) {
    node.addEventListener(eventType, value)
    new Watcher(this.vm, eventType, newValue => {
      node.removeEventListener(eventType, value)
      node.addEventListener(eventType, newValue)
    })
  }
  // 处理 v-html 指令
  htmlUpdater(node, value, key) {
    node.innerHTML = value;
    new Watcher(this.vm, key, (newValue) => {
      node.innerHTML = newValue;
    });
  }
  // 处理 v-text 指令
  textUpdater(node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue;
    });
  }
  // v-model
  modelUpdater(node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue;
    });
    // 双向绑定
    node.addEventListener("input", () => {
      this.vm[key] = node.value;
    });
  }

  // 编译文本节点,处理差值表达式
  compileText(node) {
    // console.dir(node)
    // {{  msg }}
    let reg = /\{\{(.+?)\}\}/;
    let value = node.textContent;
    if (reg.test(value)) {
      let key = RegExp.$1.trim();
      node.textContent = value.replace(reg, this.vm[key]);

      // 创建watcher对象,当数据改变更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue;
      });
    }
  }
  // 判断元素属性是否是指令
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  // 判断节点是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
  // 判断节点是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

3、参考 Snabbdom 提供的电影列表的示例,利用 Snabbdom 实现类似的效果,如图:

<img src="images/Ciqc1F7zUZ-AWP5NAAN0Z_t_hDY449.png" alt="Ciqc1F7zUZ-AWP5NAAN0Z_t_hDY449" style="zoom:50%;" />

import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'
import { originalData } from './originData'
let patch = init([styleModule, eventListenersModule])
let data = [...originalData]
const container = document.querySelector('#app')
let sortBy = 'rank'
let vnode = view(data)
// 初次渲染
let oldVnode = patch(container, vnode)
// 渲染
function render() {
  oldVnode = patch(oldVnode, view(data))
}
// 生成新的VDOM
function view(data) {
  return h('div#container', [
    h('h1', 'Top 10 movies'),
    h('div', [
      h('a.btn.add', { on: { click: add }, style: { ['margin-right']: '10px' } }, '添加'),
      'Sort by: ',
      h('span.btn-group', [
        h(
          'a.btn.rank',
          {
            class: { active: sortBy === 'rank' },
            on: {
              click: () => {
                changeSort('rank')
              },
            },
            style: { margin: '10px' },
          },
          'Rank'
        ),
        h(
          'a.btn.title',
          {
            class: { active: sortBy === 'title' },
            on: {
              click: () => {
                changeSort('title')
              },
            },
            style: { margin: '10px' },
          },
          'Title'
        ),
        h(
          'a.btn.desc',
          {
            class: { active: sortBy === 'desc' },
            on: {
              click: () => {
                changeSort('desc')
              },
            },
            style: { margin: '10px' },
          },
          'Description'
        ),
      ]),
    ]),
    h('div.list', data.map(movieView)),
  ])
}

// 添加一条数据 放在最上面
function add() {
  const n = originalData[Math.floor(Math.random() * 10)]
  data = [{ rank: data.length + 1, title: n.title, desc: n.desc, elmHeight: 0 }].concat(data)
  render()
}
// 排序
function changeSort(prop) {
  console.log(1111)
  sortBy = prop
  data.sort(function (a, b) {
    if (a[prop] > b[prop]) {
      return 1
    }
    if (a[prop] < b[prop]) {
      return -1
    }
    return 0
  })
  render()
}

// 单条数据
function movieView(movie) {
  return h(
    'div.row',
    {
      key: movie.rank,
      style: {
        display: 'none',
        delayed: { transform: 'translateY(' + movie.offset + 'px)', display: 'block' },
        remove: { display: 'none', transform: 'translateY(' + movie.offset + 'px) translateX(200px)' },
      },
      hook: {
        insert: function insert(vnode) {
          movie.elmHeight = vnode.elm.offsetHeight
        },
      },
    },
    [
      h('div', { style: { fontWeight: 'bold' } }, movie.rank),
      h('div', movie.title),
      h('div', movie.desc),
      h(
        'div.btn.rm-btn',
        {
          on: {
            click: () => {
              remove(movie)
            },
          },
        },
        '删除'
      ),
    ]
  )
}
// 删除数据
function remove(movie) {
  console.log(movie)
  data = data.filter(function (m) {
    return m.title !== movie.title
  })
  render()
}

上一篇下一篇

猜你喜欢

热点阅读