手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法

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

let vm = new Vue({
 el: '#el'
 data: {
  o: 'object',
  dog: {}
 },
 method: {
  clickHandler () {
   // 该 name 属性是否是响应式的
   this.dog.name = 'Trump'
  }
 }
})
  • 不是响应式的
  • 解决
    data定义的时候给定义属性name
data:{
    dog:{
        name:''
    }
}

原理: vue的Observer方法里面对data做了递归的响应式处理。

  • 通过Vue.set()方法实现
 handleClick () {
  this.$set(this.dog, 'name', 'xxxx')
  setTimeout(() => {
    this.dog.name = 'oo444ooo'
  }, 2000)
}

原理:vue源码里面set方法通过调用defineReactive$$1方法给这个属性变成响应式的,并通过dep.notify()方法通知依赖更新。

  • 通过Object.assign()
handleClick () {
  this.dog.name = 'xxxxx'
  this.dog = Object.assign({}, this.dog)
  setTimeout(() => {
    this.dog.name = 'oo444ooo'
  }, 2000)
}

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

diff算法是通过同层的树节点进行比较。

1、老节点不存在,直接添加新节点到父元素
2、新节点不存,从父元素删除老节点。
3、新老节点都存在
3.1 判断是否是相同节点(根据key、tag、isComment、data同时定义或不定义)

  • 相同直接返回
  • 不是相同节点
  • 如果新老节点都是静态的,且key相同。从老节点拿过来,跳过比对的过程。
  • 如果新节点是文本节点,设置节点的text
  • 新节点不是文本节点
    • 新老节点子节点都存在且不同,使用updateChildren函数来更新子节点
    • 只有新节点字节点存在,如果老节点子节点是文本节点,删除老节点的文本,将新节点子节点插入
    • 只有老节点存在子节点,删除老节点的子节点

3.2 updateChildren

  • 给新老节点定义开始、结束索引

  • 循环比对新节点开始VS老节点开始、新节点结束VS老节点结束、新节点开始VS老节点结束、新节点结束VS老节点开始并移动对应的索引,向中间靠拢

  • 根据新节点的key在老节点中查找,没有找到则创建新节点。

  • 循环结束后,如果老节点有多的,则删除。如果新节点有多的,则添加。

4、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。
这里跟history模式不同的地方在于事件的处理

   clickHandle (e) {
        location.hash = this.to
        this.$router.data.current = this.to
        e.preventDefault()
    }
    initEvent () {
        window.addEventListener('hashchange', () => {
            this.data.current = location.hash.slice(1)
        })
    }

hash完整版实现

let _Vue = null
export default class VueRouter {
  constructor (options) {
    this.options = options
    this.routerMap = {}
    this.data = _Vue.observable({
      current: '/'
    })
  }

  static install (Vue) {
    if (VueRouter.install.installed) return
    VueRouter.install.installed = true
    _Vue = Vue
    _Vue.mixin({
      beforeCreate () {
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init()
        }
      }
    })
  }

  init () {
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
  }

  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routerMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        return h('a', {
          attrs: {
            href: this.to
          },
          on: {
            click: this.clickHandle
          }
        }, [this.$slots.default])
      },
      methods: {
        clickHandle (e) {
          location.hash = this.to
          this.$router.data.current = this.to
          e.preventDefault()
        }
      }
    })
    const _this = this
    Vue.component('router-view', {
      render (h) {
        const component = _this.routerMap[_this.data.current]
        return h(component)
      }
    })
  }

  initEvent () {
    window.addEventListener('hashchange', () => {
      this.data.current = location.hash.slice(1)
    })
  }
}

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

  • v-html 实现
   htmlUpdater(node,value,key){
       node.innerHTML = value;
       new Watcher(this.vm, key, (newValue)=>{
           node.innerHTML = newValue
       })
   }
  • v-on 指令
    • 处理节点的时候对属性做一下判断 含有’:'认为是v-on:click这种
    • 然后取出事件类型、事件名称
    • 通过addEventListener在node节点上做事件监听
    • 根据事件名称在vm里面的$options.methods里找到方法执行
   compileElement(node){
       // 遍历属性节点
       Array.from(node.attributes).forEach(attr=>{
           let attrName = attr.name;
           if(this.isDirective(attrName)){
               attrName = attrName.substr(2)
               let key = attr.value;
               if (attrName.indexOf(':')!==-1) {
                   let eventType = attrName.split(':')[1]
                   this.handleEvent(this,node,eventType,key)
               }
               this.update(node, key, attrName)
           }
       })
   }
    handleEvent(vm,node,eventType,eventName){
       node.addEventListener(eventType,()=>{
           vm.vm.$options.methods[eventName]()
       })
   }

3、参考 Snabbdom 提供的电影列表的示例,实现类似的效果(去动画)。

import { h, init } from 'snabbdom'
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
import { originalData } from './originData'

let patch = init([style,eventlisteners])

let data = [...originalData]
const container = document.querySelector('#container')

var 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 } }, 'Add'),
                    'Sort by: ',
                    h('span.btn-group',
                        [
                            h('a.btn.rank',
                                {
                                    'class': { active: sortBy === 'rank' },
                                    on: { click: [changeSort, 'rank'] }
                                }, 'Rank'),
                            h('a.btn.title',
                                {
                                    'class': { active: sortBy === 'title' },
                                    on: { click: [changeSort, 'title'] }
                                }, 'Title'),
                            h('a.btn.desc',
                                {
                                    'class': { active: sortBy === 'desc' },
                                    on: { click: [changeSort, 'desc'] }
                                }, '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) {
    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]}}, 'x')]);
}
// 删除数据
function remove(movie) {
    data = data.filter(function (m) {
        return m !== movie;
    });
    render()
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值