Git Product home page Git Product logo

blog's People

Contributors

xiaodaogithub avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

Vue:为什么可以通过this取到属性、方法?

前言

​ 第一次写 Blog,心里还是有点激动和惶恐的,也算自己在毕业前做的一个向前的决定吧,把一些东西记录下来。我的记忆挺不好的,大多概念我也都曾经烂记于心,但大都随着短暂的时间推移而遗忘,做这个决定一是为了记忆,二是锻炼一下自己的表述能力,也算是对自己的一个挑战吧。

​ 如果 Blog 的文章对你有帮助的话,可以 star 一下,算是对我的一个鼓励吧,如果看到表述错误的地方,也请指出,我看到了会尽快的去处理的,谢谢。

Vue源码可以配合Vue-analysis 同步观看。

从入口开始

通常我们的 Vue 程序是定义在main.js 中的,如下:
Snipaste_2019-05-04_16-05-05

我们通过 new 一个 Vue 类,通过传入一个对象来开始我们的 Vue 程序。 然后我们就可以在钩子函数中通过this.xxx 来获取到我们所需要的属性及方法。说到这里,就有一个问题了,我们的 data 是一个函数,即使求得返回值我们也应该是 this.data.xxx 来获取,或者通过循环将 data 拷贝到 Vue 实例上去?这里先留个疑问,后面我们自然就知道了。

既然我们是 new 一个类,所以我们最先应该来到最开始定义 Vue 函数的地方src\core\instance\index.js
Snipaste_2019-05-04_16-17-23

在这里我们可以看到 Vue 最开始只是一个简单的函数类,随后会在 Vue.prototype 上面定义一些属性和方法逐步去扩展这个类,慢慢的就形成了 Vue。

从这个函数我们可以看到,我们最开始执行的便是 this._init 方法了。
Snipaste_2019-05-04_16-27-33

这个截图中解释的也比较清楚了,这里需要注意的就是 mergeOptions 这个函数了,这个函数会将本身和传入的 options 进行合并。 不同的属性合并的策略是不同的,向 components 组件是一种合并方式,而钩子函数又是一种合并方式,这个函数的作用就是对不同的属性使用不同的合并策略进行合并。

随后我们又会执行一些列的 init 方法,这里我们重点看一些 initState, 其他的方法可以自行查看一些具体都干了什么,后面到对应章节也会再回来介绍。
Snipaste_2019-05-04_16-33-17

在这个函数中,我们可以看到一系列的 init 方法,这里我们进入 initData 方法,其他的实现都类似,可以自己去分析一下。

Snipaste_2019-05-04_16-40-04

函数最开始我们便会拿到 vm.$options.data , 在组件中,我们的 data 一般会定义为函数返回对象的形式(对象是引用类型,防止多个组件引用统一对象),通过执行该函数获得到该对象然后赋值给 vm._data(下划线一般表示这是一个私有属性,只给函数内部使用)。

然后我们会进入要 while 循环中,由于 initProps initMethodsinitData 之前执行,所以我们会检测是否和之前定义的属性,函数名重名(仅在开发环境中),随后我们会执行 proxy 这个函数 proxy(vm, '_data', key)
Snipaste_2019-05-04_16-46-33

通过该函数我们可以看到,我们会将对象的属性名称通过 Object.defineProperty 定义到 Vue 实例上面去,这是我们可以访问的原因,但我们访问该属性触发 get 函数的时候,我们其实访问的是 this._data 下面对应的属性,this.xxx 只是为我们方便操作数据的一个映射。

函数的最后会将 data 定义为观察者对象,这部分会在后面对应的章节中去说明。

我觉得,Vue 这么做的一个原因有两点,一:方便操作,我们直接可以通过 this 来获取。二:
保护了数据的修改在可控的范围内,减少bug。

好了,第一篇文章到这里就结束了,后面我会尽量有时间就写点,不过,毕竟咱都是程序员(哭)。

预排序遍历树算法和JSON互转

预排序遍历树算法和JSON互转

预排序遍历树算法简介

image

预排序遍历树算法简称mptt,主要应用于层级关系的存储和遍历。

MTTP 不直接存储父分类信息,而是通过 left、right 配合depth来表示层级关系。

  • 优点
    • 查询效率高,只需要一次查询即可获得层级结构中某个节点的所有子节点,无需递归查询
  • 缺点
    • 插入、删除、移动节点效率较低,需要修改当前节点后面所有节点的左右值

由于公司数据库采用的是这样的一种格式存储,返回给前端的是一维的数组,列表展示是完全没有问题的,但在前端某些特殊情况下需要json的嵌套格式来展示数据,所以需要将mptt转为json,而搜索一番没找到相应的实现,遂自己实现了mptt树和json的互转,如有需要的同学可以对代码进行适当的更改以适应自己项目。

MTTT 转 JSON
const mptToJson = (arr) => {
  if (!Array.isArray(arr)) {
    throw Error('mpt to json parameter must be a array');
  }
  // 根据depth来,存放层级关系,
  let stack = [];
  // 存放嵌套json
  let result = [];
  // 结果的map结构
  let resultMap = {};
  // 上一次遍历的节点
  let last = null;
  // 引用当前遍历的节点的子节点,递归的时候使用
  let children = null;
  function judgeDepth(item) {
    // 没有last,说明是第一次进入,一定是一级节点
    if (!last) {
      // stack放入该节点
      stack.push(item);
      // result存放一级节点
      result.push(item);
      // 设置上一次的调用接待室
      last = item;
      // children设置为result,表示一级节点的父级是初始化的数组
      children = result;
      
      // 不是第一次进入,说明last一定有值
    } else {
      // 当前节点和上一个节点depth相同,说明是同级节点
      if (item.depth === last.depth) {
        // stack层级移除最后一个节点
        stack.pop();
        // 获取stack的最后一层,stack存放的是父子层级
        last = stack[stack.length - 1];
        // 如果有last,说明当前节点是有父节点的
        if (last) {
          // 引用父节点的children
          children = last.children!;
          // 设置当前节点pid
          item.pid = last.id;
        } else {
          // 否则childre就是最外层数组
          children = result;
        }
        // chilren push当前节点
        children.push(item);
        // stack层级添加当前节点
        stack.push(item);
        // 设置last为当前节点
        last = item;
       // 当前节点depth比上一个节点depth大,说明是上一个节点的子节点
      } else if (item.depth > last.depth) {
        // 引用上一个节点的children
        children = last.children;
        children!.push(item);
        // 设置pid
        item.pid = last.id!;
        // 深度增加一层,stack存放当前节点
        stack.push(item);
        last = item;
      } else {
        // 这时表示既不是同级节点也不是父子节点,depth后退了一层
        stack.pop();
        // 重新设置last节点
        last = stack[stack.length - 1];
        // 重新调用该函数
        judgeDepth(item);
      }
    }
  }
  function eachArr(nodeArr) {
    nodeArr.forEach((item) => {
      // 在map结构中根据id来存放每一个节点,避免循环数组来查找节点
      resultMap[item.id] = item;
      // 给每一个节点增加children属性,用于json嵌套使用
      item.children = [];
      // 节点父id,用于嵌套中表示节点父子关系
      item.pid = undefined;
      // 对每一个节点调用函数
      judgeDepth(item);
    });
  }
  
  eachArr(arr);
  return {
    result,
    resultMap,
  };
};
JSON 转 MPTT
const jsonToMpt = (arrData) => {
  if (!Array.isArray(arrData)) {
    throw Error('json to mpt parameter must be a array');
  }
  // 我这里设置left值是从2开始的,根节点是单独存储的
  let left = 2;
  // 存放结果
  let result = [];
  function eachArr(arr) {
    let i = 0;
    let len = arr.length;
    for (i; i < len; i++) {
      // 获取每一个json数组的节点
      let item = arr[i];
      // 设置当前节点左值
      item.leftValue = left++;
      // result存放当前节点
      result.push(item);
      // 当前节点有子节点
      if (item.children && item.children.length > 0) {
        // 递归对当前节点的子节点数组调用该函数
        eachArr(item.children);
      }
      // 递归结束后当前的节点的右值就是正确的右值了
      item.rightValue = left++;
    }
  }
  eachArr(arrData);
  return result;
};

FAQ

  1. MPTT转json后增加、删除怎么做

    如果对实时性要求非常高的话,可以获取到当前节点的Pid,只需要修改父节点及之后节点的左右值即可,但这样前端等待时间比较长,如果前端实时性要求比较高的话可以把操作放在前端待用户操作一定时间后统一的保存到后端,这样需要区分哪些数据是新添加的,哪些是删除的。

  2. MPTT树一定要转json展示吗

    MPTT查询根据左值排序后是一个一维的已排序好数组,可以根据depth来实现层级展示

Vue递归渲染组件的过程

Vue递归渲染组件的过程

之前我们分析了如何通过 this 能获取到我们定义的属性、方法等内容,这节我们将会对接下来的过程进行一个详细的分析,这里只分析重点代码,对于没有讲到的细节部分,可以自己比源码阅读分析一下。

紧接着上一回为什么可以通过this取到属性、方法? ,我们可以会看到余下代码:

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
}

我们看到的vm.$options.el其实就是我们通过 new Vue({el: '#app'}) 中传入的 el,通常,只有第一次执行 init方法才会有该属性(一般来说后面执行子组件的 init 方法是没有这个属性的),我们这里调用的 $mount方法有两个场景,当我们使用 vue init 创建一个Vue-cli项目的时候,命令行选项会提供下图两个选项,选择 runtime -only还是 runtime+compiler 版本。如果我们选择的是 Runtime + Compiler 版本,这里的 vm.$mount是在 src\platforms\web\entry-runtime-with-compiler.js下,选择 Runtime + Only 版本 vm.$mount src\platforms\web\runtime\index.js下面,两个版本的区别在于 Runtime + Compiler 版本 多了一个将 template 编译为 render 函数的那部分代码,这样代码包会大些,并且如果我们是线上编译的话网页的速度也会变得非常慢,通常还是建议直接使用 Runtime - Only 版本。
Snipaste_2019-05-11_14-49-56

但这里我们选择 Runtime + Compiler 版本进行分析,因为这个函数结尾还是会调用Runtime Only 那里定义的$mount方法。

vm.$mount 做了什么

进入到 src\platforms\web\entry-runtime-with-compiler.js,我们会看到 const mount = Vue.prototype.$mount 这样一个代码,这里的 mount就是在 Runtime + only 里面定义的 $mount 方法,在这里做一个引用,在函数的最后会调用这个方法。

在带compiler版本中,这个$mount方法的作用就是它会判断你有没有定义 render 方法(后面用作生成vnode节点),当我们没有传入render函数,它会调用一系列的函数来生成一个render 方法,这个其实就是编译.vue文件的过程了,后面我们再详细分析这部分。

Snipaste_2019-05-11_15-10-37

当我们没有定义 render 方法的时候,它会先尝试来获取template,看看我们有没有定义 template 属性,如果我们定义了 template,它会判断是不是字符串、HTML节点或者其他的东西,对不同的类型执行不同的操作。

如果获取到 template的话,我们会调用 compileToFunctions 来生成一个 render 函数,这部分内容在之后会有单独的介绍。这里我们先接着往下看。

在 render 函数之后,我们会调用 mount.call 函数,这个函数就是在 Runtime + only 版本中调用的 $mount 方法,它在 src\platforms\web\runtime\index.js下面。这个方法会调用 src\core\instance\lifecycle.js的mountComponent 方法。

mountComponent 在调用了 beforeMount 钩子函数后会创建下面这个函数:

updateComponent = () => {
      vm._update(vm._render(), hydrating)
}
  // 创建一个观察者对象,会调用updateComponent函数来开始渲染页面
new Watcher(vm, updateComponent, noop, {
   before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true )

这里创建了一个观察者对象,这个对象作用就是将数据和当前的观察者对象相关联起来。

进入这个函数,先进行了一堆初始化操作,我们直接看到末尾!
Snipaste_2019-05-11_15-33-22

这里我们的 this.lazy是undefined,所以,我们这里会调用一次 this.get()。这次调用算是正式开启了组件的渲染环节。这个函数我们会先调用一下 pushTarget(this)pushTarget这个函数维持了一个全局的数组,我们每次调用这个函数,就会调用数组的 push方法将当前这个观察者对象添加到队列中去,然后把this赋值给currentTarget,由于我们的组件是可以嵌套其他组件的并且组件的渲染时从父到子的过程,所以我们会依次的将他们存储进来,在 get 函数末尾会调用 popTarget() 这个方法将调用完成的删除掉(也就是最近添加的一个), popTarget 是在调用 this.getter.call(组件调用是从父到子,所以调用结束的顺序是从子到父)之后调用的,所以这个队列的最后一个就是当前正在执行的 Watcher。

创建Vnode

this.getter.call 调用的就是之前定义的 updateComponent 函数,先通过 vm._render() 函数创建一个Vnode,这个函数定义在src\core\instance\render.js里面。

这个函数开始的其他逻辑先可以不用看,后面涉及到相关部分再讲解,我们先看 try catch 函数里面的的方法,

vnode = render.call(vm._renderProxy, vm.$createElement) ,当我们第一次进来的时候(后面子组件的render函数是由vue-loader生成的),这个render函数其实就是最开始调用 new Vue({render: h => h(App)})中传入的render 函数,后面再进来传入render函数的就是通过 template中的内容编译出来的render函数。我们这里传入的 h 其实就是这里的vm.$createElement方法,其中的参数 a 就是 初始化传入的App(已经被编译过的)这个组件。

  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

createElement 方式定义在src\core\vdom\create-element.js里面,这个函数会对参数进行一些参数的校验后会调用 _createElement 这个方法, vnode 就是在这里创建的。

函数开始也是做一些不同情况的处理,先判断 是否定义了 is(<component is="App" />) 属性,如果定义了,其实正在起作用的是 is 的值,到这里如果还没有 tag 的话,就返回一个空的 vnode。

紧接着我们会判断children 是不是数组,并且 children[0] 是不是函数,函数说明是一个scopeSlots ,这部分会在编译的时候看到。

随后会对children进行normalize 操作,就是将数组扁平化处理,将children排成一维数组。

接下来就是 createElement 最关键的部分了。

Snipaste_2019-05-11_17-56-58

如果tag是字符串类型的话,会判断是不是原生的html标签名或者是不是已经声明过(Vue.component或者components中定义)的组件,当tag不是字符串和自定义组件都会调用createComponet 方法,其他的都会直接创建对应的Vnode,接下来,我们分析下 createElement方法。

const baseCtor = context.$options._base
if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

这里的baseCtor是在src\core\global-api\index.js中定义的,传入的标签如果是对象的话,我们会调用baseCtor.extend 方法,也就是 Vue.extend 方法(src\core\global-api\extend.js)。

这个方法就是创建子组件的构造函数返回出去。

const Sub = function VueComponent (options) {
      this._init(options)
}
 Sub.options = mergeOptions(
      Super.options,
      extendOptions
)

这个构造函数其实和Vue是一样的,后面的操作都是在个Sub上面挂载Vue上面的属性和方法,不断对Sub这个构造函数进行增强,而传入的参数都是放在Sub.options中的,在接下来的patch过程中会执行这个构造函数等等,最后会返回这个构造函数。

在createElement中,我们直接跳到installComponentHooks 这个函数,其他的到相对应的部分再说。

这个函数的作用就是将定义好的钩子函数绑定到data对象中去,在构建vnode的时候作为属性传入进去,最后返回这个vnode。

 // 这里返回的只是一个占位符vnode,
 // 实际调用的是Ctor这个构造函数
 const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
 )

patch过程就是将vnode渲染到页面上去

在通过 vm._render()函数返回vnode节点之后,我们会执行 vm._updata(vnode)这个函数(src\core\instance\lifecycle.js).

    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
        // 每次patch后调用这个函数恢复当前的activeInstance
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      // 第一次调用_update时候vm.$el指向html节点
      // 子组件第一次调用的时候vm.$el 是undefined
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

这里我们会先保存之前vm._vnode(如果存在vm._vnode的话,应该是走更新组件的逻辑),然后将传入的vnode赋值给它,这里由于我们是还处于渲染阶段,所以vm._vnode 是undefined。而setActiveInstance维护了一个全局的activeInstance ,用于保存当前的this(Vue实例)。由于这里preVode 是undefined,所以我们会执行第一个_patch 函数(src\platforms\web\runtime\index.js)。

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
// 定义了创建、更新directive,ref逻辑
import baseModules from 'core/vdom/modules/index'
// 平台相关的生成代码如web平台是创建属性、事件等代码
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.

const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

在这里,我们可以看到真正的patch函数,它其实是采用了函数柯里化的技巧,在最开始将这些只需要执行一次的代码在编译阶段就执行掉了,避免了反复执行相同的代码。这里的模块在注释中写的很清楚,就不再复述了。通过createPatchFunction我们可以找到最终的patch代码。

在这个函数的开始我们可以看到这样一个循环,它的作用就是将就创建、更新style,class 、attrs等属性的相同名称的方法push到同一个数组中去。

  // 将这些基础方法的相同类别的方法统一放至到相同数组中,
  // 方便后面统一调用
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

在这个函数的最后会返回一个 patchpatch 函数,这个函数就是我们实际调用的函数。

patch过程

这个函数patchpatch (oldVnode, vnode, hydrating, removeOnly)最开始会判断有没有定义vnode,没有传入的话说明是组件执行销毁操作,如果是更新或者挂载的话,vnode是一定有的。

Snipaste_2019-05-12_12-57-02

接下来会执行这样一段逻辑,这个逻辑其实就是判断是渲染还是更新vnode。

如果oldVnode 是undefined的话,则说明调用是子组件的vm._update方法。如果定义了oldVnode,会判断oldVnode是不是一个真实的HTML节点,不是的话会判断是不是组件更新的逻辑。这部分就是对不同的情况执行不同的逻辑。

由于我们第一次进入这个函数,oldVode是一个HTML元素,所以我们会进入 if(isRealElement) 逻辑,这部分逻辑就是创建了一个空的vnode,把这个HTML节点作为vnode的一项保存起来,我觉得这部分的代码其实是为了让后面的代码可以重用而设计的,当我们oldVode和Vode不满足sameVnode方法的时候,我们也会进入else部分的代码,所以isRealElement就是抹平两者情况的差异,从而可以公用后面的逻辑。

除了组件更新的逻辑(这部分会作为单独的章节来分析),其他的都会进入createElm这个函数,所以我们接下来会分析这个函数的细节。

首先我们会看到:

 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
}

进入这个函数,我们会先判断 vnode.data中是否定义了hook钩子函数,这个属性只有组件节点生成vnode(在src\core\vdom\create-element.js中生成的)的时候才会创建的,所以我们第一次进来,这个函数已经在src\core\vdom\create-component.js定义过了。
Snipaste_2019-05-12_14-22-48

这里需要注意的是vnode.componentInstance这个属性(keep-alive组件,最开始的vnode挂载都会根据这个属性判断)。由于只有组件vnode才初始化了init方法,所以只有组件节点才具有componentInstance 属性,这个函数就是实例化在Vue.extend 中定义的子构造函数,然后调用$mount方法,这个方法就是在src\platforms\web\runtime\index.js中定义的,到这里为止,已经开始子组件的初始化过程了,然后又会重复之前的步骤(当然,也是有一些不同的地方,可以对照着代码去跑跑看)。

假设我们子组件又执行到了patch方法,然后又会执行createElm ,而此时createElm(vnode, insertedVnodeQueue) 由于子组件的oldVode是undefined,所以我们会执行o'l'dVode是undefined情况下的createElm方法。

在createElm方法中,我们又会执行createComponent方法,但是就一般的写法,我们的子组件的template开头是一个闭合的html标签。
Snipaste_2019-05-12_14-34-27

所以我们在创建Vnode的时候,是没有初始化init(这个方法只有组件创建vnode才会有)方法的,所以这里会跳过createComponent 这个方法,直接执行后面的逻辑。
Snipaste_2019-05-12_14-41-50

如果vnode.tag是存在的话,我们会进入这个逻辑,首先进行的是createChildren这个方法,这个方法就是对每一个children调用createElm方法,然后子再调用子的,这样依次递归,然后从下到上执行insert方法,就把子节点插入到父节点,这样就创建了一个真实的节点,patch过程其实就是这样一个过程。

似乎到这里完美的结束了,但这里还有一个问题,就是最上面的一个节点(调用new Vue时第一次进入patch函数中时我们第一次调用init方法时src\core\vdom\create-component.js),调用子组件的构造函数是没有传入parentElm的,所以它的是再createComponent 中插入的,那里有个判断vnode.componentInstance实例是否存在,只有组件节点才存在组件实例,而插入就是在那里进行的。

最后,执行到那里,页面是有两个id 为app的元素的,

Snipaste_2019-05-12_15-54-09
所以我们还需要把之前用做占位的app给删除掉,在patch函数的最后会执行removeVnodes 把最开始的那个app给删除掉。

到此,组件渲染到页面的过程就结束了,不过这么多写在一起,实在是不容易理解,还需要对照着源码来自己去分析几遍。

从原型链看babel编译class

最开始想到这个问题,是源于一位慕课老师的公众号的一篇文章中发布的一个开发性的题目,大概意思是想获取一个对象以及原型链上出现的所有以特定单词开头的属性或者方法名称的集合。这个问题并不难,但需要对 Es6 的 class 编译后的对象有一定的了解才能快速的解决问题,虽然我也解决了这个问题,但似乎我并没有关注过 Es6 编译后的代码是怎么实现继承的,所以才有了这一篇文章。
不过在开始之前还是推广一下老师的 github 开源项目 TaleLin,我并不是项目的参入者,但我能感受到他们对于这系列的用心。

首先,我们先来了解一下原型链的细节吧。

function Person(name) {
   // 定义每个实例私有的属性
    this.name = name;
  }
// 定义实例共有的方法
Person.prototype.showName = function() {
    alert( this.name )
}
  var p1 = new Person( 'Bob' )
  var p2 = new Person( 'Jack' )
  p2.showName()

在 Es5 中,我们使用 new 关键字来实例化一个类,通常的写法类似于上面这样,到这里,首先我们应该思考的,何为 prototype ?

prototype 是什么?

prototype属性只有函数才有,通常 是用来存储构造函数( Person ) 的所有实例 ( p1, p2 ) 所共有的属性和方法的地方,防止重复创建相同的逻辑代码。
prototype 默认只有一个不可枚举的属性 constructor ,该属性指向 prototype 所属的构造函数,这里就是 Person 对象

Snipaste_2019-05-10_16-36-36

proto 又是啥?

如果我们在控制台对 Person 或 Person.prototype 调用 console.dir() 命令的话,我们可以看到,他们都有一个 __proto__ 的属性。
__proto__ 是每一个对象(除了 null)都有的属性,它指向该对象的原型对象,可以通过它来获取到构造函数原型上面定义的方法,当我们尝试去获取一个属性的时候,会先在自己的属性中查找,如果没有则通过 __proto__ 到该对象的原型对象上面查找,依次向上查重,直到找到该方法或者到原型链的尽头才停止。在这个实例中,我们可以直接调用showName,也是因为我们可以通过 __proto__ 来访问 Person.prototype 来获得 showName 方法,正是是因为它,才构成了我们通常所说的原型链。
Snipaste_2019-05-10_16-53-20

__proto__并不是语言标准中的方法,而是浏览器为了方便我们访问原型对象而提供的方法,__proto__ 其实是定义在Object.prototype 上面的方法,可以理解成 Object.getPrototypeOf(obj)

原型链剩下的部分我们再配合 class 的编译后代码来讲解。

class 如何实现的?

我们先讨论单个 class 的实现,后面再说继承的实现。

class A {
    constructor(name) {
      this.name = name;
    }
    showName() {
      console.log(this.name)
    }
  }

在这里我直接贴出编译后的代码主体部分:

var A =
    function () {
        function A(name) {
            // A函数只能通过 new 调用
            _classCallCheck(this, A);

            this.name = name;
        }
        // 通过 Object.defineProperty为
        // A.prototype 添加方法
        _createClass(A, [{
            key: "showName",
            value: function showName() {
                console.log(this.name);
            }
  }]);

        return A;
    }();

可以看到,这是一个立即执行的闭包函数,我们先执行 _createClass ,该方法的就是通过Object.defineProperty 来给 A.prototype 添加后面的属性和方法,这个有一个细节就是通过
class 定义的方法默认是不可枚举的。
_classCallCheck 则是通过 this instanceOf A 来判断我们是不是通过 new 方法调用A函数,原则上,class 定义的类只能作为构造函数使用。

继承的实现

  class B extends A {
      constructor(name,age) {
       // 调用超类的构造函数
        super(name)
        this.age = age

      }
      showAge() {
        console.log(this.age)
      }
  }
// 编译后
var B =
    /*#__PURE__*/
    function (_A) {
        // 实现继承
        _inherits(B, _A);
        // 创建B构造函数
        function B(name, age) {
            var _this;
        //  class声明只能使用 new 关键字调用
            _classCallCheck(this, B);
        // _possibleConstructorReturn 判断是否调用super
        //  _getPrototypeOf(B).call(this, name) 调用超类的构造函数
            _this = _possibleConstructorReturn(this, _getPrototypeOf(B).call(this, name));
            _this.age = age;
            return _this;
        }

        _createClass(B, [{
            key: "showAge",
            value: function showAge() {
                console.log(this.age);
            }
        }]);

        return B;
    }(A);

_inherits(B, _A); 方法也很简单, 其实就是寄生组合式继承的实现。
Snipaste_2019-05-10_17-21-35

从图中我们可以看出,实现方法就是我们平时很熟悉的调用方法。

而后面 this, _getPrototypeOf(B).call(this, name) 则是通过apply来调用超类的构造函数,_possibleConstructorReturn判断是不是调用过super指向父类构造函数了。

总结

到这里我们可以看出,class 只不过是一个语法糖而已,虽然浏览器内部不一定是这么实现的,但babel编译后的代码确实就是以前的模式。当然有兴趣的同学可以尝试一下其他特性,这里就不一一解释了。

vue数据的获取和更新

上一篇,我们分析了组件的patch过程,接下来,我准备分析的数据的获取和更新。

我们都知道,在Vue的2.x版本是通过Object.defineProperty来定义get和set函数的,但在分析get和set之前,我们还需要先来看一下组件是如何通过Object.definePropery来定义数据的。

从initData来看组件定义响应式数据

我所选择的切入点是initData,这是在init方法中执行initState函数时来对组件定义的data做修饰的函数,具体定义在src\core\instance\state.js下面,之前我们说过,该函数最后会执行oberve(data)函数,下面我们来具体分析下这个函数的细节部分。

observe这个函数定义在src\core\observer\index.js下面,这个函数的作用只是对参数进行一个过滤。函数最开始会判断是否定义了__ob__属性,这个属性是在Observe构造函数中定义的,它的作用有两个,一是防止对同一个属性反复调用Observe函数,二是可以通过__ob__来访问Observe原型上面的方法。因为只有对象和数组才可以被定义为响应式的,因此这里会对所传值进行一次过滤。

    // 通过object.defineProperty定义一个不可枚举的__ob__属性,
    // 这个属性可以在触发更新时,手动调用dep.notify来通知订阅者
    def(value, '__ob__', this)
    // value是Array
    if (Array.isArray(value)) {
      // 浏览器支持 __proro__ 属性访问原型对象
      if (hasProto) {
        // 通过__proro__继承数组的方法, arrayMethods 是继承了数组方法的对象
        protoAugment(value, arrayMethods)
      } else {
        // 通过循环添加
        copyAugment(value, arrayMethods, arrayKeys)
      }

      // 数组会循环调用每一项,如果子项是数组或者对象,也会调用Observer方法
      this.observeArray(value)

      // value是对象
    } else {
      this.walk(value)
    }

数组和对象定义响应式的方法是不一样。对象可以直接通过Object.defineProperty来定义,而数组则需要通过对方法的拦截来实现。我们先看数组的实现。

这里会判断hasProto是否为true,其实就是看浏览器支不支持__proto__属性,copyAugment是兼容性写法,这里我们看protoAugment函数就好了,这个函数其实就是将value.proto 指向arrayMethods,所以我们需要了解arrayMethods到底是什么东西。

arrayMethods定义在src\core\observer\array.js下面。我们可以看到,这部分代码是在编译阶段就已经执行过了,这部分的内容就是数组响应式原理的真相。

这里我们可以看到这样一个数组:

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  // 通过闭包缓存数组原型已有的同名方法
  const original = arrayProto[method]
  // 通过Object.defineProperty重新定义相同名称的方法
  def(arrayMethods, method, function mutator (...args) {
    // 调用原有的方法求值
    const result = original.apply(this, args)
    // 创建observer对象的时候会挂载一个__ob__对象,
    // 既可以防止重复定义,也方便添加新属性的时候可以方便调用转换方法
    const ob = this.__ob__
    let inserted
    switch (method) {
      // 这三个方法会在数组上新增元素,通过 ob 来将新增的
      // 数值转换为观察者对象
      case 'push':
      case 'unshift':
        inserted = args
        break
      // splice方法第三个参数以后才是插入的数值
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 有新增的元素,则调用转换方法
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 当前的观察者对象属性发生了变法,通知订阅者
    ob.dep.notify()

    // 返回修改后的值
    return result
  })
})

这是数组用来操作数组的部分函数名。官网说这些操作可以触发数组的响应式更新,原因是Vue对上面这些方法进行了拦截。还记得之前我们说到的__ob__属性吗,这里就可以用到了,当我们调用以上中的某个方法来操纵数组时,Vue会通过__ob__来获取到Observe上面定义的观察者的notify方法来手动更新依赖,从而实现数组的响应式更新。

回到Observe函数中,我们接着会调用this.observeArray(value),这个函数是循环数组,对每一项调用observe函数,最后都会走到值不是数组的else逻辑,也就是调用this.walk(value)方法了。这个方法是对对象的每一个值都调用defineReactive来定义响应式对象,这个定义函数就是响应式的关键了。

函数的开始会调用new Dep来初始化一个订阅者对象,每当触发该对象的get函数的时候,就把触发的Watcher添加到这个订阅者队列中去,然后在触发set的时候,手动通知这个队列中的每一项来更新。

我们传入的value属性可以是原始类型也可以是对象,如果是对象的话我们也应该对对象的每一项来调用该方法将其定义为响应式的。

接下来就是通过Object.defineProperty来对值定义get和set方法了。

对于get函数的作用就是每当触发get的时候将当前的watcher添加到对应的订阅者队列中去,然后在触发set的时候发布通知每一个订阅了这个对象的Watcher来更新。这里的depend和notify特别是noeify函数有兴趣的可以去追踪看一下,这里就不做具体分析了。

//val可能是数组或对象,我们应该递归调用observe将子元素也覆盖到
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 定义了get方法的话则会调用定义的方法,否则返回val
      const value = getter ? getter.call(obj) : val
      // Dep.target 是当前的观察者对象,在mountComponent中会new Wacher,
      // 当我们调用watcher的get()函数时,会调用pushTarget(this),this会被赋值给Dep.target
      // 当触发get函数的时候,就可以通过dep.depend来将当前的watcher添加这个的订阅者中去
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          // 当前的这个val如果是引用类型的话,子元素也应该
          // 添加当前的Watcher,子元素改动,也要通知这个watcher
          childOb.dep.depend()
          // 对value的每一项的订阅队列都添加当前Watcher
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 获取到之前的值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // value变化了才会触发set
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // #7981: for accessor properties without setter
      // 只定义了getter而没有定义setter
      if (getter && !setter) return
      // 定义了setter函数的话则调用我们定义的setter函数
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对新赋值的值进行转换为observe
      childOb = !shallow && observe(newVal)
      // 通知每一个watcher更新
      dep.notify()
    }
  })

组件的更新逻辑

当我们触发set函数的时候,最后会调用dep.notify函数,这个函数最终执行的是Watcher.run方法,这个方法会调用wathcer.get函数,这个函数会重新执行vnode的构建,子组件的渲染。但此时是执行组件更新逻辑,部分地方是有所不同的。

当创建好vndoe后,我们会执行组件的patch过程src\core\vdom\patch.js,这时。我们会进入这样一个逻辑

 if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
 }

如果我们不满足sameVnode,就和之前的patch过程一样,重新创建元素,然后将之前的元素一次给销毁掉,如果满足sameVnode,就会进入patchVnode的逻辑,这就是大名鼎鼎的diff算法了。

初次进入不要被一些边缘处理逻辑给分散了注意力,我们只看主要逻辑即可.

首先我们可以看到const elm = vnode.elm = oldVnode.elm这样一段逻辑,这里的oldVnode.elm其实就是页面中该vnode对应的html元素了,对于相同的vnode,我们只对新旧vnode的不同之处进行修改,省去了部分节点的创建过程,这样性能也就提高了,所以接下来的代码都是对elm进行修改,将vnode和oldVnode直接的差异给抹平。

接下来我们会看到这样一段代码:

  const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      // 比较两个节点的属性是否改变,改变了调用对应的update函数
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }

    // 新旧vnode的children都存在的话会来对比他们的children
    // 然后再调用patchVnode这样递归来对比更新
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

这里是通过比较oldVnode和vnode,先对当前节点进行对比,然后再递归对字节的进行对比,这样可以最大化的来利用已经创建过的元素。

updateChildren就是对字节点进行对比的过程,也就是diff算法。

这个函数你会看到:

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // oldStartVnode是undefined向右移一位
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

       // oldEndVnode是undefined向左移一位
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]

       // oldStartVnode和newStartVnode是相同的vnode
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
         // 将这两个节点调用patchVnode,对比更新属性
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)

        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

        // 对比结尾的两个vnode是否满足sameVnode
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

          // oldStartVnode与newEndVnode是同一个节点的话,说明要将elm中的开始节点
        // 变成最后一个节点,这时调用insertBefore来调换位置
      } 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]

           // oldEndVnode和newStartVnode时相同节点
        // 将oldEndvnode调换到最前面
      } 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 {
        // 获取oldVnode的children所有的key集合
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 如果newStartVnode定义了key的话,获取oldKey的集合同相同key的vnode的位置
        // 否则循环oldVnode依次判断是否有符合sameVnode的节点,符合则返回vnode的位置
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

          // 如果这样还没找到的话则说明时一个新的vnode,调用createElm来创建它
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 如果找到相同key的vnode,则获取到这个vnode
          vnodeToMove = oldCh[idxInOld]
          // 满足相同vnode则调换位置
          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
            // 仅仅是key相同,其他的不相同,当作是一个新的元素来创建
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }

diff算法的结果就是尽可能的重用已经创建的html元素,所以它会对vnode进行一些列的尝试比对,这里会对不同的节点进行sameVnode判断,满足sameVnode就会调用patchVnode来进行更新。而key的作用就是在这里做sameVnode判断用的。

这里用一个实例的演示下key的作用:

// html
<ul>
   <li v-for="(item, index) in arr" :key="index">{{item}}</li>
</ul>
 <button @click="reverse">反转</button>
//js
 reverse() {
      this.arr = this.arr.reverse()
}

假设我们是会一个列表进行反转每一项,这里有三种情况:1.没有定义key属性,2.用数组索引定义key属性,3.用唯一id表示定义可以属性。

一: 没有定义key属性的情况

如果我们没有定义key属性,新旧vnode对比时,两边的第一个元素的key都是undefined,列表的标签吗都是li,又是通过循环生成的,所以很容易就满足的sameVnode,这样,我们就通过修改子节点的变化属性来达成元素的重用。相比全部重新创建,还是可以省区很多节点的创建过程。

二: 使用index作为key

使用索引做为key的话,如果我们对数组只是部分循序改变了的话,可以完全重用的代码其实就是从第一个子节点到循序改变的部分(第一种情况也是这样),循序改变的部分就是可没有定义key一样的判断了,还是通过修改属性以实现重用。

一、二两种情况其实是差别不大,从原理上看基本没什么区别。

三: 使用唯一id作为key

这也是推荐的写法,不管数组的排序如何改变,id是不变的,这样可以充分利用diff算法的优势,对于循序改变的节点只需移动位置即可,不需要创建html节点或者修改属性。这样性能是最高的。

xjb写了这么多,对于细节方面和实现逻辑还需自己去琢磨理会,这里只是对流程的一个叙述,详情可以对着注释源码来分析。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.