美文网首页
Vue 响应式原理 与 diff 算法

Vue 响应式原理 与 diff 算法

作者: A_走在冷风中 | 来源:发表于2022-09-13 17:15 被阅读0次

    一、简答题

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

    相关文章

      网友评论

          本文标题:Vue 响应式原理 与 diff 算法

          本文链接:https://www.haomeiwen.com/subject/tpadertx.html