赞
踩
众所周知,Vue框架是一个渐进式的响应式框架,它可以做到数据的实时监听以及动态修改,而在vue的底层中究竟是如何实现的呢?在本篇文章中将会一一介绍如何监听数据的变化,以及两层数据劫持等代码实现。本文是基于(1)来接着写的,有时间的小伙伴可以看一下(1)哦,当然不看也不会有太大的影响。链接如下
以下是项目的目录,其中dist文件夹是打包后生成的,index.html是新增的,也是实际的主页。
src文件夹下,index.js是打包后的入口文件,即去使用new Vue()时运行index.js文件,其余的是响应式的一些监听方法。接下来会对这些一一讲解。
该文件是使用new Vue()时所调用的文件,这里主要是进行一个初始化的方法,比如挂载一些用户选项等,目前这里写的是一个初始化data的方法,代码如下。
- import { initMixin } from "./init"
-
- function Vue(options){ // options就是用户的选项
- // debugger//可以调试代码
- this._init(options)//默认调用_init()
- }
-
- initMixin(Vue)//扩展了init方法
-
- export default Vue
该文件是一个initMixin方法,用于进一步封装初始化的方法,为了方便理解,这里我们将this使用vm来代替,并且把用户的选项挂载到vm实例上,然后进行一个状态的初始化。
- import { initState } from "./state"
-
- //这是个初始化的文件,通过这方法传入Vue然后进行初始化操作,方便解耦
- export function initMixin(Vue){//给Vue增加init方法的
- Vue.prototype._init = function(options){ // 用于初始化操作
- // 在vue中,vm.$options 就是获取用户的配置
-
- // 很多api都是这样,使用$XXX来写,所以$开头都是vue自己的属性
- const vm = this
- vm.$options = options//将用户的选项挂载到实例上,即这些是data里的数据
-
- //初始化状态
- initState(vm)
- }
- }
-
-
这个文件是用于初始化状态,首先进入initState()方法,判断用户的data里是否有数据,有数据的时候才会进行初始化。
而后在initData()中,由于Vue2的data可能是返回一个对象,也可能是返回一个方法,这是无法确定的,所以先进行数据的处理,将数据处理成一个对象。并且在这个方法中我们进行第一次劫持,将字段添加,使用户可以直接this.name调用而不需要this._data.name。而后进行数据的监听。
没有进行劫持的时候,调用data里的name实例,需要:this._data.name
进行第一次劫持后,只需要this.name就可以调用了
- import { observe } from "./observe/index";
-
- export function initState(vm){
- const opts = vm.$options;//获取所有的选项
- // if(opts.props){
- // initProps()
- // }
- if(opts.data){
- initData(vm)
- }
- }
-
- function proxy(vm,target,key){
- Object.defineProperty(vm,key,{ // vm.name
- get(){
- return vm[target][key] // vm._data.name
- },
- set(newValue){
- vm[target][key] = newValue
- }
- })
- }
- function initData(vm){
- // debugger
- //假如用户输入的是函数,这里就是判断后变成对象
- let data = vm.$options.data;//data可能是函数,也可能是对象
- data = typeof data === 'function' ? data.call(vm) : data//这样就处理了data数据
- // console.log(data)
-
- //data是用户的对象,将放回的对象放在了实例的_data上,并且进行观测
- vm._data = data
- //对数据进行劫持 vue2里采用了一个api defineProperty
- observe(data)
-
- // 将vm._data用vm来代理
- for(let key in data){
- proxy(vm,'_data',key)
- }
- }
该文件主要是对数据进行劫持,给对象里的属性添加get和set方法,并且判断是否已经进行过劫持。实现思路如下:
首先进入observe()方法,进行一系列判断后,如果没有被劫持过,那么我们去new一个Observe()类的对象,这时候会调用这个类里面的构造器,其中传入的参数data就是我们在state.js中处理过一次的对象。
在这个构造器里,我们先将他本身挂载到一个不可枚举的__ob__实例上,然后判断这个数据是否为数组,这时候有两种情况如下:
1. 是数组的时候,我们需要重写常用的七个变异方法,这里重写的代码会放在下面第六条。重写完方法后,由于数组中是可以存放对象的,那么我们也要将数组里的对象劫持,所以定义一个for循环,将数组里的数据判断是不是对象,如果是对象的话重新进行劫持。
注意:因为在oberve()方法中进行过判断,所以当数组里存放的不是对象的时候会return出去。
2.是对象的时候,这种情况就比较简单了,只需要进行数据劫持,赋予get和set方法就好了,当然同理对象中可能嵌套对象,所以我们也是进行for循环来判断。
注意:由于这里是“重新定义属性”,所以在性能方面就有落后,因此Vue2的性能是不如Vue3的。
- import { newArrayProto } from "./array";
-
- class Observer {
- constructor(data) {
- // Object.defineProperty只能劫持已存在的属性,后面增加或者删除的属实是无法劫持的(vue2里会为此单独写一些api,比如$set $delete)
-
- Object.defineProperty(data,'__ob__',{
- value:this,
- enumerable:false // 将__ob__变成不可枚举(循环的时候无法获取)
- });
-
- // data.__ob__ = this; // 给数据加了一个表示,如果数据上有__ob__,则说明该数据被检测过 将Observe的实例放在data的对象上
- if (Array.isArray(data)) {//判断,如果是一个数组,就不需要劫持了
- //这里重写数组中的七个变异方法,这几个方法是可以修改数组本身的
-
- data.__proto__ = newArrayProto // 需要保留数组原有的特性,并且可以重写部分方法
-
- this.observerArray(data) // 如果数组中放的是对象,可以监听到数据的变化
- } else {
- this.walk(data);
- }
- }
- walk(data) { // 让这个data循环对象,让属性依次劫持
- // '重新定义'属性,因为是重新定义,所以性能受到影响
- Object.keys(data).forEach(key => defineReactive(data, key, data[key]));
- }
- observerArray(data) { // 观测数组
- data.forEach(item => observe(item))
- }
- }
-
- // 第一个参数,要定义的是谁,key值,value值
- export function defineReactive(target, key, value) { // 闭包 属性劫持
- observe(value) // 采用递归 如果这个值是对象,会递归再次进行一次劫持
- Object.defineProperty(target, key, {
- get() { // 取值的时候执行get
- return value
- },
- set(newValue) { // 修改的时候执行set
- // 判断相同的时候,就不进行操作,不同的时候,进行赋值
- if (newValue === value) return
- value = newValue
- }
- })
- }
-
- export function observe(data) {
- //对这个对象进行劫持
- //先判断是不是对象
- if (typeof data !== 'object' || data == null) {
- return;//只对对象进行劫持
- }
-
- if(data.__ob__ instanceof Observer){ // 如果有这个实例,说明已经被代理过了
- return data.__ob__
- }
-
- //如果一个对象被劫持过了,那就不惜要再次劫持了(要判断是否被劫持过,可以增添一个实例来判断是否被劫持)
- return new Observer(data);
- }
这个文件是重写了数组的变异方法,所有能够修改原数组的方法都是变异方法,下面一共是七个,因为不能直接覆盖数组的原型,所以我们采取新数组的方式进行重写。
- //对数组中的部分方法进行重写
-
- let oldArrayProto = Array.prototype; // 获取数组的原型
-
- // newArrayProto.__proto__ = oldArrayProto
- export let newArrayProto = Object.create(oldArrayProto)
-
- let methods = [//找到所有的变异(能修改原数组的方法都是变异方法)方法
- 'push',
- 'pop',
- 'shift',//向后追加
- 'unshift',
- 'reverse',//反序
- 'sort',//排序
- 'splice'//删除
- ] // concat slice都不会改变原来的数组
-
- methods.forEach(method=>{
- // arr.push(1,2,3)
- newArrayProto[method] = function(...args){//这里重写了数组的方法
- // 内部调用原来的方法,这叫做函数的劫持 ,切片变成
- // push.call(arr)
- const result = oldArrayProto[method].call(this,...args)//这里的this就是19行的arr,
-
-
- // console.log(method)
- //需要对新增的数据再次进行劫持
- let inserted
- let ob = this.__ob__;
- switch(method){
- case 'push':
- case 'unshift':
- inserted = args;
- break;
- case 'splice':
- inserted = args.slice(2);
- break
- default:
- break;
- }
- console.log(inserted) // 新增的内容
- if(inserted){
- // 对新增的数组再次进行观测
- ob.observerArray(inserted);
- }
-
-
- return result
- }
- })
vue2的响应式可以说是入门源码的第一步,所以是一个十分重要的知识点,学习源码可以让我们对编写代码有一个更加清晰的了解和认知,同时在开发过程中遇到了bug能够更容易发现产生的原因。好啦本文大概就这些内容,如果有写的不好的地方希望能够指正出来,祝大家能够写出自己满意的代码~需要代码的可以私聊,暂时还没有上传git。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。