Vue核心三大模块实现原理: 响应式、vdom、模板编译

知其然知其所以然,只有了解了vue原理才能更好的应用.

响应式

什么是vue响应式?

响应式就是组件data的数据一旦发生变化, 会立即触发视图的重新渲染,是实现数据驱动的第一步.如下例所示:

 <template>
    <p>{{name}}</p>
    <button @click="changeMes">改名</button>
 </template>
 <script>
 export default {
   data() {} {
      return {
         name: 'aley'
      }
   },
   methods: {
     changeMes() {
      // name值改变,视图发生更新
       this.name = '杨家祈'
     }
   }
 }
 </script>
如何实现响应式

原理: vue.js 通过Object.defineProperty()来劫持各个属性的setter,getter,通过getter读取data中的数据(收集视图依赖的数据);当数据变更时,自动调用setter方法, 发布消息给订阅者,触发相应的监听回调,进行视图更新.

  • 如何侦测数据的变化(数据劫持/代理):Object.definePropertyes6的proxy(vue3.0启用,这里暂且不讲)
    基本用法:
    let data = {}
    let name = 'zs'
    Object.defineProperty(data, 'name' , {
       get: function() {
          console.log('get')
          return name
       },
       set: function(newVal) {
         console.log('set')
         name = newVal
       }
    })
    console.log(data.name)  // get  zs
    data.name = 'aley'
    console.log(data.name)  // set  get  aley
    
    响应式代码演示:
    注意:
    Object.defineProperty无法监听新增/删除属性, 可通过Vue.set新增响应性属性//Vue.delete删除属性
    Object.defineProperty只能监听对象的变化,无法监听数组的变化,需要特殊处理
    // 复杂对象
    let  data = {
      name: 'aley',
      info: {
         age: 18,
         address: ['昆明', '建水']
      },
      nums: [1,2,3,4]
    }
    
    // 数组特殊处理
    // 重新定义数组原型,避免污染全局数组原型
    const oldarrPrototype = Array.prototype
    // 创建新原型对象,原型指向arrPrototype , 再扩展新的方法,不会影响原型
    const newProto = Object.create(oldarrPrototype) ; // Array {}
    ['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach((method) => {
      newProto[method] = function() {
          // 触发视图更新
         updateView()
         oldarrPrototype[method].call(this, ...arguments)
         // 相当于 Array.prototype.push.call(this, ...arguments)
       
      }
    })
    
    // 监听数据
    observe(data)
    // 监听数据
    function observe(obj) {
     if (!obj || typeof obj !== 'object') return
     // 数组特殊处理
     if (Array.isArray(obj)) {
        obj.__proro__ = newProto
        return
     }
     // 重新定义各个属性
     Object.keys(obj).forEach((key) => {
       defineReactive(obj, key, obj[key])
     })
    }
    // 定义各个属性
    function defineReactive(obj, key, value) {
     // 如果属性值为对象,需深听监听,递归子属性
     observe(value)
     Object.defineProperty(obj, key, {
       get: function() {
         console.log('get')
          return value
       },
       set: function(newVal) {
         if (newVal !== value) {
            // 如果新属性值为对象,需深听监听,递归子属性
            observe(newVal)
            console.log('set')
            value = newVal
            // 更新视图
            updateView()
         }  
       }
     })
    }
    function updateView() {
     console.log('更新视图')
    }
    // data.name= '杨家祈'     
    // data.info.age = 8   // get -> set ->  '更新视图'
    data.nums.push(2)  // 如果不特殊处理数组的监听, 无法立即更新视图
    console.log(data)
    
    : 为了解决以上问题, vue3.0启用Proxy实现响应式,但proxy兼容性不好,无法使用polyfill

虚拟DOM(vdom)

什么是虚拟DOM

虚拟DOM: js模拟DOM结构,计算出最小的变更,操作DOM

  • 用JS对象结构模拟DOM树,然后用这个JS对象构建一个真正的 DOM 树,插到文档当中;
  • 当状态变更的时候,重新构造一棵新的对象树。
  • 比较两棵虚拟DOM树的差异,diff算法是vdom中最核心,最关键的部分;
  • 把两棵树之间的差异应用到所构建的真正的DOM树上,视图就更新了
    参考文档: 深度剖析:如何实现一个 Virtual DOM 算法

产生背景:

  • 当传统的api或jQuery操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程.轻微的触碰可能就会导致页面重排,频繁操作可能导致页面卡顿,耗费性能.

  • vue和react都是数据驱动视图,不直接操作DOM,如何有效操作DOM?
    相对于DOM对象,原生的 JS 对象处理起来更快,而且更简单。DOM 树上的结构、属性信息我们都可以用 JS对象表示出来.JS的执行速度很快, 特别v8引擎普及的背景下.
    JS模拟DOM结构:

    <div id="div" class="container">
      <p>vdomp>
      <span style="color:#f0f0f0f">
         <a>diff算法a>
      span>
    div>
    
    {
      tag: 'div',
      props: {
         id: 'div',
        className: 'container'
      },
      children: [
        {
          tag: 'p',
          children: 'vdom'
        },{
          tag: 'span',
          props: {style: 'color: #f0f0f0'},
          children: [
            {
               tag: 'a',
               children: 'diff算法'
            }
          ]
        }
      ]
    }
    
什么是Diff算法

diff算法是vdom中最核心,最关键的部分。diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁

树diff的时间复杂度O(n^3) ,如遍历tree1/遍历tree2 /排序 ,三个步骤下来, 如果是1000个节点就计算1亿次,算法不可用, 基于以下几点,优化时间复杂度为O(n):

  • 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
  • 同一层级的一组节点,通过唯一的id进行区分;
  • 只在同层级进行比较, 不会跨层级比较;
  • tag不同,则直接删掉重建,不再深度比较
    Vue核心三大模块实现原理: 响应式、vdom、模板编译_第1张图片
  • tag和key,两者都相同,则认为是相同节点,不再深度比较

以循环渲染v-forkey举例说明:

key的作用就是用来跟踪节点身份, 对比组件自身新旧DOM进行更新的, diff算法会根据key值去对新旧元素进行比对,如果key值对应的dom元素被改动的话,则会进行重新渲染,删除之前的旧元素将新的元素放进去.
: key不能乱用,尽量使用和业务实体相关联的值,尽量不用index!!

let list = ['a', 'b', 'c']
<!--遍历数组 -->
<ul>
    <!--key值为item,如果把a,b的值调换,现key值所对应的dom元素没有变化,此时同样的只需变换元素排列顺序就可以了 -->
    <li v-for="(item, index) in list" :key="item"></li>
    <!--key值为index,如果把a,b的值调换,通过key值去对比,发现index对应的值不一样,则会删除之前旧的元素,生成新的元素,会增加DOM操作次数,降低性能-->
    <li v-for="(item, index) in list" :key="index"></li>
</ul>

模板编译

模板不是html, 它有插值,指令,js表达式,能实现循环,判断。html是标签语言,只有JS能实现循环判断等,因此把模板转化为某种JS代码(虚拟 DOM 渲染函数),即为模板编译

编译过程:

  1. 模板解析为render函数,执行render函数返回vnode;

    const template = '

    {{message}}

    '
    //message : 11 // 模板编译 with(this) { // 创建p标签 返回vnode

    11

    return _c('p', [_v(_s(message))]) } // _c createElement -> vnode // _v createTextNode // _s toString
  2. 基于vnode再执行patch和diff

  3. 渲染和更新

总结

Vue核心三大模块实现原理: 响应式、vdom、模板编译_第2张图片

初次渲染过程:
  1. 解析模板为render函数
  2. 触发响应式,监听data属性getter,setter
  3. 执行render函数(期间会触发getter),生成vnode
  4. patch(elem,vnode) elem:挂载节点
更新过程:
  1. 修改data,触发setter
  2. 重新执行render函数,生成newVnode
  3. patch(vnode,newVnode),diff算法比较差异
总结

使用Object.defineProperty将data中的所有属性都转为存取器属性,然后在首次渲染过程中把属性的依赖关系记录下来并为这个Vue实例添加观察者。当数据变化时,setter会调用Dep.notify通知订阅者数据变动,最后订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。

Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少
渲染函数

你可能感兴趣的:(VUE)