Vue 响应式原理 与 diff 算法

一、简答题

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. 判断是否是相同节点(根据 key、tag、isComment、data 同时定义或不定义)相同直接返回,不是相同节点如果新老节点都是静态的,且 key 相同。
      从老节点拿过来,跳过比对的过程。
      如果新节点是文本节点,设置节点的 text,新节点不是文本节点。新老节点子节点都存在且不同,使用 updateChildren 函数来更新子节点
      只有新节点字节点存在,如果老节点子节点是文本节点,删除老节点的文本,将新节点子节点插入
      只有老节点存在子节点,删除老节点的子节点

    2. updateChildren
      给新老节点定义开始、结束索引
      循环比对新节点开始 VS 老节点开始、新节点结束 VS 老节点结束、新节点开始 VS 老节点结束、新节点结束 VS 老节点开始并移动对应的索引,向中间靠拢
      根据新节点的 key 在老节点中查找,没有找到则创建新节点。

    • 如果新开始节点和老开始节点相同,移动索引比较下一个开始节点
    • 如果新开始节点和老结束节点相同, 将老结束节点放到前面去,移动索引,继续比较
    • 如果新结束节点和老结束节点相同, 移动索引
    • 如果新结束节点和老开始节点相同, 将老开始节点放到后面去,移动索引
    • 如果四种都不满足, 就用新的开始节点的 key 去老节点中寻找相同的, 如果节点相同
      就把老节点中的那个节点放到前面去, 如果 key 相同,节点不同,就创建新的节点放入对应的位置
    • 循环结束后,如果老节点有多的,则删除。如果新节点有多的,则添加。

二、编程题

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()
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容