乐趣区

关于vue.js:手写vue原理数据响应化至模版渲染

大多数初学者只晓得 vue 中 data 变动,数据就会随之变动,模版数据也会随之变动。咱们须要有知其然而知其所以然的态度,上面就简略的实现下 数据响应式 模版渲染

简介

MVVM 框架的三要素:数据响应式、模板引擎及其渲染
数据响应式: 监听数据变动并在视图中更新

  • Object.defineProperty()
  • Proxy

模版引擎: 为模版语法翻译

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

渲染:把虚构 dom 转化为实在 dom

  • 模板 => vdom => dom

简略实现下数据响应原理

面试时候对于 vue 大家都会被问到其响应式原理,这个很简略都晓得是利用
Object.defineProperty() 实现变更检测,上面简略实现下。

每隔 1 秒 obj.foo 的值取以后工夫,始终在变更,每次变更都会调用 Object.defineProperty 中的 set 办法,这时能拿到新的 value 值,告诉 update 去更新视图。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app"></div>

  <script>
    // 数据响应式
    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {get() {console.log('get', key);
          return val
        },
        set(newVal) {if (newVal !== val) {console.log('set', key, newVal);
            val = newVal

            // 更新函数
            update()}
        },
      })
    }

    const obj = {}
    defineReactive(obj, 'foo', 'foo')
    
    function update() {app.innerText = obj.foo}

    setInterval(() => {obj.foo = new Date().toLocaleTimeString()}, 1000);
  </script>
</body>

</html>

最终效果图为如下,能够看出每次 obj.foo 的变更都会触发 get 和 set 办法。

vue 中的数据响应化

实现目标:counter 变动时候模版语法失去解析,nvue.js 是咱们实现响应式的 vue 原理代码。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p @click="add">{{counter}}</p>
    <p n-text="counter"></p>
    <p n-html="desc"></p>![QQ20200807-165008-HD 2.gif](/img/bVbKWkF)
  </div>

  <script src="nvue.js"></script>

  <script>
    const app = new NVue({
      el:'#app',
      data: {
        counter: 1,
        desc: '<span style="color:red"> 数据响应 </span>'
      },
      methods: {add() {this.counter++}
      },
    })
    setInterval(() => {app.counter++}, 1000);
    
  </script>
</body>
</html>

原理剖析

1. 首先如图咱们须要在初始化过程中,对data 进行响应化解决,劫持监听 data 内的所有属性。
2. 同时对模板执行编译,找到其中动静绑定的数据,解析指令。例如解析下面中的 n -text,从 data 中获取并初始化视图,这个过程产生在 Compile 中。
3. 同时定义一个更新函数和 Watcher,未来对应数据变动时 Watcher 会调用更新函数。
4. 因为 data 的某个 key 在一个视图中可能呈现屡次,所以每个 key 都须要一个管家 Dep 来治理多个 Watcher。
5.data 中数据一旦发生变化,会首先找到对应的 Dep,告诉所有 Watcher 执行更新函数。

具体实现 nvue

1. 执行初始化,对 data 执行响应化解决,nvue.js, 其中

// 数据响应式
function defineReactive(obj, key, val) {}

// 让咱们使一个对象所有属性都被拦挡 observe
function observe(obj) {if (typeof obj !== 'object' || obj == null) {return}
// 创立 Observer 实例: 当前呈现一个对象,就会有一个 Observer 实例
  new Observer(obj)
}

// 1. 响应式操作
class NVue {constructor(options) {
    // 保留选项
    this.$options = options
    this.$data = options.data;

    // 响应化解决
    observe(this.$data)
  }
}

// 做数据响应化
class Observer {constructor(value) {
    this.value = value
    this.walk(value)
  }

  // 遍历对象做响应式
  walk(obj) {Object.keys(obj).forEach(key => {defineReactive(obj, key, obj[key])
    })
  }
}

2. 为 $data 做代理, 这里 data 中的参数都要写成 vm.$data[key],而咱们心愿每次 vm[key]更新时候就能够触发,所以这里做了个代理,把 vm.$data[key]值塞入 vm[key]中

class NVue {constructor(options) {
    ...

    // 代理
    proxy(this)
    
    // 编译器
    new Compiler('#app', this)
  }
}

// 代理 data 中数据, 转发作用
function proxy(vm) {Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {get() {return vm.$data[key]
      },
      set(v) {vm.$data[key] = v
      }
    })
  })
}

3. 编译 Compile,编译模板中 vue 模板非凡语法,初始化视图、更新视图。

// Compiler: 解析模板,找到依赖,并和后面拦挡的属性关联起来
// new Compiler('#app', vm)
class Compiler {constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    // 执行编译
    this.compile(this.$el)
  }

  compile(el) {
    // 遍历这个 el
    el.childNodes.forEach(node => {
      // 是否是元素,编译元素
      if (node.nodeType === 1) {this.compileElement(node)
        // 是否为 {{}} 文本,编译文本
      } else if (this.isInter(node)) {this.compileText(node)
      }

      // 递归
      if (node.childNodes) {this.compile(node)
      }
    })

  }

  // 解析绑定表达式{{}}
  compileText(node) {
    // 获取正则匹配表达式,从 vm 外面拿出它的值
    // node.textContent = this.$vm[RegExp.$1]
    console.log(RegExp.$1)
    this.update(node, RegExp.$1, 'text')
  }

  // 编译元素
  compileElement(node) {
    // 解决元素下面的属性,典型的是 n -,@结尾的
    const attrs = node.attributes
    Array.from(attrs).forEach(attr => {// attr:   {name: 'n-text', value: 'counter'}
      console.log(attr)
      const attrName = attr.name
      const exp = attr.value
      if (attrName.indexOf('n-') === 0) {
        // 截取指令名称 text
        const dir = attrName.substring(2)
        // 看看是否存在对应办法,有则执行
        this[dir] && this[dir](node, exp)
      }
    })
  }

  // n-text
  text(node, exp) {// node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }

  // n-html
  html(node, exp) {// node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }

  // dir: 要做的指令名称
  // 一旦发现一个动静绑定,都要做两件事件,首先解析动静值;其次创立更新函数
  // 将来如果对应的 exp 它的值发生变化,执行这个 watcher 的更新函数
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])

    // 更新,创立一个 Watcher 实例
    new Watcher(this.$vm, exp, val => {fn && fn(node, val)
    })

  }

  // 更新 v -text 文本内容
  textUpdater(node, val) {node.textContent = val}
  // 更新 v -html 文本内容
  htmlUpdater(node, val) {node.innerHTML = val}

  // 文本节点且形如{{xx}}
  isInter(node) {return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

}

4.依赖收集

视图中会用到 data 中某 key,这称为依赖。同一个 key 可能呈现屡次,每次都须要收集进去用一个 Watcher 来保护它们,此过程称为依赖收集。多个 Watcher 须要一个 Dep 来治理,须要更新时由 Dep 对立告诉, 通过下面的原理剖析图能够很容易看出。

实现思路:
1. 在 defineReactive 函数中为每一个 key 创立一个 Dep 实例。
2. 初始化视图每一个 key 创立对应的一个 watcher 实例。
3. 因为触发 name1 的 getter 办法,便将 watcher1 增加到 name1 对应的 Dep 中。
4. 当 name1 更新,setter 触发时,便可通过对应 Dep 告诉其治理所有 Watcher 更新。

申明 watcher 和 Dep

// 治理一个依赖,将来执行更新
class Watcher {constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn

    // 读一下以后 key,触发依赖收集
    Dep.target = this
    vm[key]
    Dep.target = null
  }

  // 将来会被 dep 调用
  update() {this.updateFn.call(this.vm, this.vm[this.key])
  }
}

// Dep: 保留所有 watcher 实例,当某个 key 发生变化,告诉他们执行更新
class Dep {constructor() {this.deps = []
  }

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

依赖收集,创立 Dep 实例

// 数据响应式
function defineReactive(obj, key, val) {

  // 递归解决
  observe(val)

  // 创立一个 Dep 实例
  const dep = new Dep()

  Object.defineProperty(obj, key, {get() {console.log('get', key);

      // 依赖收集: 把 watcher 和 dep 关联
      // 心愿 Watcher 实例化时,拜访一下对应 key,同时把这个实例设置到 Dep.target 下面
      Dep.target && dep.addDep(Dep.target)

      return val
    },
    set(newVal) {if (newVal !== val) {console.log('set', key, newVal);
        observe(newVal)
        val = newVal

        // 告诉更新
        dep.notify()}
    },
  })
}

最终实现成果

那么事件处理怎么做呢,能够本人试一下,前面文章也会阐明。心愿本文可能让初学者对 vue 数据响应真正理解。

退出移动版