vue源码

01-vue源码学习-柯里化函数

一、概念:

柯里化: 一个函数原本有多个参数, 之传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
偏函数: 一个函数原本有多个参数, 之传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数.
为什么要使用科里化? 为了提升性能. 使用科里化可以缓存一部分能力.

二、代码实现

vue源码中判断是否是html标签函数

    let tags = 'div,p,a,img,ul,li'.split(',')
    function makeMap(tags) {
      let set = {}
      tags.forEach(tag => set[tag.toLowerCase()] = true)
      return function (tagname) {
        return !!set[tagname.toLowerCase()]
      }
    }

    let isHtmlTag = makeMap(tags)
    console.log(isHtmlTag);
    console.log(isHtmlTag('div'));

三、柯里化函数原理其实是闭包

闭包使用的利弊衡量:

使用闭包可能会存在内存泄露,或者性能会有些问题。但是,如果在具体的场景中,使用闭包能够提高性能,而且内存泄露风险并不高,其实是可以使用闭包的。开发过程中,很多时候的抉择,并不是绝对的,只有适合的方法或者架构,极少有完美的方法或者架构。

02-vue源码学习-vue的基本执行流程和简单渲染模型

一、vue的基本执行流程

获得模板: 模板中有 “坑”(比如{{}})
利用 Vue 构造函数中所提供的数据来 “填坑”, 得到可以在页面中显示的 “标签了”
将标签替换页面中原来有坑的标签
总的来说:Vue利用我们提供的数据和 页面中模板生成了一个新的 HTML 标签 ( node 元素 ),替换到了页面中放置模板的位置.

二、简单渲染模型

Vue 本质上是使用 HTML 的字符串作为模板的, 将字符串的 模板 转换为 AST(抽象语法树), 再转换为 VNode(虚拟dom).

  • 模板 -> AST
  • AST -> VNode
  • VNode -> DOM

那一个阶段最消耗性能?

最消耗性能是字符串解析 ( 模板 -> AST )

三、模仿vue的渲染流程

将真正的 DOM 转换为 虚拟 DOM
将虚拟 DOM 转换为 真正的 DOM

<!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="root">
    <div class="c1">
      <div title="tt1" id="id">{{ name }}</div>
      <div title="tt2">{{ age }}</div>
      <div title="tt3">{{ gender }}</div>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    </div>
  </div>
  <script>
    /** 虚拟 DOM 构造函数 */
    class VNode {
      constructor( tag, data, value, type ) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = [];
      }

      appendChild ( vnode ) {
        this.children.push( vnode );
      }
    }
    /** 由 HTML DOM -> VNode: 将这个函数当做 compiler 函数 */
    function getVNode( node ) { 
      let nodeType = node.nodeType;
      let _vnode = null;
      if ( nodeType === 1 ) {
        // 元素
        let nodeName = node.nodeName;
        let attrs = node.attributes;
        let _attrObj = {};
        for ( let i = 0; i < attrs.length; i++ ) { // attrs[ i ] 属性节点 ( nodeType == 2 )
          _attrObj[ attrs[ i ].nodeName ] = attrs[ i ].nodeValue;
        }
        _vnode = new VNode( nodeName, _attrObj, undefined, nodeType );

        // 考虑 node 的子元素
        let childNodes = node.childNodes;
        for ( let i = 0; i < childNodes.length; i++ ) {
          _vnode.appendChild( getVNode( childNodes[ i ] ) ); // 递归
        }

      } else if ( nodeType === 3 ) {

        _vnode = new VNode( undefined, undefined, node.nodeValue, nodeType );
      }

      return _vnode;
    }

    /** 将虚拟 DOM 转换成真正的 DOM */
    function parseVNode( vnode ) {
      // 创建 真实的 DOM
      let type = vnode.type;
      let _node = null;
      if ( type === 3 ) {
        return document.createTextNode( vnode.value ); // 创建文本节点
      } else if ( type === 1 ) {

        _node = document.createElement( vnode.tag );

        // 属性
        let data = vnode.data; // 现在这个 data 是键值对
        Object.keys( data ).forEach( ( key ) => {
          let attrName = key;
          let attrValue = data[ key ];
          _node.setAttribute( attrName, attrValue );
        } );

        // 子元素
        let children = vnode.children;
        children.forEach( subvnode => {
          _node.appendChild( parseVNode( subvnode ) ); // 递归转换子元素 ( 虚拟 DOM )
        } );

        return _node;
      }

    }

    let rkuohao = /\{\{(.+?)\}\}/g;
    /** 根据路径 访问对象成员 */
    function getValueByPath( obj, path ) {
      let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
      let res = obj;
      let prop;
      while( prop = paths.shift() ) {
        res = res[ prop ];
      }
      return res;
    }
    /** 将 带有 坑的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
    function combine( vnode, data ) {
      let _type = vnode.type;
      let _data = vnode.data;
      let _value = vnode.value;
      let _tag = vnode.tag;
      let _children = vnode.children;


      let _vnode = null;

      if ( _type === 3 ) { // 文本节点 

        // 对文本处理
        _value = _value.replace( rkuohao, function ( _, g ) {
          return getValueByPath( data, g.trim() );
        } );

        _vnode = new VNode( _tag, _data, _value, _type )

      } else if ( _type === 1 ) { // 元素节点
        _vnode = new VNode( _tag, _data, _value, _type );
        _children.forEach( _subvnode => _vnode.appendChild( combine( _subvnode, data ) ) );
      }

      return _vnode;
    }


    function JGVue( options ) {
      this._data = options.data;
      let elm = document.querySelector( options.el ); // vue 是字符串, 这里是 DOM 
      this._template = elm;
      this._parent = elm.parentNode;

      this.mount(); // 挂载
    }  

    JGVue.prototype.mount = function () {
      // 需要提供一个 render 方法: 生成 虚拟 DOM
      this.render = this.createRenderFn() // 带有缓存 ( Vue 本身是可以带有 render 成员 )

      this.mountComponent();
    }
    JGVue.prototype.mountComponent = function () {
      // 执行 mountComponent() 函数 
      let mount = () => { // 这里是一个函数, 函数的 this 默认是全局对象 "函数调用模式"
        this.update( this.render() )
      }
      mount.call( this ); // 本质应该交给 watcher 来调用, 但是还没有讲到这里

      // 为什么
      // this.update( this.render() ); // 使用发布订阅模式. 渲染和计算的行为应该交给 watcher 来完成
    }

    /**
     * 在真正的 Vue 中使用了 二次提交的 设计结构
     * 1. 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
     * 2. 先 有 AST 和 数据 生成 VNode ( 新, render )
     * 3. 将 就的 VNode 和 新的 VNode 比较 ( diff ), 更新 ( update )
     */

    // 这里是生成 render 函数, 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
    JGVue.prototype.createRenderFn = function () {
      let ast = getVNode( this._template );
      // Vue: 将 AST + data => VNode
      // 我们: 带有坑的 VNode + data => 含有数据的 VNode
      return function render () {
        // 将 带有 坑的 VNode 转换为 待数据的 VNode
        let _tmp = combine( ast, this._data );
        return _tmp;
      }
    }

    // 将虚拟 DOM 渲染到页面中: diff 算法就在里
    JGVue.prototype.update = function ( vnode ) {
      // 简化, 直接生成 HTML DOM replaceChild 到页面中
      // 父元素.replaceChild( 新元素, 旧元素 )
      let realDOM = parseVNode( vnode );

      // debugger;
      // let _ = 0;

      this._parent.replaceChild( realDOM, document.querySelector( '#root' ) );
      // 这个算法是不负责任的: 
      // 每次会将页面中的 DOM 全部替换
    }


    

    let app = new JGVue( {
      el: '#root',
      data: {
        name: '张三'
        , age: 19
        , gender: '难'
      }
    } );

  </script>
</body>
</html>

03-vue源码学习-响应式原理(对象和数组)

一、响应式原理

什么是响应式原理:

意思就是在改变数据的时候,视图会跟着更新。

实现响应式原理的核心:Object.defineProperty

Object.defineProperty( 对象, '设置什么属性名', {
  writeable
  configurable
  enumerable:  控制属性是否可枚举, 是不是可以被 for-in 取出来
  set() {}  赋值触发
  get() {}  取值触发
} )

二、对象响应式化

    <script>
        let o = {
            name: 'jim',
            age: 19,
            gender: '男'
        }

        // 简化后的版本
        function defineReactive(target, key, value, enumerable) {
            // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
            Object.defineProperty(target, key, {
                configurable: true,
                enumerable: !!enumerable,
                get() {
                    console.log(`读取 o 的 ${key} 属性`); // 额外
                    return value
                },
                set(newVal) {
                    console.log(`设置 o 的 ${key} 属性为: ${newVal}`); // 额外
                    value = newVal
                }
            })
        }
        let keys = Object.keys(o)
        keys.forEach(key => {
            defineReactive(o, key, o[key], true)
        })

    </script>

三、对象数组响应式化

    <script>
        let data = {
            name: '张三',
            age: 19,
            course: [
                { name: '语文' },
                { name: '数学' },
                { name: '英语' },
            ]
        }
        // 简化后的数据响应函数
        function defineReactive(target, key, value, enumerable) {
            // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
            if (typeof value === 'object' && value != null && !Array.isArray(value)) {
                // 是非数组的引用类型
                reactify(value); // 递归
            }
            Object.defineProperty(target, key, {
                configurable: true,
                enumerable: !!enumerable,
                get() {
                    console.log(`读取 o 的 ${key} 属性`); // 额外
                    return value
                },
                set(newVal) {
                    console.log(`设置 o 的 ${key} 属性为: ${newVal}`); // 额外
                    value = newVal
                }
            })
        }

        // 将对象 o 响应式化
        function reactify(o) {
            keys = Object.keys(o)
            for (let i = 0; i < keys.length; i++) {
                let key = keys[i]
                let value = o[key]
                // 判断这个属性是不是引用类型, 判断是不是数组
                // 如果引用类型就需要递归, 如果不是就不用递归
                //  如果不是引用类型, 需要使用 defineReactive 将其变成响应式的
                //  如果是引用类型, 还是需要调用 defineReactive 将其变成响应式的
                // 如果是数组呢? 就需要循数组, 然后将数组里面的元素进行响应式化
                if (Array.isArray(value)) {
                    for (let j = 0; j < value.length; j++) {
                        reactify(value[j])
                    }
                } else {
                    // 对象或值类型
                    defineReactive(o, key, value, true);
                }
            }
        }
        reactify(data)

    </script>

四、拦截数组方法

在改变数组时,加入的元素也要响应式化,所以要对数组方法进行重写,采用的方式是拦截数组的方法。

拦截数组方法:

<script>
        let ARRAY_METHOD = [
            'push',
            'pop',
            'shift',
            'unshift',
            'reverse',
            'sort',
            'splice',
        ];

        // 思路, 原型式继承: 修改原型链的结构
        let arr = [];
        // 继承关系: arr -> Array.prototype -> Object.prototype -> ...
        // 继承关系: arr -> 改写的方法 -> Array.prototype -> Object.prototype -> ...
        let array_methods = Object.create(Array.prototype)
        ARRAY_METHOD.forEach(method => {
            array_methods[method] = function () {
                // 调用原来的方法
                console.log('调用的是拦截的 ' + method + ' 方法');
                // 将数据进行响应式化
                let res = Array.prototype[method].apply(this, arguments)
                return res
            }
        })

        arr.__proto__ = array_methods
    </script>

五、完整的对象和数组响应式化代码

  <script>


    let data = {
      name: '张三',
      age: 19,
      course: [
        { name: '语文' },
        { name: '数学' },
        { name: '英语' },
      ]
    }; // 除了递归还可以使用队列 ( 深度优先转换为广度优先 )



    let ARRAY_METHOD = [
      'push',
      'pop',
      'shift',
      'unshift',
      'reverse',
      'sort',
      'splice',
    ];
    let array_methods = Object.create(Array.prototype);
    ARRAY_METHOD.forEach(method => {
      array_methods[method] = function () {
        // 调用原来的方法
        console.log('调用的是拦截的 ' + method + ' 方法');

        // 将数据进行响应式化
        for (let i = 0; i < arguments.length; i++) {
          reactify(arguments[i]);
        }

        let res = Array.prototype[method].apply(this, arguments);
        // Array.prototype[ method ].call( this, ...arguments ); // 类比
        return res;
      }
    });

    // 简化后的版本
    function defineReactive(target, key, value, enumerable) {
      // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )

      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        // 是非数组的引用类型
        reactify(value); // 递归
      }

      Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,

        get() {
          console.log(`读取 ${key} 属性`); // 额外
          return value;
        },
        set(newVal) {
          console.log(`设置 ${key} 属性为: ${newVal}`); // 额外
          value = newVal;
        }
      });
    }
    // 将对象 o 响应式化
    function reactify(o) {
      let keys = Object.keys(o);

      for (let i = 0; i < keys.length; i++) {
        let key = keys[i]; // 属性名
        let value = o[key];
        if (Array.isArray(value)) {
          // 数组
          value.__proto__ = array_methods; // 数组就响应式了
          for (let j = 0; j < value.length; j++) {
            reactify(value[j]); // 递归
          }
        } else {
          // 对象或值类型
          defineReactive(o, key, value, true);
        }
      }
    }


    reactify(data);


  </script>

04-vue源码学习-proxy

proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

app._data.name
// vue 设计, 不希望访问 _ 开头的数据
// vue 中有一个潜规则:
//  - _ 开头的数据是私有数据
//  - $ 开头的是只读数据
app.name
// 将 对 _data.xxx 的访问 交给了 实例

// 重点: 访问 app 的 xxx 就是在访问 app._data.xxx

假设:

var  o1 = { name: '张三' };
// 要有一个对象 o2, 在访问 o2.name 的时候想要访问的是 o1.name
Object.defineProperty( o2, 'name', {
  get() {
    return o1.name
  }
} );

访问 app 的 xxx 就是在访问 app._data.xxx

Object.defineProperty( app, 'name', {
  get() {
    return app._data.name
  },
  set( newVal ) {
    app._data.name = newVal;
  }
} )

将属性的操作转换为 参数

function proxy( app, key ) {
  Object.defineProperty( app, key, {
    get() {
      return app._data[ key ]
    },
    set( newVal ) {
      app._data[ key ] = newVal;
    }
  } )
}

问题:

在 vue 中不仅仅是只有 data 属性, properties 等等 都会挂载到 Vue 实例上

function proxy( app, prop, key ) {
  Object.defineProperty( app, key, {
    get() {
      return app[ prop ][ key ]
    },
    set( newVal ) {
      app[ prop ][ key ] = newVal;
    }
  } )
};

// 如果将 _data 的成员映射到 实例上
proxy( 实例, '_data', 属性名 )
// 如果要 _properties 的成员映射到 实例上
proxy( 实例, '_properties', 属性名 )

05-vue源码学习-发布订阅模式

发布订阅模式
目标: 解耦, 让各个模块之间没有紧密的联系

现在的处理办法是 属性在更新的 时候 调用 mountComponent 方法.

问题: mountComponent 更新的是什么??? (现在) 全部的页面 -> 当前虚拟 DOM 对应的页面 DOM

在 Vue 中, 整个的更新是按照组件为单位进行 判断, 已节点为单位进行更新.

  • 如果代码中没有自定义组件, 那么在比较算法的时候, 我们会将全部的模板 对应的 虚拟 DOM 进行比较.
  • 如果代码中含有自定义组件, 那么在比较算法的时候, 就会判断更新的是哪一些组件中的属性, 只会判断更新数据的组件, 其他组件不会更新.

复杂的页面是有很多组件构成. 每一个属性要更新的都要调用 更新的方法?

目标, 如果修改了什么属性, 就尽可能只更新这些属性对应的页面 DOM

这样就一定不能将更新的代码写死.

例子: 预售可能一个东西没有现货, 告诉老板, 如果东西到了 就告诉我.

老板就是发布者
订阅什么东西作为中间媒介
我就是订阅者

使用代码的结构来描述:

  1. 老板提供一个 账簿( 数组 )
  2. 我可以根据需求订阅我的商品( 老板要记录下 谁 定了什么东西, 在数组中存储 某些东西 )
  3. 等待, 可以做其他的事情
  4. 当货品来到的时候, 老板就查看 账簿, 挨个的打电话 ( 遍历数组, 取出数组的元素来使用 )

实际上就是事件模型

  1. 有一个 event 对象
  2. on, off, emit 方法

实现事件模型, 思考怎么用?

  1. event 是一个全局对象
  2. event.on( ‘事件名’, 处理函数 ), 订阅事件
    1. 事件可以连续订阅
    2. 可以移除: event.off()
      1. 移除所有
      2. 移除某一个类型的事件
      3. 移除某一个类型的某一个处理函数
  3. 写别的代码
  4. event.emit( ‘事件名’, 参数 ), 先前注册的事件处理函数就会依次调用

原因:

  1. 描述发布订阅模式
  2. 后面会使用到事件

发布订阅模式 ( 形式不局限于函数, 形式可以是对象等 ) :

  1. 中间的全局的容器, 用来存储可以被触发的东西( 函数, 对象 )
  2. 需要一个方法, 可以往容器中传入东西 ( 函数, 对象 )
  3. 需要一个方法, 可以将容器中的东西取出来使用( 函数调用, 对象的方法调用 ) 事件模型代码
<script>
        // 全局的 event 对象, 提供 on, off, emit 方法  
        var event = (function () {
            eventObjs = {}
            return {
                /** 注册事件, 可以连续注册, 可以注册多个事件 */
                on: function (type, handler) {
                    (eventObjs[type] || (eventObjs[type] = [])).push(handler)
                },
                /** 移除事件, 
                 * - 如果没有参数, 移除所有事件, 
                 * - 如果只带有 事件名 参数, 就移除这个事件名下的所有事件,
                 * - 如果带有 两个 参数, 那么就是表示移除某一个事件的具体处理函数
                 * */
                off: function (type, handler) {
                    if (arguments.length === 0) {
                        eventObjs = {}
                    } else if (arguments.length === 1) {
                        eventObjs[type] = 1
                    } else if (arguments.length === 2) {
                        let _events = eventObjs[type]
                        if (!_events) return
                        for (let i = _events.length; i >= 0; i--) {
                            if (_events[i] === handler) {
                                _events.splice(i, 1)
                            }
                        }
                    }
                },
                /** 
                 * 发射事件, 触发事件, 包装参数 传递给事件处理函数
                */
                emit: function (type) {
                    let args = Array.prototype.splice.call(arguments, 1)// 或 arguments 从 1 开始后的所有参数, 返回的是数组
                    let _events = eventObjs[type]
                    if (!_events) return
                    for (let i = 0; i < _events.length; i++) {
                        _events[i].apply(null, args)
                    }
                }
            }
        }())
    </script>
    <script>
        function f() { console.log(1) }
        function foo() { console.log(2) }


        // 注册事件
        event.on('click', () => console.log('第一个 click 事件')); // 无法移除
        event.on('click', () => console.log('第2个 click 事件'));
        event.on('click', () => console.log('第3个 click 事件'));
        event.on('click', () => console.log('第4个 click 事件'));
        event.on('click', () => console.log('第5个 click 事件'));

        console.log(1);
        console.log(1);
        console.log(1);
        console.log(1);
        console.log(1);
        console.log(1);


        function f() {
            event.emit('click');
        }

        // js 中 基本类型是比较值
        // 引用类型是 比较 地址
        // 引用类型与基本类型, 是将其转换为 基本类型再比较 , 如果是 === 严格等于是不转换比较
    </script>```


06-vue源码学习-理解依赖收集和派发更新(Observer、Watcher、Dep)

转载至:彻底理解Vue中的Watcher、Observer、Dep

在数据响应化时,在getter方法中做依赖收集,在setter方法中做派发更新。dep用于存储依赖和派发更新。

思考以下代码

new Vue({
  el: '#example',
  data(){
      return{
          obj:{
              a:1
          }
      }
  },
})

当我们写下这行代码时,vue将我们在data内定义的obj对象进行依赖追踪.

具体做法为执行new Observer(obj)

//经过上面的代码,我们的obj对象会变为以下的样子
{
  obj:{
    a:1,
    __ob__:{ //Observer 实例
        dep:{Dep 实例
            subs:[ //存放 Watcher 实例
              new Watcher(),
              new Watcher(),
              new Watcher(),
              new Watcher(),
            ]
        }
    }
  }
}


我们来一步步实现看下。

在obj对象上新增__ob__属性,值为Observe 类的实例,我们编写一个 def 函数,用来增加属性

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

增加啥属性呢?之前提到了,我们需要增加一个 Observer 实例,实现如下

Observe 实现

const Dep = require('./Dep')

class Observer {
  constructor(targetObject) {
    def(targetObject, '__ob__', this);//在 targetObject 上 添加  Observer 实例, setter时 通知该实例
    this.walk(targetObject)
    this.dep = new Dep()
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    });
  }

}

function observe(data) {
  if (Object.prototype.toString.call(data) !== '[object Object]') {
    return
  }
  new Observer(data)
}

function defineReactive(obj, key, val) {
  observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get');
      const ob = this.__ob__
      ob.dep.depend();
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('set');
      if (newVal === val) return
      val = newVal
      observe(newVal)
      const ob = this.__ob__
      ob.dep.notify();
    },

  })
}

function def(obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}


module.exports = Observer

这里面牵扯到了 Dep,我们也把Dep实现下

Dep

class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  depend() {
    this.subs.push(Dep.target);
  }

  notify() {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].fn();
    }
  }
}

Dep.target = null;

module.exports=Dep

Observer 类 主要做了以下事情

遍历 data 下的每一个属性,若是对象,则 执行 new Observer() ,在对象上新增__ob__属性,该属性的值为 Observer 的实例
劫持对象属性的变化,在 getter 的时候,拿到 Observer 实例的dep实例,执行dep.depend(),代码如下

  const ob = this.__ob__
  ob.dep.depend();

看下 dep.depend()做了些啥

this.subs.push(Dep.target)

Dep.target添加到 订阅数组内(this.subs)

也就是说,只要我们 Dep.target 赋值了,再执行 dep.depend(),那么该值就会被添加到 dep 的 subs 数组内,比如

实现自动添加依赖
这个时候该 Watcher出场了

Watcher 实现

const Dep = require("./Dep");

class Watcher {
  constructor(vm, exp, fn) {
    this.vm = vm;
    this.exp = exp;
    this.fn = fn;
    Dep.target = this; //将自己挂载到 Dep.target,调用 Dep.depend时会读取该变量
    this.vm[exp];
  }

  update() {
    //加入队列
  }
}

module.exports = Watcher;


根据一个小例子来理解 Watcher

const obj = {
  a: 1,
  b: {
    c: 2
  }
}

new Observer(obj)
new Watcher(obj, 'a', () => {
  console.log('Watcher 回调执行')
})
obj.a='222'

流程如下:

  1. 先观测 obj 对象(new Observer(obj)
  2. 实例化Watcher时,会执行Dep.target = this,然后执行this.vm[exp],也就是取一次值,那么会触发
    getter,将自身(Watcher实例)添加到dep的订阅者数组内
  3. 最后,改变数据时候,触发setter
 set: function reactiveSetter(newVal) {
      if (newVal === val) return
      val = newVal
      observe(newVal)
      const ob = this.__ob__
      ob.dep.notify();
    },

执行ob.dep.notify()

 notify() {
    for (let i = 0; i < this.subs.length; i++) {
      this.subs[i].fn()
    }

遍历 订阅者(subs)执行回调函数,整个流程结束

07-vue源码学习-vue的响应式原理(MVVM原理)

什么是MVVM
MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
在这里插入图片描述
要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程:
在这里插入图片描述

1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;
2.实现Observe,监听所有的数据,并对变化数据发布通知;
3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。
4.结合上述方法,向外暴露mvvm方法。

最简化 VUE的响应式原理(转载)
转载:最简化 VUE的响应式原理
下图是vue官方的原理图:
总共分为三步骤:

1、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的时候,就是去通知执行刚刚注册的函数。

2、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                      // 注意这里将当前的Watcher赋值给了Dep.target
    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.blogTitle)
}

此时会去调用这个属性blogTitle的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里讲过,不记得可以往上第三段代码),这里做的事情就是给blogTitle注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了blogTitle,那么这个组件相对应的Watcher对象都会被注册到blogTitle的Dep中。

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

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

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

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

可以用一张图来表示:
在这里插入图片描述

由此图我们可以看出Watcher是连接VUE component 跟 data属性的桥梁。

08-自我总结的vue响应式原理

在这里插入图片描述

vue是采用数据劫持配合发布者-订阅者模式的方式,通过Object.definerProperty()来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器,去通过观察者,做出对应的回调函数,去更新试图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译指令,最终利用Watcher搭起Observer,Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果。

  • 14
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Cesium Vue码是一个开项目的代码,它结合了Cesium和Vue.js两个框架的优势,用于在Web上构建高性能的3D地球应用程序。 Cesium是一个强大的基于Web的地理空间可视化引擎,可以实现全球范围内的地图展示和3D场景的渲染。而Vue.js是一个流行的JavaScript框架,用于构建用户界面。通过将这两个框架结合在一起,Cesium Vue能够提供一个更灵活和易于使用的工具,用于开发各种各样的地球应用程序。 Cesium Vue码包含了各种组件和工具,用于与Cesium引擎进行交互和集成。例如,它提供了CesiumViewer组件,用于创建一个全功能的Cesium场景,并且能够与Vue组件进行交互。它还提供了各种用于加载地理数据、设置相机位置、渲染3D对象等功能的工具函数。 使用Cesium Vue码,开发人员可以方便地创建一个具有交互性和可视化效果的地球应用程序。他们可以轻松地创建地图、添加地标和矢量数据,实现相机控制、动画效果等功能。同时,由于使用了Vue.js框架,开发人员还能够轻松管理和更新应用程序的状态,并且可以方便地与其他Vue组件进行集成。 总的来说,Cesium Vue码提供了一个强大的工具,用于开发具有高性能和可交互性的3D地球应用程序。通过结合Cesium和Vue.js的优势,它能够为开发人员提供更好的开发体验和更多的灵活性。无论是构建地图应用程序、数据可视化还是虚拟现实应用程序,Cesium Vue都是一个非常有价值的工具。 ### 回答2: Cesium Vue码是一个基于Vue框架封装的对Cesium进行集成的库。Cesium是一个用于创建各种地理和地球空间应用的开JavaScript库,而Vue是一个用于构建用户界面的渐进式JavaScript框架。 Vue的优势在于其虚拟DOM和响应式数据绑定的特性,使得开发者可以更加高效地构建可复用的组件和灵活的界面。Cesium则提供了强大的地理可视化功能,包括3D地球的展示、地理信息的呈现和地图操作等。 Cesium Vue码的主要目的是将Cesium和Vue无缝集成,使得开发者可以更加便捷地在Vue项目中使用Cesium的功能。通过Cesium Vue,我们可以轻松地将Cesium的3D地球嵌入到Vue的组件中,使用Vue的数据绑定和组件化技术来管理Cesium的状态和交互。 Cesium Vue码实现了Cesium和Vue之间的双向数据绑定,使得我们可以通过Vue的数据驱动Cesium的展示和交互。同时,Cesium Vue提供了一系列的Vue组件,用于简化和封装Cesium的常用功能,比如地图控制、实体渲染和相机操作等。 通过Cesium Vue,开发者可以利用Vue的生态系统和丰富的插件,以及Cesium的地理可视化功能,快速构建高质量的地理和地球空间应用。无论是创建交互式地图应用,还是构建具有地球模型的数据可视化工具,Cesium Vue都提供了简洁、灵活和高效的开发方式。 总之,Cesium Vue码是一个基于Vue框架封装的Cesium集成库,通过它我们可以更加便捷地在Vue项目中使用Cesium的功能,享受Vue的开发便捷性和Cesium的地理可视化能力。 ### 回答3: Cesium-Vue码是一个基于Vue.js框架开发的Cesium地球引擎的封装库,用于在Vue项目中使用Cesium进行地球可视化开发。该库主要提供了一系列在Vue组件中使用Cesium功能的API和组件。 码的结构主要包括了基于Vue的组件目录、样式文件目录和存放Cesium相关代码的目录。在组件目录中,可以看到各个Vue组件用于实现不同的地球可视化功能,如地图视图、图层控制、导航控制等。样式文件目录主要包括了用于美化地球视图的CSS样式。 Cesium-Vue码的主要功能包括: 1. 地图视图:通过Vue组件提供的功能,可以在项目中创建一个地球视图,并进行交互操作,如移动、旋转等。 2. 图层控制:该库提供了图层控制的API和组件,使得开发者可以方便地添加不同类型的图层,如地形图、影像图等,并进行切换和控制。 3. 数据可视化:通过Cesium的API和Vue的数据绑定机制,可以实现将数据可视化展示在地球上,如点、线、面等实体的展示和交互。 4. 导航控制:Cesium-Vue提供了一系列的API和组件,用于实现地球视图的导航控制,如缩放、旋转、定位等操作。 总体而言,Cesium-Vue码的设计和实现通过将Cesium地球引擎与Vue.js框架相结合,提供了一种便捷的方式在Vue项目中开发地球可视化应用。通过使用该库,可以方便地在Vue项目中创建地球视图、添加图层、展示数据,并实现交互和导航控制等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值