0

0

0

修罗

站点介绍

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

手写vue(下)

修罗 2020-10-26 1772 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)


最新评论

  • https://www.studyglobus.com/author/carroll34a5552/

    Ценники на вывоз мусора сейчас космос, любые лайфхаки по экономии в тему.

  • Richardgrold

    1xbet onlayn kazino platforması ilə vaxtınızı daha səmərəli və maraqlı keçirin. Saytın canlı kazino bölməsi istifadəçilərə fərqli bir dünya təklif edir. Burada canlı dilerli kazino oyunlarının hər bir növünü sınaqdan keçirə bilərsiniz. Krupyelərin peşəkarlığı oyunun dinamikasını və marağını artırır. Müntəzəm olaraq yenilənən oyun siyahısı cansıxıcılığa yol vermir. Hesabınızı artırmaq üçün onlarla fərqli və etibarlı üsul mövcuddur. Qazanılan pullar cəmi bir neçə dəqiqə ərzində şəxsi hesabınıza köçürülür. Aktiv fəaliyyətinizə görə fərdi bonuslar və hədiyyələr qazana bilərsiniz. Dəstək xidməti həm çat, həm də e-poçt vasitəsilə kömək göstərir. 1xbet ailəsinə qoşulun və böyük uduşlar üçün ilk addımı atın. onlayn kazino

  • 砂岩白丘

    我一直梦想, 看到你们相册那样的地方。真棒。

  • 城市地標

    我喜欢, 这里分享真实经验。你的项目 就是 属于这里的。请继续。

  • 宗教地標

    我热爱这样的想法, 去那么多国家。真的很鼓舞。

  • 觀景高地

    能感受到热爱。继续保持 感受。

  • 最佳季節

    我关注这样的资源, 写得很实在。你的网站 就是 属于这里的。加油。

  • 1

    1

  • 1

    1

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

    1

日历

2026年07月

   1234
567891011
12131415161718
19202122232425
262728293031 

文章目录

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