精华内容
下载资源
问答
  • Vue源码笔记

    2021-04-10 21:22:52
    Vue 源码笔记 使用 import Vue from 'vue'; new Vue({ render: h => h(component), // router }).$mount('#app'); Vue 实例 initData function initData { let data = vm.$options.data // 组件复用,不为...

    Vue 源码笔记

    使用

    import Vue from 'vue';
    
    new Vue({
      render: h => h(component),
      // router
    }).$mount('#app');
    

    Vue 实例

    在这里插入图片描述

    initData

    function initData {
      let data = vm.$options.data
      // 组件复用,不为函数,实例保持同一个对象的引用,导致数据污染
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)   // data.call(vm, vm)
        : data || {}
      
      // 数据响应式处理
      observe(data, true /* asRootData */)  
    }
    

    initComputed

    在这里插入图片描述

    function initComputed () {
      const watchers = vm._computedWatchers = Object.create(null)
      // 每个 computed 创建一个 watcher
      watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            { lazy: true }  // this.dirty = this.lazy = true
       )
      defineComputed()  
    }
    
    function  defineComputed () {
      const shouldCache = !isServerRendering()  
      sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : createGetterInvoker(userDef)
        sharedPropertyDefinition.set = noop  
      
      // 拦截 get  this.xxx 触发
      Object.defineProperty(target, key, sharedPropertyDefinition)  
    }
    
    function createComputedGetter () {
       return function computedGetter () {
         const watcher = this._computedWatchers && this._computedWatchers[key]
         // true, 计算属性需要重新计算
         if (watcher.dirty) {
            watcher.evaluate()
            // this.get-> pushTarget(computed-watcher) 当前 Dep.target 指向 computed-watcher
            // -> this.getter.call(vm, vm) 即 this.xxx 触发 data.get data-dep 收集到 computed-watcher, computed-watcher 收集 data-dep
            // popTarget() 当前 Dep.target 指向 render-watcher
            
            // this.dirty = false  
            // data set, data dep 中 watcher update 时 this.dirty = true 重新计算 
          }
           
          if (Dep.target) {
            // computer dep 收集到 render-watcher
            // render-watcher 不收重复 dep  
            watcher.depend()
          }
          return watcher.value  
       } 
    } 
    

    computed 、watch 对比

    • computed 具有缓存,默认只有 getter
    • watch 可以定义函数

    KeepAlive

    // 挂载组件 KeepAlive
    extend(Vue.options.components, builtInComponents)
    
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      // make current key freshest
       remove(keys, key)
       keys.push(key)
    } else {
       cache[key] = vnode
       eys.push(key)
       // prune oldest entry
       if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
    }
    

    path 中 Diff

    vue 中 Vue.prototype.patch 实现

    diff 只会同层级进行,不会跨层级比较

    在这里插入图片描述

    // 新旧节点对比
    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        // 
        let oldStartIdx = 0
        let newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newEndIdx = newCh.length - 1
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
        // removeOnly is a special flag used only by <transition-group>
        // to ensure removed elements stay in correct relative positions
        // during leaving transitions
        const canMove = !removeOnly
    
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(newCh)
        }
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
          } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            //排序  
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            // 排序   
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              // 具有相同节点  
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // 对比移动节点  
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                oldCh[idxInOld] = undefined
                // 插入节点
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
    
        
        if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }
    

    设置 key 和不设置 key 的区别

    • 不设置 key,新旧节点头尾两端相互比较
    • 设置 key,除了头尾两端比较外,还会使用 key 生成的对象 createKeyToOldIdx 中查找匹配的阶段,设置 key 可以高效利用 dom
    function createKeyToOldIdx (children, beginIdx, endIdx) {
      let i, key
      const map = {}
      for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
      }
      return map
    }
    

    $nextTick

    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    // 事件循环,宏任务、微任务,栗子 for(let i) { setTimeout(()=>{ console.log(i) }, 0) }
     timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
      }
     timerFunc = () => {
        setImmediate(flushCallbacks)
     }
     timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
     
     function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
     
    
    展开全文
  • vue源码笔记(1)

    2020-05-22 19:20:39
    Vue源码解析 前言 Vue 数据和视图 UI = render(state) state:状态,包括用户操作和数据变化 UI:页面视图 render:vue扮演得就是这个角色,在state变化之后经过一系列得加工处理,将变化反应在视图UI上。 object...

    Vue源码解析(1)

    主要是学习https://vue-js.com/learn-vue/ 这个vue源码解读的内容。自己写的笔记。文中配图是中文社区中的这个大佬的。

    前言

    Vue 数据和视图

    UI = render(state)

    state:状态,包括用户操作和数据变化

    UI:页面视图

    render:vue扮演得就是这个角色,在state变化之后经过一系列得加工处理,将变化反应在视图UI上。

    object数据变化监测

    数据的变化会引起视图得改变,所以实时监测数据得变化,就是核心的功能

    src/core/observer/index.js

    export class observer{
        constructor (value) {
            this.value = value;
            def(value,'_ob_',this);//给value添加新的—_ob_属性,值为当前的实例
            if(Array.isArray(value)){
                //数组
                if(hasProto) {
                    //判断_proto_是否可用,因为有的浏览器不支持,所以对于是否支持就要使用两种方法去处理数组
                    protoAugment(value, arrayMethods);
                }else {
                    copyAugment(value, arrayMethods, arrayKeys);
                }
            }else{
                this.walk(value);
            }
        }
        //walk方法,遍历对象的所以属性,并将每个属性转换为getter/setters
         walk (obj: Object) {
             const keys = Object.keys(obj);
             for (let i=0; i<keys.length; i++) {
                 defineReactive(obj, keys[i]);
             }
         }
    }
    
    export function defineReactive(obj, key, val ..) {
        ...val=obj[key]
        const dep = new Dep() //实例化一个依赖管理器
        Object.defineProperty(obj,key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter() {
                const value = getter ? getter.call(obj) : val;
                dep.depend();//getter里面向管理依赖的数组里添加依赖
                return value;
            },
            set: function reactiveSetter(newVal) {
                const value = getter ? getter.call(obj) : val;
                if(value == newVal) return;
                value = newVal;
                dep.notify();//setter里面通知
            }
        })
    }
    1.Data通过observer转换成了getter/setter的形式来追踪变化。
    2.当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到dep依赖中,dep.depend()3.当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知,dep.notify()4.Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
    

    简单总结一下就是:Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。为了便于理解,我们画出了其关系流程图,如下图:
    img
    使用defineProperty方法只是可以获取和修改一个值,但是如果要新增加/删除键值对的时候,需要使用Vue.setVue.delete这种全局的api

    Array数据变化监测

    数组和对象使用的方法不一样,因为Object.defineProperty()是js原型里面对象才可以使用的方法,数组用不了

    虽然和上面的数据类型不一样,但是万变不离其宗: 获取数据时收集依赖,数据变化时通知依赖更新 。(dep类的对象,用来收集watcher对象,读数据的时候,会触发getter函数把当前的watcher对象(存放在Dep.target中)收集到Dep类中,写数据的时候,则会触发setter,通知Dep类调用notyfy来触发所有的watcher对象的update方法更新对应视图)

    对于数组,data: { arr:[ ] }

    数组就是包含在data这个对象里面,所以getter是可以使用的,但是数组缺少了setter。所以在vue的源码中就是新写了array的setter。在新写的setter里面,保留了数组原生的数组处理方法,同时加上了dep.notify方法去触发watcher更新视图。

    src/observer/array.js 里面写的 setter

    const arrayProto = Array.prototype;
    export const arrayMethods = Object.create(arrayProto);
    const methodsToPath = ['push','pop','shift','unshift','splice','sort','reverse'];
    methodsToPath.forEach(function (method) {
        const original = arrayProto[method];//array原方法
        def(arrayMethods, method, function mutator (...args) {
            const result  = original.apply(this, args);
            const ob = this._ob_;
            let inserted;//inserted用来接收插入的新元素
            switch (method) {  //对数组插入新元素的三个方法
                case 'push':
                case 'unshift':
                    inserted = args;break;
                case 'splice': inserted = args.slice(2);break;//splice(index.howmany,item1..itemN),所以对args取slice(2)才能正确取到插入的元素
            }
            //对插入的新元素一样都要转换为可检测的
            if(inserted) ob.observerArray(inserted);
            ob.dep,notify();//通知依赖,更新
            return result;
        })
    })
    

    同Object一样,也有不足之处,比如arr[0]=2;这样的不太好操作,还是需要使用Vue.set和Vue.delete这样的全局api

    Virtual DOM

    1,vdom:就是使用js对象描述一个dom节点

    2,为什么需要使用vdom:因为直接操作dom节点非常消耗性能,但是数据更新之后视图必须是相应变化的,所以要尽可能减少dom操作。使用vdom就是 使用JS的计算性能来换取操作真实DOM所消耗的性能 ,并可使用dom-diff算法更新视图。

    Vue-vdom实例 src/core/vdom/vnode.js

    export default VNode {
        constructor(){
            //在构造函数里,描述真实节点需要的一系列属性
            //通过对属性的选择搭配,可以创建不同类型的VNode
        }
    }
    

    VNode分类:元素节点 组件节点 函数式组件节点 克隆节点 文本节点 注释节点 (这几类节点本质上都是VNode实例,只是属性是不一样, 而实际上只有加粗的3种类型的节点能够被创建并插入到DOM中 )

    我们在vue文件template里面写的后期会编译成VNode,创建成真实DOM渲染到页面上。数据更新后,会生成新的VNode,与旧的VNode比较,找出差异处,再进行更新页面。寻找差异就是下面介绍的diff算法。

    DOM-Diff算法

    在vue中,DOM-Diff的过程叫做patch,就是对oldVNode进行修补,得到newVNode;主要以newVNode为基准,对比oldVNode,在oldVNode上进行增、删、改,使得old变得和new的一样。

    所以patch中就是三件事:创建节点 删除节点 更新节点

    src/core/vdom/patch.js

    • 创建节点

      根据节点类型(元素节点、文本节点、注释节点),分别创建-插入

      img

    • 删除节点

      要删除节点的父元素上调用removeChild方法即可

    • 更新节点

      img

    子节点更新策略

    Vue为了避免双重循环数据量大时间复杂度升高带来的性能问题,而选择了从子节点数组中的4个特殊位置互相比对,分别是:新前与旧前,新后与旧后,新后与旧前,新前与旧后 。上面四种位置都不行的时候还是会采取基础的双循环策略。

    src/core/vdom/patch.js ---- updateChildren()

    模板编译

    把用户所写的模板转化成供Vue实例在挂载时可调用的render函数

    Vue会把用户在标签中写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生HTML找出来,经过一系列的逻辑处理生成渲染函数,也就是render函数,而render函数会将模板内容生成对应的VNode,而VNode再经过前几篇文章介绍的patch过程从而得到将要渲染的视图中的VNode,最后根据VNode创建真实的DOM节点并插入到视图中, 最终完成视图的渲染更新。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaLCopsT-1590145744222)(https://vue-js.com/learn-vue/assets/img/1.f0570125.png)]

    编译的过程分为三步:

    1. 解析:使用解析器,讲模板内容解析为AST
    2. 优化:遍历AST,将静态节点加上标记
    3. 生成:使用代码生成器将AST转换为渲染函数

    解析阶段

    ​ parser解析器里面,首先使用parseHTML对HTML进行解析,遇到动态文本内容,使用parseText(主要用于提取文本中的变量)解析;遇到过滤器使用parseFilters进行解析。

    parseHTML:src/compiler/parser/index.js + html-parser.js

    parseText:src/compiler/parser/text-parser.js

    优化阶段

    主要是对静态节点进行标记,因为静态节点一旦渲染就不会再变化,就 在patch过程中不用去对比这些静态节点了

    src/compiler/optimizer.js

    1. AST中找出所有静态节点并打上标记(静态节点的子节点也必须全部是静态的);
    2. AST中找出所有静态根节点并打上标记;(要成为静态根节点必须同时满足几个条件:节点本身是静态节点,节点要有子节点,子节点不能是只要一个纯文本节点)

    代码生成阶段

    生成render函数字符串(虚拟dom)。

    源码:获取ast, 根据当前ast元素节点属性执行不同的代码生成不同的节点,虽然元素节点属性的情况有很多种,但是最后真正创建出来的VNode无非就三种,分别是元素节点,文本节点,注释节点

    src/compiler/codegen/index.js – genNode() genElement()

    生命周期

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfIDnTJf-1590145744225)(https://vue-js.com/learn-vue/assets/img/1.6e1e57be.jpg)]

    new Vue() 初始化阶段

    vm = new Vue()实际上调用Vue类,在Vue的构造函数中,执行_init: 合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。

    src/core/instance/index.js
    src/core/instance/init.js
    export function initMixin () {
    	Vue.prototype._init = function (options) {
            //1.属性合并
            vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm)
            //2.初始化
            initLifecycle(vm)
            initEvents(vm)
            initRender(vm)
            callHook(vm, 'beforeCreate')
            initInjections(vm)
            initState(vm)
            initProvide(vm)
            callHook(vm, 'created')
            //3判断el是否存在,存在就调用$mount.否则可以手动调用
            if (vm.$options.el) vm.$mount(vm.$options.el)
        }
        }
    }
    
    initLifecycle

    给实例初始化了一些属性 以 $ 或者 _ 开头的属性。比如$children $parent 这些。主要重点是源码中对祖先parent的查找。src/core/instance/lifecycle.js

    initEvents

    模板解析的时候,处理v-on/@事件,又分为自定义事件和浏览器原生事件。(浏览器原生事件带.native);解析后生成render函数,在挂载阶段生成vdom。

    如果挂载的节点是组件节点,就使用 src/core/vdom/create-component.js createComponent()方法,返回一个组件节点。

    const listeners = data.on
    data.on = data.nativeOn
    const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        { Ctor, propsData, listeners, tag, children },
        asyncFactory
      )
    
    //下面是vdom/vnode.js里面VNode的控制器接收参数
    constructor (
        tag?: string,
        data?: VNodeData,
        children?: ?Array<VNode>,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
      ) 
    
    //listeners被传递到了组子组件里面进行处理去了
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
    

    将自定义事件,data.on赋值给listeners,将data.nativeOn赋值给data.on;然后返回VNode,data作为当前节点的data,listeners放在componentOptions里面接收。 父组件既可以给子组件上绑定自定义事件,也可以绑定浏览器原生事件。这两种事件有着不同的处理时机,浏览器原生事件是由父组件处理,而自定义事件是在子组件初始化的时候由父组件传给子组件,再由子组件注册到实例的事件系统中。 所以initEvents是处理自定义事件的

    src\core\vdom\helpers\update-listeners.js --updateListeners()

    //On:listeners
    //oldOn:oldlisteners
    新的listeners和oldlisteners对比,新的里面有,旧的没有的,使用add;旧的里面有,新的没有的,使用remove
    
    initInject

    provide 和 inject 是一对;一个负责提供,一个负责注入。他们绑定的数据都不是响应式的!!!!

    parent = {
        provide: {foo:'bar'}
    }
    child = {
        inject: ['foo'] //this.foo = 'bar'
    }
    

    src/core/instance/inject.js

    export function initInjections (vm: Component) {
      //resolveInjection将实例上的inject转换为键值对的形式
      const result = resolveInject(vm.$options.inject, vm)
      if (result) {
        //关闭可检测,因为inject和provide数据不是响应式的数据。这里我们只是想将数据绑定再vm实例上而已。
        toggleObserving(false)
        Object.keys(result).forEach(key => {
            defineReactive(vm, key, result[key])
        })
        toggleObserving(true)
      }
    }
    

    关注resolveInjection函数,是将inject(此时的inject是统一规范格式之后的)和provide一起,解析成 键值对的形式。再此之前对于用户定义的inject,写法不一,首先应该将inject统一规范化。

    inject: {
    	foo: {
    		from: 'foo',
    		default: 'xx' //可有可无
    	}
    }
    
    initState

    使用Vue的时候,我们写的props,data,methods,computed ,watch,都属于State

    export function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      //下面对集中state进行初始化的顺序不能乱。因为后面的有使用到前面的,所以前面的要先初始化
      if (opts.props) initProps(vm, opts.props)  //props
      if (opts.methods) initMethods(vm, opts.methods) //methods
      if (opts.data) {                             //data
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */) //将空对象转为响应式
      }
      if (opts.computed) initComputed(vm, opts.computed)   //computed
      if (opts.watch && opts.watch !== nativeWatch) {   //watch
        initWatch(vm, opts.watch)
      }
    }
    
    initProps:

    首先,该数据用规范化inject的方法,也进行规范化。

    props:{
        name:{
            type: xxType
        }
    }
    

    src/core/instance/state.js – ininprops

    获取propsData,遍历里面的key和val,然后将键和值通过defineReactive函数添加到props(即vm.props)中,最后判断当前的key在vm实例是不是已经存在了,不存在就调用proxy函数在vm上设置一个以key为属性的代码,当使用vm[key]访问数据时,其实访问的是vm.props[key]

    function initProps (vm: Component, propsOptions: Object) {
      //取到props数据
      const propsData = vm.$options.propsData || {}
      const props = vm._props = {}  //所有设置到props变量中的属性都会保存到vm._props中
      // cache prop keys so that future props updates can iterate using Array
      // instead of dynamic object key enumeration.
      const keys = vm.$options._propKeys = []  //指向vm.$options._propKeys的指针,用于缓存keys,更新的时候遍历它就可以
      const isRoot = !vm.$parent
      // root instance props should be converted
      if (!isRoot) {
        //不是根组件,就不用将props数据转换为响应式数据
        toggleObserving(false)
      }
      for (const key in propsOptions) {
        //遍历props的真实数据,将每个key推入vm.$options._propKeys中
        keys.push(key)
        //根据key获取value  validateProp方法需要关注!
        const value = validateProp(key, propsOptions, propsData, vm)
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          const hyphenatedKey = hyphenate(key)
          if (isReservedAttribute(hyphenatedKey) ||
              config.isReservedAttr(hyphenatedKey)) {
            warn(
              `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
              vm
            )
          }
          defineReactive(props, key, value, () => {
            if (!isRoot && !isUpdatingChildComponent) {
              //warn
            }
          })
        } else {
          //然后将键和值通过defineReactive函数添加到props(即vm._props)中
          defineReactive(props, key, value)
        }
        // 判断当前这个key是不是已经在vm上存在了
        // 不存在,调用proxy函数在vm上设置一个以key为属性的代码,当使用vm[key]访问数据时,其实访问的是vm._props[key]
        if (!(key in vm)) {
          proxy(vm, `_props`, key)
        }
      }
      toggleObserving(true)
    }
    

    里面有一个验证数据的validateProp方法:校验数据的类型,数据是否填写正确,具体可思考下表单填写的时候用的比如说required,还有自定义的校验函数…

    initMethods
    function initMethods (vm: Component, methods: Object) {
      //ff里面一定会使用到之前定义的props数据
      const props = vm.$options.props
      for (const key in methods) {
        //key就是methods里面每个方法的命名  methods[key]是方法名对应的方法体
        if (process.env.NODE_ENV !== 'production') {
          if (typeof methods[key] !== 'function') {
            //methods必须是function
            //warn
          }
          //是function  下面检查methods的命名是不是和props,是不是和vue实例有重复,有冲突
          if (props && hasOwn(props, key)) {
            //warn
          }
          if ((key in vm) && isReserved(key)) {
            //warn
          }
        }
        //最后,该方法实体是函数的话,就绑定在vm实例上!
        vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
      }
    }
    
    initData
    function initData (vm: Component) {
      let data = vm.$options.data
      //data的数据保存在vm._data里。首先判断data[type],是function就调用getData方法获取data,不是就直接取data.
      //(new vue的时候,data:{}这样写的;但是在组件中,data(){return {}},data是函数)
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      //判断获取到的data是不是对象,因为data应该是对象
      if (!isPlainObject(data)) {
        data = {}
        //warn
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      //遍历data里面的数据~~判断有没有和methods,prop,vue命名上有冲突。
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            //warn
          }
        }
        if (props && hasOwn(props, key)) {
          //warn
        } else if (!isReserved(key)) {
          // 调用proxy函数将data对象中key不以_或$开头的属性代理到实例vm._data上,这样,我们就可以通过this.xxx来访问data选项中的xxx数据了
          proxy(vm, `_data`, key)   //initProps里面也用到这个方法
        }
      }
      // observe data
      observe(data, true /* asRootData */)  //将数据转为响应式,data里面的数据是响应式的,但是prop非根数据不是
    }
    
    initComputed

    初始化函数就是将computed的属性绑定在vm上。添加computed(lazy watcher)到watcher中

    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR(服务器渲染)
      const isSSR = isServerRendering()  //是否是服务端渲染,boolen
      //遍历computed的属性,将对应的属性值记为userDef,然后获取getter,只有非ssr时候计算属性有缓存功能
      for (const key in computed) {
        const userDef = computed[key]
        //userDef是function,该函数默认为取值器getter;如果不是函数,则说明是一个对象,则取对象中的get属性作为取值器赋给变量getter
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
          //warn
        }
        //判断是不是服务端渲染,不是的话,创建一个watcher实例
        if (!isSSR) {
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
          )
        }
        //判断当前循环到的的属性名是否存在于当前实例vm上,已经存在就按照相应判断抛出错误警告。
        if (!(key in vm)) {
          //不存在就,在vm实例上设置这个计算属性,所以重点关注的是这个定义计算属性的方法defineComputed
          defineComputed(vm, key, userDef)
        } else if (process.env.NODE_ENV !== 'production') {
          //computed的命名是否符合规范,不能和prop和data等冲突
          if (key in vm.$data) {
            warn(`The computed property "${key}" is already defined in data.`, vm)
          } else if (vm.$options.props && key in vm.$options.props) {
            warn(`The computed property "${key}" is already defined as a prop.`, vm)
          }
        }
      }
    }
    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      const shouldCache = !isServerRendering()  //只有是非ssr得时候,进行缓存。
      //根据userDef是不是function进行两种设置
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key) //需要缓存时,createComputedGetter是创造具有缓存功能的get
          : createGetterInvoker(userDef) //不需要缓存时,直接使用userDef作为getter,和之前initComputed里面逻辑一样
        sharedPropertyDefinition.set = noop  //没有set
      } else {
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : createGetterInvoker(userDef.get)
          : noop
        sharedPropertyDefinition.set = userDef.set || noop
      }
      if (process.env.NODE_ENV !== 'production' &&
          sharedPropertyDefinition.set === noop) {
            //这是为了在没有设置set的情况下,给用户设置默认的set,用来抛出警告,防止用户修改了属性。
        sharedPropertyDefinition.set = function () {
            //warn
          )
        }
      }
      //object的原生方法,将key绑定在target上。这里target就是vm实例 通过vm[key]访问computed属性值
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    //创建一个具有缓存属性的getter
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          //调用watcher上的depend和evaluate --> core\observer\watcher.js
          //watch.dirty是boolen,用来标志计算属性依赖的数据是否发生变化,变化了的话就是true.初始化的时候dirty=true,因此初始化的时候一定会执行evaluate
          if (watcher.dirty) {
            watcher.evaluate()  //evaluate源码中,dirty为true,就调用计算属性的get获取新val,并返回这个数据watcher.value
            //在使用watcher.evaluate的时候,会调用计算属性的get方法,在这个方法里面不仅是返回最后的结果,还调用pusgTarget,设置了Dep.target
            /* export function pushTarget (target: ?Watcher) {
              targetStack.push(target)
              Dep.target = target
            } */
          }
          //从上面的分析可以知道,Dep.target在get被调用的时候就会存在,所以这个if一定会进去
          if (Dep.target) {
            watcher.depend()  //执行watcher.depend(),将计算属性的watcher添加到依赖中去,之后computed的数据变化,就通过watcher通知使用计算属性的视图重新渲染,watcher.update()里面写了,如果是计算属性lazy watcher ,this.dirty=true;
          }
          return watcher.value
        }
      }
    }
    
    initWatch
    function initWatch (vm: Component, watch: Object) {
      //逻辑比较简单,遍历watch,调用createWatcher
      for (const key in watch) {
        const handler = watch[key]
        if (Array.isArray(handler)) {
          for (let i = 0; i < handler.length; i++) {
            createWatcher(vm, key, handler[i])
          }
        } else {
          createWatcher(vm, key, handler)
        }
      }
    }
    
    function createWatcher (
      vm: Component,
      expOrFn: string | Function,//被侦听的属性表达式
      handler: any,
      options?: Object
    ) {
      //watch的写法很多种,
      /**
       * watch: {
            c: {
              handler: function (val, oldVal) { //... },
              deep: true
            }
         }
       */
      if (isPlainObject(handler)) {
        options = handler  //c对象
        handler = handler.handler  //c.handler才是真的handler
      }
      /**
       * watch: {
            // methods选项中的方法名
            b: 'someMethod',
        }
       */
      if (typeof handler === 'string') {
        //上面的这种写法,vm[someMethod]就是handler。初始化methods在watch之前,所以可以取到
        handler = vm[handler]
      }
      //如果不是两种写法,那么写法就是写的一个函数,直接使用就行了,不用专门处理
      /*
        a: function (val, oldVal) {
          console.log('new: %s, old: %s', val, oldVal)
        }
      */
      //最后使用vm.$watch初始化watch
      return vm.$watch(expOrFn, handler, options)
    }
    

    最后,props,methods,data,computed,watch都会被绑定在vm实例上,可以使用this.xx方法,注意的是:不要重名。

    模板编译阶段

    该阶段所做的主要工作是获取到用户传入的模板内容并将其编译成渲染函数。 在vue.js完整版中包含编译器,但是在其他版本里面runtime,是不含编译器的,比如在使用webpack的时候,我们还会安装vue-loader,就是使用此插件完成编译。

    vue.js里面关于$mount的源码

      var mount = Vue.prototype.$mount;//缓存的mount变量就是只包含运行时版本的$mount方法。在该代码之前,还有一个Vue.prototype.$mount的定义
      Vue.prototype.$mount = function (el,hydrating) {
        el = el && query(el);
        /* istanbul ignore if */
        //el不能是body/html标签元素,这样会破坏文档结构
        if (el === document.body || el === document.documentElement) {
          //warn
          return this
        }
    
        var options = this.$options;
        // resolve template/el and convert to render function
        //如果用户没有自定义render函数
        if (!options.render) {
          var template = options.template;//获取用户定义的template
          if (template) {
            if (typeof template === 'string') {
              if (template.charAt(0) === '#') {  //template是id选择符开头,就获取id选择的内容
                template = idToTemplate(template);//就获取id选择的内容
                /* istanbul ignore if */
                if (!template) {
                  //warn
                }
              }
            } else if (template.nodeType) {   //如果template是节点类型,取innerHTML内容
              template = template.innerHTML;
            } else {  //否则,这个传入的template是无效的
              {
                warn('invalid template option:' + template, this);
              }
              return this
            }
          } else if (el) {
            //传入的template不存在的时候,就寻找外部的template
            template = getOuterHTML(el);
          }
          //经过上面操作,template已经取得
          if (template) {
            /* istanbul ignore if */
            if (config.performance && mark) {
              mark('compile');
            }
            //具体原理见src/compiler,经过三个阶段,生成render
            var ref = compileToFunctions(template, {
              outputSourceRange: "development" !== 'production',
              shouldDecodeNewlines: shouldDecodeNewlines,
              shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
              delimiters: options.delimiters,
              comments: options.comments
            }, this);
            var render = ref.render;
            var staticRenderFns = ref.staticRenderFns;
            options.render = render;  //render就是渲染函数,我们把它设置在$options里面
            options.staticRenderFns = staticRenderFns;
          }
        }
        return mount.call(this, el, hydrating)
      };
    

    总结: 从用户传入的el选项和template选项中获取到用户传入的内部或外部模板,然后将获取到的模板编译成渲染函数render。

    挂载阶段

    这部分的工作:第一部分是将模板渲染到视图上,第二部分是开启对模板中数据(状态)的监控。两部分工作都完成以后挂载阶段才算真正的完成了。src\core\instance\lifecycle.js

    function mountComponent (vm,el,hydrating) {
        vm.$el = el;
        if (!vm.$options.render) {
          //在编译阶段,应该会生成render放在vm.$options.render上,没有获取到可用的,就生成空节点放上面
          vm.$options.render = createEmptyVNode;
          {
            /* istanbul ignore if */
            if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
              vm.$options.el || el) {
              //warn
            } else {
              //warn
            }
          }
        }
        callHook(vm, 'beforeMount');  //触发beforeMount 该钩子函数触发后标志着正式开始执行挂载操作。
    
        var updateComponent;
        /* istanbul ignore if */
        if (config.performance && mark) {
          //...
        } else {
          updateComponent = function () {
            vm._update(vm._render(), hydrating);
            //首先执行渲染函数vm._render()得到一份最新的VNode节点树,然后执行vm._update()方法对最新的VNode节点树与上一次渲染的
            //旧VNode节点树进行对比并更新DOM节点(即patch操作),完成一次渲染。
            //这个完成之后,就会将最新的模板内容渲染到视图页面中。
          };
        }
    
        //Watcher类构造函数的第二个参数支持两种类型:函数和数据路径(如a.b.c)。
        //如果是数据路径,会根据路径去读取这个数据;如果是函数,会执行这个函数。
        //现在我们可知,第二个参数是updateComponent方法,所以这个方法会被执行,
        //就会触发数据或者函数内数据的getter方法,而在getter方法中会将watcher实例,将updateComponent中的所有数据
        //添加到该数据的依赖列表中(dep.depend),当数据发生变化时就会通知(dep.notify)依赖列表中所有的依赖,
        //依赖接收到通知后就会调用第四个参数回调函数去更新视图。直到实例被销毁
        new Watcher(vm, updateComponent, noop, {
          before: function before () {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate');   //触发beforeUpdate
            }
          }
        }, true /* isRenderWatcher */);
        hydrating = false;
    
        if (vm.$vnode == null) {
          vm._isMounted = true;
          callHook(vm, 'mounted');   //触发mounted  标志着挂载完成的生命周期钩子函数
        }
        return vm
      }
    

    销毁阶段

    在该阶段所做的主要工作是将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。src\core\instance\lifecycle.js

    Vue.prototype.$destroy = function () {
        const vm: Component = this   //获取当前实例
        if (vm._isBeingDestroyed) {//实例是否处于正在被销毁的状态
          return
        }
        callHook(vm, 'beforeDestroy')  //触发钩子函数,开始销毁
        vm._isBeingDestroyed = true
        // remove self from parent
        const parent = vm.$parent   //取当前实例的父级
        if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
          //父级存在,且父级没有在销毁的过程中,就把当前实例从父级移除
          remove(parent.$children, vm)
        }
        //接触父子关系之后,将自己身上的依赖追踪和事件监听移除。
        if (vm._watcher) {
          vm._watcher.teardown() //teardown:从所有依赖向的Dep列表中将自己删除.
        }
        let i = vm._watchers.length
        while (i--) {
          vm._watchers[i].teardown()
        }
        if (vm._data.__ob__) {
          vm._data.__ob__.vmCount--
        }
        // call the last hook...
        vm._isDestroyed = true   //销毁标志
        vm.__patch__(vm._vnode, null)  //vnode置为null
        // fire destroyed hook
        callHook(vm, 'destroyed')
        // turn off all instance listeners. 移除实例上的所有事件监听器
        vm.$off()
        // remove __vue__ reference
        if (vm.$el) {
          vm.$el.__vue__ = null
        }
        // release circular reference (#6759)
        if (vm.$vnode) {
          vm.$vnode.parent = null
        }
      }
       //父级存在,且父级没有在销毁的过程中,就把当前实例从父级移除
          remove(parent.$children, vm)
        }
        //接触父子关系之后,将自己身上的依赖追踪和事件监听移除。
        if (vm._watcher) {
          vm._watcher.teardown() //teardown:从所有依赖向的Dep列表中将自己删除.
        }
        let i = vm._watchers.length
        while (i--) {
          vm._watchers[i].teardown()
        }
        if (vm._data.__ob__) {
          vm._data.__ob__.vmCount--
        }
        // call the last hook...
        vm._isDestroyed = true   //销毁标志
        vm.__patch__(vm._vnode, null)  //vnode置为null
        // fire destroyed hook
        callHook(vm, 'destroyed')
        // turn off all instance listeners. 移除实例上的所有事件监听器
        vm.$off()
        // remove __vue__ reference
        if (vm.$el) {
          vm.$el.__vue__ = null
        }
        // release circular reference (#6759)
        if (vm.$vnode) {
          vm.$vnode.parent = null
        }
      }
    
    展开全文
  • Vue源码笔记之项目架构

    千次阅读 2019-08-24 16:43:40
    本阅读笔记基于Vue 2.6.10,主要记录了自己对Vue源码的一些理解,并参考了刘博文著的《深入浅出Vue.js》一书以及csdn博主恰恰虎的Vue源码学习系列文章。由于能力有限,笔记中对源码的认识可能不够深入,如果感兴趣,...

    终于开启了Vue源码的阅读之旅!虽然只有三个月左右的使用经验,阅读源码时会比较吃力,但是无所谓,当我们欣赏一件“艺术品”时,重点在于是否用心去欣赏,而不在于欣赏到什么程度。

    本阅读笔记基于Vue 2.6.10,主要记录了自己对Vue源码的一些理解,并参考了刘博文著的《深入浅出Vue.js》一书以及csdn博主恰恰虎的Vue源码学习系列文章。由于能力有限,笔记中对源码的认识可能不够深入,如果感兴趣,可以去官网下载Vue源码进行阅读,github地址为https://github.com/vuejs/vue

    下载的源码在webstorm中打开是这样的,让我们挨个分析,找出需要研究的重点代码:

    1. 项目总体结构

    在这里插入图片描述
    各个文件夹的说明如下:

    vue-dev
      |- .circleci //配置文件,可以忽略
      |- .github   //github相关配置,可以忽略
      |- benchmarks//性能测试代码
      |- dist      //构建后的各个版本的Vue
      |- examples  //官方提供的Vue示例
      |- flow      //facebook的静态类型检查工具
      |- node_modules //项目的依赖库
      |- packages  //存放独立发布的包的目录
      |- scripts   //构建命令及相关配置
      |- src   //Vue源码,重点关注对象
      |- test      //测试文件 
      |- types     //基于typescript的类型声明
         ...       //配置文件,可以忽略 
         package.json  //项目依赖描述
         ...       //配置文件,可以忽略
    

    对项目有一定了解的人应该都很容易看出来,src文件夹存放的就是整个项目的源码,而其他所有的文件都是它的衍生品。阅读源码只需要把目光集中在这里即可。

    下面我们打开src目录,看看各个文件夹的作用:

    2. src子目录概览

    首先打开src下的一级子目录:
    在这里插入图片描述
    各个文件夹的作用如下:

    src
      |- compiler //Vue编译器,用于解析Vue模板:template
      |- core     //Vue核心代码。Vue对象的构造和初始化,
                  //全局API,响应式系统构建,虚拟DOM
                  //的构建都在这里定义,并且是平台无关的
      |- platforms//平台相关的代码,主要指Vue支持的三种平台:
      			  //web(浏览器),weex和server(服务端渲染)
      |- server   //服务端渲染相关代码
      |- sfc      //单文件组件(.vue文件)的解析
      |- shared   //全局方法和共享的静态变量等
    

    compiler:
    上述子目录中,compiler是Vue的编译器,负责将模板template解析成渲染函数render,比如下面的模板:

    <div id="app">
      <ul>
        <li v-for="item in items">
          itemid:{{item.id}}
        </li>
      </ul>
    </div>
    

    会被Vue的编译器编译为下面的渲染函数:

    function render(){
      with(this){
      return 
         _c('div',{
             attrs:{"id":"app"}
         },
         [_c('ul',_l((items),function(item){
           return _c('li',
               [_v("\n itemid:"+_s(item.id)+"\n ")]
           )}
          )
         )]
       )}
    }
    

    执行完编译过程得到这个函数之后,Vue就不再需要模板了。Vue只需要调用这个函数,就可以生成该组件对应的虚拟DOM节点(VNode)。_c,_l,_v,_s等都是Vue内部定义的渲染函数会用到的函数。详细列表可以参考src/core/instance/render-helpers/index.js:
    在这里插入图片描述
    core:
    这里是Vue最为核心的代码。Vue构造函数定义及对象初始化、响应式系统的搭建、全局API定义、平台无关的组件keep-alive的定义、虚拟DOM树的构建等都在这里完成。这里定义的属性和方法是平台无关的,即它们在Vue支持的任意一个平台都是一致的,后面会继续展开介绍。

    platforms:
    Vue被设计为可以在多个平台上运行,这包括web(浏览器)、weex(阿里巴巴的一款跨平台框架)和server(服务端渲染)。对于Vue而言,与平台无关的代码被放到core中,而与平台相关的则放到platforms中定义。例如,使用服务端渲染时不会用到transition组件(服务端渲染不提供类似的过渡效果),那么这个组件的定义就被放到了platforms/web下面去定义,而不会出现在Vue的核心代码core中。而keep-alive组件则是在三个平台下都可以使用的,因此keep-alive在core/components中定义。

    server:
    服务端渲染相关文件,定义了server平台使用的相关方法,由于我们主要探讨Vue在web中的使用,这里将不再详述。

    sfc:
    单文件组件编译。在使用Vue构建单页应用时,我们通常会用到单文件组件,也就是vue后缀的文件,如button.vue。这类文件的结构通常如下:

    //模板定义
    <template>
      ...
    </template>
    //脚本代码,data、methods等都在这里定义
    <script>
      ...
    </script>
    //样式定义,scoped表示该样式只对当前组件有效,
    //如果去掉会泄露为全局样式
    <style scoped>
      ...
    </style
    

    sfc下的代码负责将上述单文件组件解析为对象的形式。

    shared:
    全局方法和共享的静态变量。如shared/constants中定义了生命周期钩子:
    在这里插入图片描述
    shared/utils.js则定义了大量的工具方法,如isUndef、isObject、hasOwn等等。

    从源码阅读的角度来说,我们最应该关注的是compiler、core和platforms这三个目录。compiler负责模板编译,core负责Vue核心功能的搭建,platforms负责处理平台差异。

    下面我们就集中来看这三个目录。

    3. compiler/core/platforms结构

    3.1 src/compiler

    上面讲到,compiler定义了Vue编译器,下面是compiler的结构:
    在这里插入图片描述
    Vue编译模板分为三步:

    1. 解析HTML,生成一棵抽象语法树AST。
    2. 标记静态节点。
    3. 生成渲染函数。

    对应上述目录结构来看:
    compiler/parser下的文件负责第一步,如(该例子来自csdn恰恰虎的文章:VUE源码学习第七篇–编译(parse)):

    <div id="app">
      <ul>
        <li v-for="item in items">
          itemid:{{item.id}}
        </li>
      </ul>
    </div>
    

    将被解析为:

    {
        "type": 1,
        "tag": "div",
        "attrsList": [
        {
           "name": "id",
           "value": "app"
        }
        ],
        "attrsMap": {
          "id": "app"
        },
        "children": [
        {
          "type": 1,
          "tag": "ul",
          "attrsList": [],
          "attrsMap": {},
          "parent": {
          "$ref": "$"
        },
          "children": [
          {
            "type": 1,
            "tag": "li",
            "attrsList": [],
            "attrsMap": {
              "v-for": "item in items"
            },
            "parent": {
              "$ref": "$[\"children\"][0]"
            },
            "children": [
              {
                "type": 2,
                "expression": "\"\\n itemid:\"+_s(item.id)+\"\\n \"",
                "tokens": [
                  "\n      itemid:",
                {
                  "@binding": "item.id"
                },
                "\n    "
                ],
                "text": "\n      itemid:{{item.id}}\n    "
                }
            ],
            "for": "items",
            "alias": "item",
            "plain": true
            }
                ],
                "plain": true
            }
        ],
        "plain": false,
        "attrs": [
            {
                "name": "id",
                "value": "\"app\""
            }
        ]
    }
    

    该对象就被称为一棵抽象语法树。实际上就是对模板字符串的一种结构化描述。

    compiler/optimizer.js负责第二步,标记静态节点。所谓标记静态节点,就是将不需要更新的节点标记出来,提高虚拟DOM的比对速度。比如:

    <div>
      <div>
        <p>{{message}}</p>
      </div>
      
      <div>
        <p>Hello World!</p>
      </div>
    </div>
    

    这里的第一个段落p绑定到了变量message,因此它的内容将随着message值的变化而变化。而第二个段落p里面是静态文本,那么无论业务数据如何变化,它渲染的内容都是不会变的,所以第二个p和包裹它的div将被标记为静态节点,进行虚拟DOM比对时将跳过该节点(其中包裹这个p标签的div会被标记为静态根节点,因为它的所有子节点都是静态节点)。

    compiler/codegen(code generate,代码生成)负责根据上述经过静态标记的抽象语法树生成渲染函数。生成结果即为上文compiler简介中的那个render函数:

    function render(){
      with(this){
      return 
         _c('div',{
             attrs:{"id":"app"}
         },
         [_c('ul',_l((items),function(item){
           return _c('li',
               [_v("\n itemid:"+_s(item.id)+"\n ")]
           )}
          )
         )]
       )}
    }
    

    得到这个渲染函数后,模板就已经没用了,Vue会为当前对象新增一个方法:vm._render用于保存该渲染函数。构建虚拟DOM就借助于该渲染函数。

    注意:
    运行Vue代码并不总是需要编译器。web平台的Vue分为两个版本:完整版本和运行时版本。两者的区别就是前者包含了编译器,而后者不包含编译器。通常在两种情况下我们不需要编译器:

    1. 手写渲染函数。
    2. 使用打包工具打包项目。

    我们知道,编译器的作用就是将模板编译为渲染函数。因此如果我们选择手写渲染函数,就可以使用运行时版本的Vue,它比完整版的Vue代码少了上千行,可以有效压缩框架体积。另外,如果使用打包工具如webpack进行打包,这些打包工具会在打包时提前将模板编译为渲染函数,因此打包后的文件使用的就是运行时版本。

    3.2 src/core

    这一部分是Vue的核心代码,我们来看目录结构(由于文件较多,这里不全部展开):
    在这里插入图片描述
    components,它里面定义了keep-alive组件,由于在各个平台都可以使用该组件,因此它被定义在核心代码中。

    global-api,顾名思义,它定义了一些全局api,包括:Vue.component(全局组件注册)、Vue.directive(定义全局指令)、Vue.filter(全局过滤器)、Vue.extend(Vue继承接口)、Vue.mixin(Vue混入)、Vue.use(插件安装入口)。

    instance,该文件夹定义了Vue实例的相关属性和方法。首先在instance/index.js中定义了Vue构造函数,然后使用mixin(混入)向Vue原型对象上注入了init(初始化)、state(状态)、events(事件)、lifecycle(生命周期)、render(渲染)相关的所有方法。使用new Vue({ … })构造Vue实例对象时,也是在这里进行初始化的,后面的文章将重点讨论。

    observer(观察者),构建响应式系统的相关方法。通过Object.defineProperty这个原生api构建起来的响应式系统,非常值得学习。最重要的概念包括:

    1. Observer:观察者,用于监听数据变化,并通知发布者。
    2. Dep:发布者,用于收集依赖,数据变化时通知watcher更新视图。
    3. Watcher:订阅者,管理视图更新。
    4. queueWatcher:视图更新队列。当数据变化时,Vue默认不会立即更新视图,而是暂时放入一个微任务队列中,等数据全部更新完毕再去更新视图。

    util,工具方法,如debug接口,error工具,环境参数接口等,这里不再详述。

    vdom,虚拟DOM相关代码,vnode.js定义了虚拟DOM节点的结构,patch.js定义了比对和修补DOM树的相关方法,其余文件通过这两个文件来构建和更新虚拟DOM树。

    3.3 src/platfroms

    这里根据平台的不同,向核心版本的Vue添加了一些平台相关的方法和属性。目录结构如下:
    在这里插入图片描述
    这里分为两个目录:web和weex,我们这里不探讨weex平台下Vue的使用,因此该文件夹可以暂时忽略。

    web根目录下有五个带entry前缀的文件,它们是使用打包工具生成Vue代码时的五个入口文件(也就是说打包工具从这里开始打包),不同的入口文件引用的模块不同,最终生成的代码版本也不一样。对应关系如下:

    1. entry-compiler,生成独立的Vue编译器。
    2. entry-runtime,生成运行时版本的Vue,不包含编译器。
    3. entry-runtime-with-compiler,生成完整版Vue。
    4. entry-server-basic-renderer,生成服务端Vue的基本版本。
    5. entry-server-renderer,生成服务端Vue的完整版本。

    除了这五个入口文件,还有4个文件夹:compiler、runtime、server和util。

    compiler,编译器,这里只是从src/compiler/index.js中导入核心版本的编译器,然后传入web平台相关参数,生成用于web平台下的编译器。

    runtime,运行时Vue,从src/core/index.js导入核心版本的Vue,向其扩展一些只能用于web平台下的方法和组件,得到可以运行在web平台上的Vue。

    server,为服务端渲染扩展的一些只能在服务端使用的方法和指令等,关于服务端渲染这里不再详述。

    util,平台处理部分用到的工具方法。

    Vue的整体架构和打包过程图解

    Vue源码的整体结构如下(图片参照深入浅出Vue.js一书手绘):
    在这里插入图片描述
    Vue完整版的打包过程如下(同样参考自深入浅出Vue.js):
    在这里插入图片描述

    总结

    本文是对Vue源码项目结构的大致分析,暂未涉及代码的实现。后面将分别介绍Vue的构造和初始化、响应式原理、编译器、虚拟DOM。由于本人水平有限,可能无法过于深入的讲解它们的实现细节(仅web平台下的完整版Vue就有一万三千多行代码,探究其中的所有细节并没有太大意义),因此将以介绍其实现原理为主。希望在本系列文章完结时,我本人能对Vue的源码有更进一步的认识。

    文章链接

    Vue源码笔记之项目架构
    Vue源码笔记之初始化
    Vue源码笔记之响应式系统
    Vue源码笔记之编译器
    Vue源码笔记之虚拟DOM

    展开全文
  • Vue 编程不良人Vue教程,学习笔记源码
  • Vue源码笔记之编译器

    千次阅读 2019-09-08 10:27:13
    如果单从Vue的角度来说,template(模板)并没有存在的必要。在Vue中,真正要使用的是从template编译生成的渲染函数,利用它可以直接生成虚拟DOM。实际上Vue向我们提供了直接书写渲染函数的能力(这样就可以不用写...

    什么是编译器?

    如果单从Vue的角度来说,template(模板)并没有存在的必要,它只是为了方便开发者使用Vue而设计的。在Vue中,真正要使用的是从template编译生成的渲染函数,利用它可以直接生成虚拟DOM。实际上Vue向我们提供了直接书写渲染函数的能力(这样就可以不用写模板,也不需要编译器)。但是渲染函数写起来往往不那么直观,如果是一个很复杂的DOM结构,开发者很难知道如何去书写这个渲染函数。为了降低开发者学习Vue的心智负担,Vue提供了更直观、更简洁的模板,它基于HTML语法,对前端开发者来说非常友好。但是Vue最终需要的还是渲染函数,于是Vue就必须具备将模板编译成渲染函数的能力,而这个能力,就是由编译器(compiler)提供的。

    举个例子,假如我们没有编译器,我们需要像下面这样去书写一个渲染函数:

    var app = new Vue({
      el: "#app",
      data: {
        items: [{id: 1}]
      },
      render(){
        with(this){
        return 
           _c('div',{
             attrs:{"id":"app"}
           },
           [_c('ul',_l((items),function(item){
             return _c('li',
               [_v("\n itemid:"+_s(item.id)+"\n ")]
             )}
            )
           )]
         )}
      }
    })
    

    这个渲染函数执行之后将得到一棵虚拟DOM树,其中的_c、_l、_v、_s都在Vue的src/core/instance/render-helpers下面提供。而这里的渲染函数(render)如果写成模板,结构是这样的:

    <template>
      <ul>
        <li v-for="item in items">
          itemid:{{item.id}}
        </li>
      </ul>
    </template>
    

    看上去非常直观和简洁,不是吗?对于习惯了书写HTML的前端开发者来说,这种写法显然更具有吸引力(不过某些情况下,书写渲染函数有它独特的优势,实际上它比模板更灵活)。但是这种写法对Vue来说非常不友好,它既不是标准的HTML,又不是可以执行的函数,因此既不能直接用于渲染,又不能用于生成虚拟DOM。

    为了解决这个矛盾,Vue需要能将易于开发者理解的模板转化为易于Vue使用的渲染函数。Vue把实现这个功能的代码封装在src/compiler下面(还有少量平台相关的代码位于src/platforms/web/compiler下)。我们把负责将模板编译成渲染函数的代码作为一个整体称为编译器。

    编译过程简介

    模板的核心编译过程分为三步:

    1. 解析模板,生成抽象语法树。
    2. 标记静态节点。
    3. 生成渲染函数。

    来看一下src/compiler/index.js的结构,它很清晰地表达了模板编译的步骤:

    import { parse } from './parser/index'
    import { optimize } from './optimizer'
    import { generate } from './codegen/index'
    import { createCompilerCreator } from './create-compiler'
    
    export const createCompiler = createCompilerCreator(function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)  //生成抽象语法树
      if (options.optimize !== false) {
        optimize(ast, options)       //标记静态节点
      }
      const code = generate(ast, options) //生成渲染函数(及静态渲染函数)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })
    

    这里使用了JavaScript中的偏函数,它是一种高阶函数,借鉴自函数式编程思想。偏函数利用了JavaScript中的函数既可以作为参数传入另一个函数,也可以作为一个函数的返回值的特性,可以将一些通用函数包装成专用函数。

    上面的代码中,首先引入了编译的三个步骤所需要的函数:parse、optimize和generate。然后是一个通用的编译器的构造器createCompilerCreator,它接受一个基础构造器,这里的大部分代码都是在定义这个基础构造器。上面的代码压缩一下如下:

    export const createCompiler = 
      createCompilerCreator(function baseCompile (){ ... })
    

    所以实际上createCompiler的值是createCompilerCreator的返回值,而不是我们上面看到的那个对象(它是我们传入的参数baseCompiler的返回值)。所以下面我们就来看createCompilerCreator是怎么实现的:

    //引自src/compiler/create-compiler.js
    export function createCompilerCreator (baseCompile) {
      return function createCompiler (baseOptions) {
        function compile (
          template: string,
          options?: CompilerOptions
        ): CompiledResult {
          ...
          // 将options的参数归并到finalOptions,为了方便理解,这里写的伪代码
          mergeOptionsToFinalOptions();
    	  //调用传入的基础编译器,生成编译后结果
          const compiled = baseCompile(template.trim(), finalOptions)
    	  ...
          return compiled
        }
    
        return {
          compile,
          compileToFunctions: createCompileToFunctionFn(compile)
        }
      }
    }
    

    从上面的代码可以看出,createCompilerCreator 的返回值是一个名为createCompiler的函数。它接受一个baseOptions作为参数,返回一个名为compile的函数,这个函数实际上就是我们的编译器了。我们只需要把模板(template)作为参数传给它,它就会返回给我们一个非常有用的对象,该对象包含了三个属性,分别是抽象语法树、字符串格式的渲染函数和静态渲染函数数组。至此,模板就被转化成了一个包含上述三个属性的对象。

    整理一下上面的思路:

    1. 以createCompilerCreator函数为入口,我们传入了一个基础编译器函数baseCompiler作为参数,得到了一个createCompiler函数。
    2. 调用createCompiler函数,传入一个配置对象baseOptions作为参数,得到一个编译器函数compile。
    3. 调用compile函数,传入模板字符串,得到一个对象,包含抽象语法树、渲染函数和静态渲染函数数组。

    编译过程详解

    这里主要介绍Vue编译模板的三个步骤,从这三个步骤中就可以看出模板转化为渲染函数的大致过程。

    1. 解析模板,生成抽象语法树

    Vue将这个功能封装成了函数parse,使用parse( template, options )即可将template解析为抽象语法树。我们先来看一下这个转化的前后对比。

    假设我们有如下的模板(引自恰恰虎的Vue源码系列文章VUE源码学习第七篇–编译(parse)):

    <div id="app">
      <ul>
        <li v-for="item in items">
          itemid:{{item.id}}
        </li>
      </ul>
    </div>
    

    当我们把这个模板作为字符串传入parse函数,并附带相关的配置options后,就会得到下面这个JavaScript对象:

    {
        "type": 1,
        "tag": "div",
        "attrsList": [
            {
                "name": "id",
                "value": "app"
            }
        ],
        "attrsMap": {
            "id": "app"
        },
        "children": [
            {
                "type": 1,
                "tag": "ul",
                "attrsList": [],
                "attrsMap": {},
                "parent": {
                    "$ref": "$"
                },
                "children": [
                    {
                        "type": 1,
                        "tag": "li",
                        "attrsList": [],
                        "attrsMap": {
                            "v-for": "item in items"
                        },
                        "parent": {
                            "$ref": "$[\"children\"][0]"
                        },
                        "children": [
                            {
                                "type": 2,
                                "expression": "\"\\n      itemid:\"+_s(item.id)+\"\\n    \"",
                                "tokens": [
                                    "\n      itemid:",
                                    {
                                        "@binding": "item.id"
                                    },
                                    "\n    "
                                ],
                                "text": "\n      itemid:{{item.id}}\n    "
                            }
                        ],
                        "for": "items",
                        "alias": "item",
                        "plain": true
                    }
                ],
                "plain": true
            }
        ],
        "plain": false,
        "attrs": [
            {
                "name": "id",
                "value": "\"app\""
            }
        ]
    }
    

    从JavaScript的角度来说,这是一个普通的对象(同时也是一个标准的JSON对象)。它本身是一种树形结构,是对模板结构的一种抽象描述,因此这个对象就被称为抽象语法树。这个转化正是parse函数的最终目的。

    parse函数中最重要的是调用了parseHTML函数来解析HTML结构,这个解析过程与HTML引擎的解析过程类似。以下面的简单模板为例:

    <div id="app">
      <p>{{ name }}</p>
    <div>
    

    下面就来看parseHTML函数的大致过程(这主要涉及到编译原理和正则表达式的知识,我们只进行简述):

    1. 定位“<”,因为它是标签的开始标记。
    2. 使用正则表达式解析“<”后面的单词,它是标签名,这里是div。
    3. 依次解析标签名后面的属性,它以“=”作为标志,因此可以使用正则表达式进行解析。
    4. 匹配“>”,它表示开始标签结束。此时函数解析的内容包括<div id=“app”>。解析完开始标签后,将解析出来的标签压栈,因为之后需要匹配结束标签。此时栈内就保存了div对应的解析结果,即:
    stack.push(
    { 
      tag: tagName, 
      lowerCasedTag: tagName.toLowerCase(), 
      attrs: attrs, 
      start: match.start, 
      end: match.end 
    })
    
    1. 继续按照上面的方式,解析“<”,它表示一个新的标签开始。重复1 - 4步,即可解析出p元素,然后将其按同样的方式压栈。
    2. 解析绑定的变量{{ name }}。注意,这里所谓的解析并不替换name的值,而是以"_s(name)"的格式保存到p的expression属性里,表示这是一个表达式,在生成渲染函数时是需要替换的。_s是编译器提供的工具函数,它负责文本替换。
    3. 解析p元素的结束标签</p>,并将之前压入栈的p对象出栈。
    4. 解析div元素的结束标签</div>,并将之前栈内的div元素出栈。

    经过上述步骤,template模板的每个字符都被解析完毕,栈内的元素也被清空,因此模板被完全解析,最终得到前面展示的那样的抽象语法树。这个抽象语法树就是parse函数最终的输出。

    2. 标记静态节点

    标记静态节点的主要目的是提高虚拟DOM比较的效率。虚拟DOM通过diff算法来判断一棵树的哪一部分发生了变化,然后根据这个变化去更新实际的DOM。如果不进行静态节点标记,Vue就必须要完全地比较树中的所有节点,如下面的例子:

    <template>
      <div>
        <h1>标题</h1>
        <p>{{ content }}</p>
      </div>
    </template>
    

    上面的模板编译完将生成下面的结构:
    在这里插入图片描述
    显然div的左侧分支h1的内容永远都不会变化,所以在进行比较时,就没有必要去对h1进行比对。这样每次数据发生变化时,只需要去检查p元素的内容有没有变化即可,这在大部分情况下可以大大提升diff算法的效率(不过对于这里的情况,h1还是会参与比对,因为它只含有一个文本节点,进行处理的收益很低)。

    Vue标记静态节点分为两步:

    1. 标记所有静态节点
    2. 标记静态根节点

    代码如下(引自src/compiler/optimizar.js):

    export function optimize (root, options) {
      if (!root) return
      isStaticKey = genStaticKeysCached(options.staticKeys || '')
      isPlatformReservedTag = options.isReservedTag || no
      // 标记所有静态子节点
      markStatic(root)
      // 标记静态根节点
      markStaticRoots(root, false)
    }
    

    实际上标记静态节点的目的就是找到所有静态根节点,然后设置其staticRoot = true,这样在进行虚拟DOM树比较的时候就可以跳过该节点。静态根节点可以这样描述:如果某个节点的所有直接子节点都是静态节点,那么该节点就是静态根节点。这也就意味着,一个节点一旦被标记为静态根节点,那么它的直接子节点本身是不会变化的(但是子节点的子节点不一定)。

    首先我们来看第一步,Vue是如何认定一个节点为静态节点的。Vue使用一个函数isStatic来检查一个节点是否是静态节点,静态节点的条件包括:

    1. 是文本节点(node.type === 3)。直接判定为静态节点。
    2. 有pre属性。这表示开发者不希望节点的内容被编译,因此认定为静态节点。
    3. 没有动态绑定、没有if/for条件、不是Vue内置组件、不是自定义组件、不是带有for条件的template的直接子元素以及staticKeys没有被缓存。满足这里的所有条件也将被判定为静态节点。

    标记完所有的静态节点后,就可以进行静态根节点的标记。该过程从根节点开始进行,首先从当前节点开始检查,当满足下面几个条件时,就可以认为该节点是静态根节点:

    1. 被标记为static。
    2. 直接子元素存在(children.length > 0)。
    3. 不满足直接子元素只有一个,且是文本节点。

    这里之所以不把没有子节点以及只有一个文本节点的静态节点认定为静态根节点,是因为标记它带来的收益很低,甚至没有直接刷新该节点的性能高,所以干脆不对它进行标记。注意,这里判定一个节点是否为静态根节点,不需要检查它子节点的后代节点,因为diff算法在做比较的时候,每次就只比较当前节点及其子节点(后代节点是递归比较的,与当前节点无关)。因此,只要一个节点是静态的,且直接子元素不变,那它就被认为是静态根节点。

    随后就需要对当前节点的子元素进行遍历,来判断它的子元素是否为静态根节点,然后一直递归下去:

        if (node.children) {
          for (let i = 0, l = node.children.length; i < l; i++) {
            markStaticRoots(node.children[i], isInFor || !!node.for)
          }
        }
        if (node.ifConditions) {
          for (let i = 1, l = node.ifConditions.length; i < l; i++) {
            markStaticRoots(node.ifConditions[i].block, isInFor)
          }
        }
    

    首先检查当前节点的子元素是否为静态根节点,然后检查所有带有if判断条件的子节点,这样就可以遍历所有的子节点,进行静态根节点的标记。在标记子节点时,会继续递归地遍历,直到所有的节点都被标记完。

    3. 生成渲染函数

    得到经过标记的抽象语法树(AST)后,就可以进行渲染函数生成了。实际上这里生成的渲染函数并不是一个真正的函数,而是一个字符串,不过把它作为参数传递给Function构造函数,就可以得到渲染函数了。我们先来看一下一个渲染函数的结构,以下面的模板为例:

    <template>
      <div>
        <h1>标题</h1>
        <p>{{ content }}</p>
      </div>
    </template>
    

    它生成的渲染函数如下:

    "with(this){return _c('div',[_c('h1',[_v('标题')]),_c('p',[_v(_s(content))])])}"
    

    上述字符串作为参数传递给Function构造函数后得到的结果如下:

    new Function("with(this){return _c('div',[_c('h1',[_v('标题')]),_c('p',[_v(_s(content))])])}")
    
    => f(){
      with(this){
        return _c('div', [
          _c('h1', [
            [_v('标题')]
          ]),
          _c('p', [
            _v(_s(content))
          ])
        ])
      }
    }
    

    该字符串被解析为了一个函数,函数中带有大量的辅助函数,这些辅助函数是在执行渲染函数时将被调用的,每个辅助函数可以用于生成不同类型的节点。如_c用于生成普通的标签节点,_v用于生成文本节点,而_s用于生成字符串(因此传入_s的值将作为变量,调用toString方法转化为字符串)。所有这些辅助函数定义在src/core/instance/render-helpers下面。

    下面我们来看Vue根据抽象语法树生成渲染函数的大致过程。引自src/compiler/codegen/index.js:

    export function generate (ast, options) {
      const state = new CodegenState(options)  //获取状态
       //使用AST和状态生成渲染函数
      const code = ast ? genElement(ast, state) : '_c("div")'
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
      }
    }
    

    这样返回的render就是我们上面说的那个很长的字符串了,在需要调用的时候,传入Function构造函数即可。staticRenderFns在渲染静态根节点时被调用,它是一个数组,每个成员对应一个静态根节点。上面的代码中,最重要的就是genElement函数,它会根据ast的结构生成不同类型的节点,比如遇到一个div,它就会返回_c(‘div’, …)这样一段字符串,如果这个div内部还有待解析的节点,就会递归下去,生成上面的一段很长的字符串。

    genElement的执行过程如下:

    export function genElement (el, state) {
      if (el.parent) {  //继承pre属性
        el.pre = el.pre || el.parent.pre
      }
    
      if (el.staticRoot && !el.staticProcessed) {
        return genStatic(el, state)  //生成静态节点
      } else if (el.once && !el.onceProcessed) {
        return genOnce(el, state)   //生成一次性节点
      } else if (el.for && !el.forProcessed) {
        return genFor(el, state)    //生成for节点
      } else if (el.if && !el.ifProcessed) {
        return genIf(el, state)     //生成if节点
      } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
        return genChildren(el, state) || 'void 0'  //生成子节点
      } else if (el.tag === 'slot') {
        return genSlot(el, state)   //生成插槽节点
      } else {
        // 生成组件或者标签对象
        let code
        if (el.component) {    //解析注册的组件
          code = genComponent(el.component, el, state)
        } else {
          let data
          if (!el.plain || (el.pre && state.maybeComponent(el))) {
            data = genData(el, state)
          }
    
          const children = el.inlineTemplate ? null : genChildren(el, state, true)
          code = `_c('${el.tag}'${
            data ? `,${data}` : '' // data
          }${
            children ? `,${children}` : '' // children
          })`
        }
        // module transforms
        for (let i = 0; i < state.transforms.length; i++) {
          code = state.transforms[i](el, code)
        }
        return code
      }
    }
    

    这里每个分支用于处理某个特定类型的节点,处理流程相对较为复杂,这里不再详述,感兴趣的请阅读src/compiler/codegen.index.js相关代码。这些函数的返回值形如:

    return `_o(${genElement(el, state)},${state.onceId++},${key})`
    

    即从ast中解析出当前节点的属性,作为参数传入生成当前节点类型对应的辅助函数中,最终以字符串的形式返回。这样就会得到形如_o( … )这样一个字符串,经过递归处理,最终会形成一个很长的调用栈,这就是渲染函数了。

    编译器最终生成的结果就是一个字符串形式的渲染函数,以及静态根节点对应的渲染函数数组。将该字符串传入Function即可得到渲染函数,调用数组中的函数则可以生成对应的静态根节点。而得到渲染函数后,调用它就可以得到当前组件对应的虚拟DOM节点(VNode),它将用于生成虚拟DOM树。

    总结

    编译器在Vue中是一个相对独立的存在,它的唯一作用就是将模板编译为渲染函数,供虚拟DOM使用。如果项目经过webpack打包(它会提前编译模板),或者每个组件都是手写的渲染函数,那么项目中的Vue是不包含编译器的,这样可以压缩库的大小。

    实际上编译器涉及到的细节很多,这里只是大致了解了编译器的工作流程,并没有详细探讨编译器的完整实现,对编译器原理感兴趣的可以进一步阅读源码。后文将探讨虚拟DOM的原理,它是Vue最重要的模块之一,敬请期待。

    文章链接

    Vue源码笔记之项目架构
    Vue源码笔记之初始化
    Vue源码笔记之响应式系统
    Vue源码笔记之编译器
    Vue源码笔记之虚拟DOM

    展开全文
  • vue源码笔记

    2018-12-29 17:33:04
    // 缓存一个对某个字符串执行某个计算规则的结果,避免重复计算带来的开销 function cached (fn) { var cache = Object.create(null); return (function cachedFn (str) { var hit = cache[str];...
  • Vue源码笔记之虚拟DOM

    2019-09-16 12:55:21
    Vue源码笔记之响应式系统 一文中我们说到,Vue自动更新视图的基础是响应式系统,即使没有虚拟DOM,响应式系统也足以实现视图的自动更新(早期的Vue就是这样做的)。在响应式系统中,当数据发生变化时,订阅者watcher...
  • Vue源码笔记之初始化

    2019-08-31 17:04:34
    Vue自身的构造。主要是关于如何向Vue的原型prototype添加方法。 Vue实例的初始化。即new Vue({ … })时执行的操作。 Vue自身的构造过程 1. 定义构造函数 当我们像下面一样引入Vue.js文件时: <script src=...
  • : undefined } 普通的children处理:最后也是返回一组一维vnode的数组,当children是Array时,执行normalizeArrayChildren normalizeArrayChildren 代码较长,此处就不贴了,可以自己对照源码来分析: - 定义res - ...
  • 学习vue源码笔记

    2021-09-02 09:59:44
    function cached(fn) { var cache = Object.create(null) return function cachedFn(str) { var hit = cache[str] return hit || (cache[str] = fn(str)) } } /** * Camelize a hyphen-delimited string. ...
  • Vue的响应式系统是一个精心搭建的监控系统,它负责监测项目中的数据变化,然后通知对该数据“感兴趣”的订阅者进行相关操作。我们分别来理解“数据”、“感兴趣”以及“订阅者”这三个关键词。 这里指的数据,就是...
  • Vue源码笔记(一)

    2021-10-14 11:23:59
    前言 现在程序猿越来越内卷了,当年面试可能只是问问vue如何使用,问一些option api的问题。...那么,下面我们就开启vue源码的探索学习之路吧! 解读目录 首先我们找到Vue项目的一级目录 └── vue ├── BACKER
  • 这次开始学习vue源码的知识,并且把学习的内容都写成笔记,方便以后复习。 本节的内容是:了解vue源码目录设计,为后面做源码分析做好准备。 1.目录结构 src ├── compiler # 编译相关 ├── core # 核心代码 ...
  • Vue源码笔记(一)监测数据---observer类依赖收集Dep类---依赖管理器依赖---Watcher类 监测数据—observer类 借助 Object.defineProperty()实现,具体实现代码: // 源码位置:src/core/observer/index.js /** * ...
  • 这次开始学习vue源码的知识,并且把学习的内容都写成笔记,方便以后复习。 本节的内容是:了解vue源码入口文件。 入口文件 我们上一节分析了Vue.js的构建过程,我们这次分析的是 Runtime + Compiler 构建出来的 Vue...
  • vue源码阅读笔记

    2021-02-25 11:25:43
    开始阅读前先明确以下内容: 1、源码中出现大量以下划线_ 为首的变量名,这里_表示私有变量,不推荐外界访问 ...以下是个人笔记,持续更新中~ 为什么在mounted()中就能通过this.messsage 的方式访问
  • vue资料 笔记 源码

    2018-11-27 15:09:48
    vue基础资料 里面含有老师讲课的笔记 例子的源码 笔记非常仔细 代码也很规范 适合初学者学习
  • 一、VUE 1.1 MVVM VUE也是基于MVVM模式实现的。特点就是数据双向绑定 在MVVM模式中,分成三个部分: M模型model V视图view VM视图-模型view-model 前端的本质是, 将人眼可读性强的数据,转换成机器可读性强...
  • vue源码学习笔记

    千次阅读 2018-06-17 20:20:37
    Vue的本质 Vue的本质就是用一个Function实现的Class,然后在它的原型prototype和本身上面扩展一些属性和方法。 它的定义是在src/core/instance/index.js里面定义 使用ES5的方式,即用函数来实现一个class,不用...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 7,803
精华内容 3,121
关键字:

vue源码笔记

vue 订阅