精华内容
下载资源
问答
  • coderwhy Vue面试题-vue响应式原理笔记

    Vue的响应式原理

    • 数据发生改变,界面跟着更新,并不是理所当然的,Vue内部是做了很多封装的

    依赖技术的分析和学习

    • 首先,来看一个最简单的Vue响应式例子:
     <div id="app">
            {{message}}
     </div>
       <script src='./js/vue.js'></script>
        <script>
            const app = new Vue({
                el: '#app',
                data: {
                    message: '哈哈哈哈',
                    name: 'coderYYY'
                }
            })
        </script>
    
    • 分析
      • 1.app.message修改数据,Vue内部是如何监听message数据的改变?
        • Vue2 -> object.defineProperty -> 监听对象属性的改变
        • Vue3 -> Proxy
      • 2.当数据发生改变,Vue是如何知道要通知哪些人,界面发生刷新?
        • 发布者订阅者模式
        const obj = {
                message: '哈哈哈哈',
                name: 'yyy'
            }
            // 1.监听值的改变 -> Object.defineProperty(属性所在的对象,要添加或者修改的对象属性,属性描述符)
            Object.keys(obj).forEach(key => {
                let value = obj[key]
    
                Object.defineProperty(obj, key, {
                    set(newValue) {
                        console.log('监听' + key + '改变');
                        // 告诉谁?谁用告诉谁?谁在用?
                        // 根据解析html代码,获取到哪些人有用属性
                        // 张三/李四/王五 在用
                        value = newValue
                        // dep.notify()// 通知
                    },
                    get() {
                        // 谁用一次就会调用一次get
                        // 张三: get -> update
                        // 李四: get -> update
                        // 王五: get -> update
                        console.log('获取' + key + '对应的值');
                        return value
                    }
                })
            })
            // 2.发布者订阅者模式
            // 发布者
            class Dep { // 存储所有对属性有依赖的东西
                constructor() {
                    this.subscribe = [] // subscribe 订阅 这个数组记录谁要订阅属性
                }
                addSub(watcher) {
                    this.subscribe.push(watcher)
                }
                notify(){
                    this.subscribe.forEach(item=>{
                        item.update()
                    })
                }
            }
            const dep = new Dep()
            // 订阅者
            class Watcher {
                constructor(name) {
                    this.name = name;
                }
                update() {
                    console.log(this.name + '发生update');
                }
            }
            const w1 = new Watcher('张三')
            dep.addSub(w1)
            const w2 = new Watcher('李四')
            dep.addSub(w2)
            const w3 = new Watcher('王五')
            dep.addSub(w3)
            dep.notify()
    

    在这里插入图片描述
    在这里插入图片描述

    通过图解理解过程

    在这里插入图片描述

    展开全文
  • 前端面试题:vue响应式原理 Vdom diff

    千次阅读 2020-08-22 16:30:14
    vue响应式原理,也算是面试中再常见不过的题目了,之前遇见这道题目只会说:利用的是Object.defineProperty进行的数据劫持,监听数据的变化,通知watcher进行的数据更新。总的来说这是没错的,但是只要面试官...

    vue的响应式原理,也算是面试中再常见不过的题目了,之前遇见这道题目只会说:利用的是Object.defineProperty进行的数据劫持,监听数据的变化,通知watcher进行的数据更新。总的来说这是没错的,但是只要面试官进一步的问,那一定是满脸的问号。昨天一天也是没有面试机会,所以就研究了一天这个东西,算是搞明白了(自我感觉),今天就把他来写成文章,希望大佬看到哪里不对给出指导,本文可能会有点长。上正文。

    现在最流行的框架非vue,react莫属,他们流行起来的原因,离不开响应式,因为它在做一些数据更新的时候,会去更新相应的视图,把我们从操作DOM中释放出来,让我们不再去自己操作dom,这也就是所说的数据驱动吧。

    React是通过this.setState去改变数据,然后根据新的数据重新渲染出虚拟DOM,最后通过对比虚拟DOM找到需要更新的节点进行更新。也就是说React是依靠着虚拟DOM以及DOM的diff算法做到这一点的。

    vue在2.0中依赖的是Object.defineProperty,那我们就先来介绍一下Object.defineProperty,这个方法是干什么的呢?我也是去看了一下它的文档,它主要是用来给一个对象添加属性,或者修改它现有的属性的,然后把这个对象返回,然后呢,在defineProperty中,有set和get,set在设置(修改)属性的值的时候被触发,get在获取属性的值的时候被触发

    举个例子(可以试着自己写一下)

    var oldValue;
    var obj = {};
    Object.defineProperty(obj,"text",{
    	get : function(){
    		console.log('get被调用');
    		return oldValue
    	},
    	set : function(newValue){
    		console.log('set被调用')
    		oldValue = newValue
    	}
    })

    为了验证上边加粗的地方那句话,我们以上边为例,就是在obj对象上添加text属性,并对它进行监听,看看是不是我们获取或修改text的值的时候会调用get和set方法。

    get set调用

    这个呢是我在控制台里直接运行的结果,在我直接调用Object.defineProperty的时候,会返回这个对象,返回空对象obj,这个是没错的,在我设置obj.text的时候,打印set被调用是没错的,紧接着通过obj.text获取值的时候,会去调用get方法并打印。

    我们了解了Object.defineProperty的用法之后,就可以实现一个简单的绑定了,不知道大家还记不记得刚开始学vue的时候,一个input和一个p的故事,随着input内容的改变,p的内容跟着改变。接下来我们就通过Object.defineProperty,来实现一个简单的绑定。

    <input type="text" id="input" />
    <p id="p"></p>
    
    var inPut = document.getElementById('input');
    var p = document.getElementById('p');
    var obj = {}
    Object.defineProperty(obj, 'msg', {
      set: function (newVal) {
        input.value = newVal
        p.innerText = newVal
      }
    })
    Object.defineProperty(obj,'text',{
    })
    inPut.addEventListener('keyup', function (event) {
      obj.msg = event.target.value
    })

    这时候一个绑定就实现了,如果面试官让手写实现一个双向绑定,我觉得这些应该是可以通过的了。这时候的效果就是input输入什么p标签显示什么了。

    效果展示

    下面我就要说一下观察者模式了,主要分为注册环节和发布环节。

    蒙了吧,脑瓜子是不是WongWong的,那就对了,举个简单的例子:

    我们在准备秋招的时候,会给公司投递自己的简历,然后就会进入安排笔试的环节(筛学历的就不说啦,都是泪),这个时候我们总不能隔一小段时间来问公司一下什么时间安排笔试,隔一段时间问一次对我们来说是特别麻烦的呀,但是公司也要招人啊,这个时候呢公司的处理就和观察者模式特别的像了,为什么这么说呢?

    当公司有人投递简历的时候,他会让你填写手机号,邮箱等一些信息,就是观察者模式的注册环节。

    等笔试安排的时候,他会给所有投简历的人发短信,发邮件,这就是观察者模式的发布环节。

    function Recruit() {
       this.dep = [];
       resume(paper) {
           this.dep.push(paper)
       }
       writtenExamination() {
           this.dep.forEach(item => item())
       }
    }
    
    var recruit = new Recruit()
    
    //张三李四王五投简历并留下电话
    recruit.resume(()=>{'console.log("张三")'})
    recruit.resume(()=>{'console.log("李四")'})
    recruit.resume(()=>{'console.log("王五")'})
    
    //通知他仨参加笔试
    wantCake.notify()

    了解完相关的预备知识之后,我们来正式的说响应式的原理。

    我们也是跟着生命周期的路线来介绍,首先是init阶段, vue 的 data的属性都会被reactive化,也就是加上 setter/getter函数。

    function defineReactive(obj: Object, key: string, ...) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter () {
                ....
                dep.depend()
                return value
                ....
            },
            set: function reactiveSetter (newVal) {
                ...
                val = newVal
                dep.notify()
                ...
            }
        })
    }
    class Dep {
        static target: ?Watcher;
        subs: Array<Watcher>;
        depend () {
            if (Dep.target) {
                Dep.target.addDep(this)
            }
        }
        notify () {
            const subs = this.subs.slice()
            for (let i = 0, l = subs.length; i < l; i++) {
                subs[i].update()
            }
        }
    }

    其中这里的Dep就是一个观察者类,每一个data的属性都会有一个dep对象。当getter调用的时候,去dep里注册函数,然后在数据变化(setter)的时候,去通知刚才注册的函数。

    接下来说一下Mount阶段发生了什么

    mountComponent(vm: Component, el: ?Element, ...) {
        vm.$el = el
        ...
        updateComponent = () => {
          vm._update(vm._render(), ...)
        }
        new Watcher(vm, updateComponent, ...)
        ...
    }
    class Watcher {
      getter: Function;
      constructor(vm: Component, expOrFn: string | Function, ...) {
        ...
        this.getter = expOrFn
        Dep.target = this
        this.value = this.getter.call(vm, vm)
        ...
      }
    }

    mount 阶段的时候,会创建一个Watcher类的对象。这个Watcher实际上是连接Vue组件与Dep的桥梁。每一个Watcher对应一个vue component。

    这里可以看出new Watcher的时候,constructor 里的this.getter.call(vm, vm)函数会被执行。getter就是updateComponent。这个函数会调用组件的render函数来更新重新渲染。

    而render函数里,会访问data的属性,比如说访问到上边例子

    render: function (createElement) {
      return createElement('h1', this.text)
    }

    这个时候呢会去调用text的getter函数

    // getter函数
    get: function reactiveGetter () {
        ....
        dep.depend()
        return value
        ....
     },
    
    // dep的depend函数
    depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
    }

    在depend的函数里,Dep.target就是watcher本身(我们在class Watch里讲过,不记得可以往上第三段代码),这里做的事情就是给text注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了text,那么这个组件相对应的Watcher对象都会被注册到text的Dep中。

    这个过程就叫做依赖收集。

    收集完所有依赖text属性的组件所对应的Watcher之后,当它发生改变的时候,就会去通知Watcher更新关联的组件。

    updata(更新)阶段

    当text发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用update函数更新。

    notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
    }

    什么意思呢:就是说有多少个数据被监听,就会有多少个Dep去进行收集事件,然后紧接着创建watcher,每一个用到这个数据的地方(component)都会有一个watcher,然后watcher链接Dep和component。

    总结一下:组件初始化的时候,先给每一个Data属性都注册getter,setter,也就是reactive化。然后再new 一个自己的Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM。在调用render的时候,就会需要用到data的属性值,此时会触发getter函数,将当前的Watcher函数注册进sub里。

    当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。

    响应式原理差不多就是这样的啦,下面说一下vDom,都说添加了vDom的vue比之前的渲染速度提升了好几倍,但是也没有准确的数据吧,并且大大地降低了内存的消耗,接下来介绍一下什么是vDom,为什么用到他。

    我们先说一下模板转换成视图的过程:

    Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树。在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

    渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。

    VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。

    patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

    Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。

    let element={
        tagName:'ul',//节点标签名
        props:{//dom的属性,用一个对象存储键值对
            id:'list'
        },
        children:[//该节点的子节点
            {tagName:'li',props:{class:'item'},children:['aa']},
            {tagName:'li',props:{class:'item'},children:['bb']},
            {tagName:'li',props:{class:'item'},children:['cc']}
        ]
    }
    对应的html写法是:
    <ul id='list'>
        <li class='item'>aa</li>
        <li class='item'>aa</li>
        <li class='item'>aa</li>
    </ul>

    虚拟dom有什么用呢?

    虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。

    为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。

    其实虚拟DOM在Vue.js主要做了两件事:

    1、提供与真实DOM节点所对应的虚拟节点vnode

    2、将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图

    为什么要用到虚拟dom呢?

    1、具备跨平台的优势

    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

    2、操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。

    因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

    Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

    3、提升渲染性能

    Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

    为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。我们通过patch 的核心----diff 算法,找出本次DOM需要更新的节点来更新,其他的不更新。比如修改某个model 100次,从1加到100,那么有了Virtual DOM的缓存之后,只会把最后一次修改patch到view上。

    Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。

    diff 算法包括几个步骤:

    1、用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中

    2、当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异

    3、把所记录的差异应用到所构建的真正的DOM树上,视图就更新了

    diff算法的实现,diff 算法本身非常复杂,实现难度很大。两个核心函数实现流程:

    patch(container,vnode) :初次渲染的时候,将VDOM渲染成真正的DOM然后插入到容器里面。

    patch(vnode,newVnode):再次渲染的时候,将新的vnode和旧的vnode相对比,然后之间差异应用到所构建的真正的DOM树上。

    function createElement(vnode) {    
    var tag = vnode.tag  
    var attrs = vnode.attrs || {}    
    var children = vnode.children || []    
    if (!tag) {       
     return null  
      }    
    // 创建真实的 DOM 元素    
    var elem = document.createElement(tag)   
     // 属性    
    var attrName    
    for (attrName in attrs) {    
        if (attrs.hasOwnProperty(attrName)) { 
               // 给 elem 添加属性
               elem.setAttribute(attrName, attrs[attrName])
            }
        }
        // 子元素
        children.forEach(function (childVnode) {
            // 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。
            elem.appendChild(createElement(childVnode))  // 递归
        })    // 返回真实的 DOM 元素   
     return elem
    }

    第二种情况,这里我们只考虑vnode与newVnode如何对比的情况: 

    function updateChildren(vnode, newVnode) {
        var children = vnode.children || []
        var newChildren = newVnode.children || []
      // 遍历现有的children
        children.forEach(function (childVnode, index) {
            var newChildVnode = newChildren[index]
      // 两者tag一样
            if (childVnode.tag === newChildVnode.tag) {
                // 深层次对比,递归
                updateChildren(childVnode, newChildVnode)
            } else { 
      // 两者tag不一样
               replaceNode(childVnode, newChildVnode) 
           }
        }
    )}

     

    展开全文
  • 一、谈一下你对MVVM原理的理解 1.MVC是Model-View- Controller的简写。即模型-视图-控制器。是最早的架构模型,是从前台到后台统一称之为MVC,比如前端交视图层(View),后端的数据库(Model),用户操作界面想...

    响应式数据的原理?

    1.首先回答对响应式的个理解,提出核心点是通过Object.defineProperty, Vue 在初始化数据时,会传入一个data对象,内部会默认的对data对象进行遍历,使用 Object.defineProperty 重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher) 如果属性发生变化会通 知相关依赖进行更新操作。 

    2.为什么要使用Object.defineProperty?

        它的特点可以使数据的获取和设置增加一个拦截的功能,我们可以在获取和设置的时候增加一些我们自己的逻辑,这个逻辑叫做依赖收集。比如我们取数据的时候可以收集一些依赖,过一会数据变化了可以告诉这些收集的依赖去更新。

    3.原理:在vue初始化的时候,会调用一个方法initData,用来初始化用户传入的data数据,然后new Observer,对数据进行观测,如果数据是个对象类型非数组的话,就会调一个this.walk(value)方法进行对象的处理,将对象进行遍历,然后使用defineReactive重新定义,采用的就是Object.defineProperty。

    总结:

            Vue采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,vue在初始话的时候,在initState方法中会调取initData方法初始化data数据,对data对象进行遍历,在这个方法中会调observe(监听器,观察者)对用户的数据进行监听,在observe中会对对象new Observe实例化创建监听,在observe中对数据进行判断,如果是数组执行observeArray深度进行监听,继续执行observe方法,如果当前传入的是对象则执行this.walk,对对象进行循环,重新定义对象的属性,这时使用的就是defineReactive,它是vue中的一个核心方法,用来定义响应式。在defineReactive方法中实例化了一个Dep(发布者),通过Object.defineProperty对数据进行拦截,把这些 property 全部转为 getter/setter。get数据的时候,通过dep.depend触发Watcher(订阅者)的依赖收集,收集订阅者,如果数据是数组,执行dependArray,对数组中的每个值通过depend都添加到依赖。set时,会对数据进行比较,如果数据发生了变化会通过dep.notify发布通知,通知watcher,更新视图。

    4.下面来看源码

    文件路径:src/core/instance/state.js

    源码:

    (1)vue在state数据初始化的时候会掉state.js中的一个initState方法

    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm) // 初始化数据
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }

    (2)initData(初始化data)

    function initData (vm: Component) {
      let data = vm.$options.data // 用户传入的数据
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) {
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      observe(data, true /* asRootData */) // 调observe对用户的数据进行观测
    }

    (3)observe方法(文件路径:src/core/observe/index.js,专门用来控制数据响应式处理的)

             里面会去判断,看数据是否已经被观测过了,如果没又被观测过就会new Observer去观测这个对象

    export function observe (value: any, asRootData: ?boolean): Observer | void {
      if (!isObject(value) || value instanceof VNode) { // 不是对象不进行监听
        return
      }
      let ob: Observer | void
      // 如果存在__ob__属性,说明该对象没被observe过,不是observer类
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) && // 是否可扩展
        !value._isVue
      ) {
        ob = new Observer(value) // 监听对象
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }

    (4)new Observer实例(监听的是两种类型,一种是数组,一种是对象)

    export class Observer {
      value: any;
      dep: Dep;
      vmCount: number; 
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) { // 判断是否是数组
          if (hasProto) {
            protoAugment(value, arrayMethods) // 改写数组原型方法
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value) // 去深度监听数组中的每一项
        } else { // 如果是对象走这儿
          this.walk(value) // 重新哦定义对象类型数据
        }
      }
      // 对当前传入的对象进行循环,重新定义对象的所有属性
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i]) // defineReactive是核心的方法,定义响应式
        }
      }
      // 深度监听
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }

    (5)defineReactive是vue中的核心方法,用来定义响应式,该方法中使用了Object.defineProperty()

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
     // 实例化一个Dep,这个Dep存在在下面的get和set函数的作用域中,用于收集订阅数据更新的Watcher
     // 这里一个Dep与一个属性(即参数里的key)相对应,一个Dep可以有多个订阅者。
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
    
      let childOb = !shallow && observe(val) // 如果还是一个对象,会继续调用observe方法,递归监听
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () { // 数据的取值
          // 如果这个属性在转化之前定义过getter,那么调用该getter得到value的值,否则直接返回val。
          const value = getter ? getter.call(obj) : val
          /*
          *注意这里,这里是Dep收集订阅者的过程,只有在Dep.target存在的情况下才进行这个操作
          *在Watcher收集依赖的时候才会设置Dep.target
          *所以Watcher收集依赖的时机就是Dep收集订阅者的时机。
          *调用get的情况有两种
          *一是Watcher收集依赖的时候(此时Dep收集订阅者)
          *二是模板或js代码里用到这个值,这个时候是不需要收集依赖的,只要返回值就可以了。
          */
          if (Dep.target) {
            dep.depend() // 收集依赖 watcher,当数据变了,会通知watcher更新数据
            // 不仅这个属性需要添加到依赖列表中,如果这个属性对应的值是对象或数组
            // 那么这个属性对应的值也需要添加到依赖列表中
            if (childOb) {
              childOb.dep.depend()
              // 如果是数组,那么数组中的每个值都添加到依赖列表里
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) { // 数据更新
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          // 当前的值和数据的值不一样的话,就会掉一个核心的方法dep.notify()
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify() // 触发数据对应的依赖进行更新,它就会通知视图更新
        }
      })
    }

    (6)dependArray()方法,如果不是对象是数组调用该方法

             在调用这个函数的时候,数组已经被observe过了,且会递归observe。(看上面defineReactive函数里的这行代码:var childOb = observe(val);)
             所以正常情况下都会存在__ob__属性,这个时候就可以调用dep添加依赖了。

    function dependArray (value: Array<any>) {
      for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend() // 添加依赖
        if (Array.isArray(e)) {
          dependArray(e) // 如果还是数据继续调用
        }
      }
    }

    (7)dep,(用来添加依赖,通知watcher更新数据)

    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>;
    
      constructor () {
        this.id = uid++ // 每个实例中的dep实例的id都是从0开始累加的
        this.subs = []
      }
      // 添加一个订阅者
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      // 删除一个订阅者
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
      // 追加依赖
      //让Watcher收集依赖并添加订阅者。
      //Dep.target是一个Watcher, 可以查看Watcher的addDep方法。
      //这个方法做的事情是:收集依赖后,调用了Dep的addSub方法,给Dep添加了一个订阅者
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
     // 通知watcher更新,通过调用subs里面的每个Watcher的update发布更新
      notify () {
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update() // 这儿会执行watcher文件下的update方法去更新数据
        }
      }
    }

    5.getter/setter方法拦截数据的不足

    (1)当对象增删的时候,是监控不到的,因为在observe data的时候,会遍历已有的每个属性添加getter/setter,而后面设置的属性并没有机会设置getter/setter,所以检测不到变化,同样的,删除对象属性的时候,getter/setter会跟着属性一起被删除掉,拦截不到变化。可以通过vue.set和vue.delete去更新数据。

    (2)getter/setter是针对对象的,对于数组的修改也是监控不到的。可以通过调用数组的变异方法,或对数组进行深度拷贝克隆来达到更新视图。

    二、相关概念

    1.双向数据绑定

    M ,即 model,指的是模型,也就是数据;V 即view,指的是视图,也就是页面展现的部分。
    双向数据绑定为:每当数据有变更时,会进行渲染,从而更新视图,使得视图与数据保持一致(model到view层);而另一方面,页面也会通过用户的交互,产生状态、数据的变化,这个时候,这时需要将视图对数据的更新同步到数据(view到model层)。

    我们上面说的Vue的数据响应式原理其实就是实现数据到视图更新原理,而视图到数据的更新,其实就是此基础上给可表单元素(input等)添加了change等事件监听,来动态修改model和 view。

    2.发布-订阅者模型

    订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。
    Vue中的Dep和Watcher共同实现了这个模型

    展开全文
  • Vue响应式原理之defineReactive defineReactive 不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助ES5新增的Object.defineProperty。 defineReactive接受五个参数。obj是要添加响应式数据的对象;...

    Vue响应式原理之defineReactive

    defineReactive

    不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助ES5新增的Object.defineProperty

    defineReactive接受五个参数。obj是要添加响应式数据的对象;key是属性名,val是属性名对应的取值;customSetter是用户自定义的setter;会在响应式数据的setter中执行,只有开发环境可用;通过shallow指定是否浅比较,默认深比较。

    
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property &amp;&amp; property.configurable === false) {
        return
      }
    
      const getter = property &amp;&amp; property.get
      if (!getter &amp;&amp; arguments.length === 2) {
        val = obj[key]
      }
      const setter = property &amp;&amp; property.set
    
      let childOb = !shallow &amp;&amp; observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal &amp;&amp; value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' &amp;&amp; customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow &amp;&amp; observe(newVal)
          dep.notify()
        }
      })
    }
    

    在函数内,首先实例化一个Dep实例depdep会在稍后添加为响应式数据自定义的get/set中发挥作用。接着获取属性描述符,如果属性不可配置,则无法调用Object.defineProperty来修改setter/getter,所以返回。

    如果原来已设置过setter/getter,缓存起来。当未自定义getter且arguments长度为2(即只传入了objkey)时,可以直接用方括号求值,使用闭包变量val缓存初始值。

    如果不是浅复制,执行observe(val),为val添加__ob__属性并返回__ob__指向的Observer实例。(只有数组和对象才可能是响应式,才能返回Observer实例)。

    使用Object.definePropertyobj[key]设置getter和setter。

    get内,如果原来已设置过getter,则用缓存的getter求值,否则使用闭包变量val作为返回值;同时添加依赖。此处为两个Dep实例添加依赖。dep是闭包变量,在getter/setter中会使用到。另一个Dep实例是childOb.dep,只用调用set/delete更新响应式数据时,才会触发;如果value是数组,还会遍历元素,为存在__ob__属性的元素收集依赖。

    set内,先获取更新前的值(逻辑和get内第一步一样)。判断更新前后的值是否相等,相等时直接返回;不等时,如果有缓存的setter,调用缓存的setter更新,否则直接赋值。值得注意的是,NaN === NaN是不成立的,反而NaN !== NaN是成立的,后面的判断语句newVal !== newVal && value !== value就是为了避免newVal/val都是NaN。在更新后的值newVal上执行observe,更新闭包变量childOb,并调用notify。

    参考链接

    原文地址:https://segmentfault.com/a/1190000017216175

    展开全文
  • 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它们有助于理解...
  • vue深入响应式原理是什么? v-model的原理? 深入响应式指的就是数据驱动 vue是通过数据劫持和事件的订阅发布模式来实现的,数据劫持指的是vue通过observer观察者对象对data选项中的属性做getter和setter设置,然后...
  • vue响应式是如何实现的? 听过太多回答,通过Object.defineProperty,可是再详细的问时,对方浑然不知。 先撸为敬 const Observer = function(data) { // 循环修改为每个属性添加get set for (let key in data) {...
  • 1.用一句话来概括Vue响应式原理。 当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),...
  • (1)vue-router实现原理 vue-router提供三种路由模式 1.hash模式 默认模式,通过路径中的hash值来控制路由跳转,不存在兼容问题 hash模式实现原理 在正常路径后跟一个 # 号,匹配 # 后边的路径为前端路由,通过window...
  • Vue数据响应式原理(以面试者的角度回答)
  • Vue 中主要通过以下 4 个步骤来实现响应式数据的 实现一个监听器(Observer),对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的...
  • Vue响应式原理

    2021-03-10 11:36:00
    Vue响应式原理 1、目标 模拟一个最小版本的Vue 响应式原理在面试的常问问题 实际项目中出现问题的原理层面的解决 ​ 给Vue实例新增一个成员是否是响应式的? ​ 给属性重新赋值成对象,是否是响应式的? 为学习Vue...
  • 12月份来临,想跳槽涨薪的朋友们都已经开始考虑起了年后的“金三银四”!但是明年的“金三银四”可能不如想象般火热。前端在飞速发展,市场却在逐渐冷却。公司招前端的面试考量点也悄然改变。但是不...
  • vue-router实现原理 vue-router提供三种路由模式 1.hash模式 默认模式,通过路径中的hash值来控制路由跳转,不存在兼容问题 hash模式实现原理 在正常路径后跟一个 # 号,匹配 # 后边的路径为前端路由,通过window....
  • 核心原理 Vue通过 Object.defineProperty 的 getter/setter 对收集的依赖项进行监听,在属性被访问和修改时通知变化,进而更新视图数据。 监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知...
  • Vue如何追踪变化 vue.js采用的是数据劫持结合发布和-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。 当你把一个普通的 ...
  • vue 自定义事件原理和订阅发布模式 订阅发布模式 订阅发布模式重点就是有一个事件中心,注册和触发都是对同一个对象 class EventEmitter { constructor() { this.subs = Object.create(null) } // 订阅事件 $...
  • 深入浅出Vue响应式原理

    千次阅读 2019-07-08 07:10:00
    前言 Vue 最独特的特性之一,是其非侵入性的响应式系统。...----官方文档 本文将针对响应式原理做一个详细介绍,并且带你实现一个基础版的响应式系统。本文代码请猛戳https://github.com/ljianshu/Blog ...
  • vue响应式原理源码:带你一步精通vue

    万次阅读 多人点赞 2020-05-07 15:29:06
    本文带你彻底理解vue响应式原理,带你写一个简易版的vue,实习vue数据响应式(数据劫持结合发布者-订阅者)、数组变异方法、编译模板(常用的指令如v-html、v-text、v-model、v-on等)、双向数据绑定的功能。
  • 戳蓝字"前端优选"关注我们哦!前言介绍从本文开始,我们正式进入vue3.0 源码解析流程。个人觉得从ceateApp入手并不是最佳的学习方案,所以我们先从composi...
  • Vue响应式原理 一.简介 Vue响应式的原理,其实就是基于ES5的Object静态方法: Object.defineProperty() 对这个方法做劫持,还有说法是代理,劫持数据的setter和getter.然后结合发布订阅模式,在数据发生变化的时候,通知...
  • 这篇文章主要是给不了解或者没接触过 vue 响应式源码的小伙伴们看的,其主要目的在于能对 vue响应式原理有个基本的认识和了解,如果在面试中被问到此类问题,能够知道面试官想让你回答的是什么?「PS:文中如有...
  • 文章目录vue响应式原理学习(1)1.什么是响应式2.实现响应式,我们需要做些什么3.如何侦测数据的变化3.1 Object.defineProperty实现3.1.1 Observer实现一个对象对所有成员的代理3.2 Proxy实现4.收集依赖4.1 为什么要...
  • 我们学习知识并不只是为了应付面试那种程度,对于面试应该作为我们的最低要求。这里就体现了数据安全思想,前端程序员或许考虑的没有Java程序员多,甚至没有为变量想过某个变量设置不可修改。由于工作的需要,我们也...
  • Vue响应式原理

    2021-05-19 17:07:22
    每天一个前端面试题之vue响应式原理 vue的官网对vue响应式原理进行了这样的描述: Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,414
精华内容 2,565
关键字:

vue响应式原理面试

vue 订阅