大多数初学者只晓得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) {}// 让咱们使一个对象所有属性都被拦挡observefunction 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数据响应真正理解。