0

0

0

修罗

站点介绍

只有了解事实才能获得真正的自由

手写vue(下)

修罗 2020-10-26 1573 0条评论 vue

首页 / 正文

手写vue(下)

MVVM框架的三要素:

数据响应式、模板引擎及其渲染

数据响应式:监听数据变化并在视图中更新: Object.defineProperty()/ Proxy

模版引擎:提供描述视图的模版语法

  • 插值:{{}}
  • 指令:v-bind,v-on,v-model,v-for,v-if

渲染:如何将模板转换为html

  • 模板 => vdom => dom

原理分析

1、new Vue() 首先执行初始化,对data执行响应化处理,这个过程发生在Observer中

2、同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在 Compile中

3、同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数

4、由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个 Watcher

5、将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

1603806026647.png

涉及类型介绍

Vue:框架构造函数

Observer:执行数据响应化(分辨数据是对象还是数组)

Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)

Watcher:执行更新函数(更新dom)

Dep:管理多个Watcher,批量更新

使用

需要引入vue.js和compile.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./vue.js"></script>
  <script src="./compile.js"></script>
</head>
<body>
  <!-- vue1.0实现,没有使用虚拟dom -->
  <div id="app">
    <p>{{counter}}</p>
    <div @click="onclick" v-text="counter"><span>19</span></div>
    <div v-html="desc"></div>
    <input type="text" v-model="desc">
  </div>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        counter: 1,
        desc:"<span>这是html</span>"
      },
      methods: {
        onclick(){
          this.counter++
        }
      }
    })
    setInterval(() => {
      app.counter++
    }, 1000)
  </script>
</body>
</html>

1603806467400.png

编写vue.js

function defineReactive(obj, key, val){
  // 递归
  observe(val)

  // 创建一个Dep和当前key一一对应
  const dep = new Dep()

  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    // val被引用,形成闭包不被释放,保存当前状态
    get(){
      console.log('get '+key);
      // 依赖收集在这里
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal){
      if(newVal !== val){
        console.log('set ' + key + ': ' + newVal);
        // 如果传入的newVal依然是obj,要对其做响应式处理
        observe(newVal)
        val = newVal

        // 值发生变化,执行更新函数
        // watchers.forEach(w => w.update())
        dep.notify()
      }
    }
  })
}

function observe(obj){
  if(typeof obj !== 'object' || obj == null) return

  // 创建Observer实例
  new Observer(obj)
}

// 代理函数,方便用户直接访问$data数据
function proxy(vm, sourceKey){
  Object.keys(vm[sourceKey]).forEach(key => {
    // 将$data中的key代理到vm属性中
    Object.defineProperty(vm, key, {
      get(){
        return vm[sourceKey][key]
      },
      set(newVal){
        vm[sourceKey][key] = newVal
      }
    })
  })
}

// 创建vue构造函数
class Vue {
  constructor(options){
    // 保存选项
    this.$options = options
    this.$data = options.data
    // 响应化处理
    observe(this.$data)

    // 代理
    proxy(this, '$data')

    // 创建编译器实例
    new Complie(options.el, this)
  }
}

// 根据对象类型决定如何做响应化
class Observer{
  constructor(value){
    this.value = value

    // 判断其类型
    if(typeof value === 'object'){
      this.walk(value)
    }
  }

  // 对象数据的响应化
  walk(obj){
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }

  // 数组的响应化

}

// 观察者:保存更新函数,值发送变化调用更新函数
// const watchers = []
class Watcher{
  constructor(vm, key, updateFn){
    this.vm = vm
    this.key = key
    this.updateFn = updateFn
    // watchers.push(this)

    // Dep.target静态属性上设置为当前watcher实例
    Dep.target = this
    // 读取触发getter
    this.vm[this.key]
    // 收集完置空
    Dep.target = null
  }

  update(){
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}

// Dep:依赖,管理某个key相关Watcher实例

class Dep{
  constructor(){
    this.deps = []
  }
  addDep(dep){
    this.deps.push(dep)
  }

  notify(){
    this.deps.forEach(dep => dep.update())
  }
}

编写compile.js

编译模板中vue模板特殊语法,初始化视图、更新视图

// 编译器

// 递归遍历dom数
// 判断节点类型,如果是文本,则判断是否是插值绑定
// 如果是元素,则遍历器属性是否是指令或事件,然后递归子元素

class Complie{
  // el宿主元素,vm vue实例
  constructor(el, vm){
    this.$vm = vm
    this.$el = document.querySelector(el)
    if(this.$el){
      console.log(this.$el);
      //执行编译
      this.complie(this.$el)
    }
  }

  complie(el){
    // 遍历el树
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 判断是否是元素
      if(this.isElement(node)){
        console.log('编译元素', node.nodeName);
        this.complieElement(node)
      }else if(this.isInter(node)){
        console.log('编译一下插值绑定' + node.textContent);
        this.complieText(node)
      }

      // 递归子节点
      if(node.childNodes && node.childNodes.length > 0){
        this.complie(node)
      }
    });
  }

  isElement(node){
    return node.nodeType === 1
  }

  // 判断是否插值表达式
  isInter(node){
    // 首先是文本标签,其次内容是{{xxx}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  // 替换插值表达式
  complieText(node){
    // // 静态调用RegExp.$1刚刚isInter方法中匹配的插值文本部分
    // node.textContent = this.$vm[RegExp.$1]
    this.update(node, RegExp.$1, 'text')
  }

  complieElement(node){
    // 节点是元素,遍历其属性列表
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      // 指令以v-xx='oo'开头
      // 属性名
      const attrName = attr.name
      // 属性值
      const exp = attr.value
      if(this.isDirective(attrName)){
        // 拿到指令名
        const dir = attrName.substring(2)
        // 执行指令
        this[dir] && this[dir](node, exp)
      }

      // 事件处理
      if(this.isEvent(attrName)){
        // @click=""
        const dir = attrName.substring(1)
        // 事件监听
        this.eventHandler(node, exp, dir)
      }
    })
  }

  // 判断是否指令
  isDirective(attr){
    return attr.indexOf('v-') === 0
  }

  // v-text
  text(node, exp){
    // // 把子元素也覆盖
    // node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }

  // v-html
  html(node, exp){
    // // 把子元素也覆盖
    // node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }

  update(node, exp, dir){
    // 指令对应更新函数xxUpdater
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])

    // 更新处理,封装更新函数,更新对应dom元素
    new Watcher(this.$vm, exp, function(val){
      fn && fn(node, val)
    })
  }

  textUpdater(node, value){
    node.textContent = value
  }

  htmlUpdater(node, value){
    node.innerHTML = value
  }

  isEvent(dir){
    return dir.indexOf('@') === 0
  }
  eventHandler(node, exp, dir){
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
    node.addEventListener(dir, fn.bind(this.$vm))
  }

  // v-model="xx"
  model(node, exp){
    // update方法只能赋值更新,单项
    this.update(node, exp, 'model')
    // 事件监听
    node.addEventListener('input', e => {
      // 新的值付给数据即可
      this.$vm[exp] = e.target.value
    })
  }
  modelUpdater(node, value){
    // 表单元素赋值
    node.value = value
  }
}

评论(0)


最新评论

  • 1

    1

  • 1

    1

  • -1' OR 2+158-158-1=0+0+0+1 or 'TKCTZnRa'='

    1

  • 1

    1

  • 1

    1

  • 1

    1

  • 1

    1

  • @@5Qa2D

    1

  • 1

    1

  • 1

    1

日历

2025年09月

 123456
78910111213
14151617181920
21222324252627
282930    

文章目录

推荐关键字: Linux webpack js 算法 MongoDB laravel JAVA jquery javase redis