Giter Site home page Giter Site logo

blog's People

Contributors

emilyzhou90 avatar

Stargazers

 avatar

Watchers

 avatar

blog's Issues

vue cli 3配置markdown

vue cli 3抽象化了webpack配置,所以没有了build文件,但你还是可以通过vue inspect查找相应配置,并通过添加vue.config.js修改相应配置。
本文列举如何将markdown文件当做组件引入,

vue.config.js

const marked = require('marked')
const renderer = new marked.Renderer()

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('md')
      .test(/\.md$/)
      .use('html-loader')
      .loader('html-loader')
      .end()
      .use('markdown-loader')
      .loader('markdown-loader')
      .options({
        pedantic: true,
        renderer
      })
  }
}

配置完后可以通过命令vue inspect --rule md来审查项目的 webpack 配置有没有生效

home.vue

<template>
  <div class="home">
    <div class="md-content" v-html="content"></div>
  </div>
</template>
<script>
import intro from '@/md/intro.md'
export default {
  data() {
    return {
      intro
    }
  }
}
</script>

参考官方文档

css选择器优先级的研究

之前理解的css选择器优先级是这样的
!important >内联样式> id > class > 元素选择器 > 伪元素

看了一下w3c的文档规范,发现压根没有我想的这么简单

!important > 内联样式 没有变化

先说说有哪些选择器:

  1. 类型选择器(type selectors)(例如, h1)
  2. 伪元素(pseudo-elements)(例如, ::before)
  3. 类选择器(class selectors) (例如,.example)
  4. 属性选择器(attributes selectors)(例如, [type="radio"]),
  5. 伪类(pseudo-classes)(例如, :hover)
  6. ID选择器(例如, #example)
  7. 通用选择器(universal selector)(*)
  8. 组合子(combinators) (+, >, ~, ' ')
  9. 否定伪类(negation pseudo-class)(属于伪元素)(:not)

这些元素的特异性如下

  • ID选择器的个数(=a)
  • 类选择器、属性选择器、伪类的个数(=b)
  • 类型选择器、伪元素的个数(=c)

连接abc为一个三位数,计算他们的优先级:

*               /* a=0 b=0 c=0 -> 优先级 =   0 */
LI              /* a=0 b=0 c=1 -> 优先级 =   1 */
UL LI           /* a=0 b=0 c=2 -> 优先级 =   2 */
UL OL+LI        /* a=0 b=0 c=3 -> 优先级 =   3 */
H1 + *[REL=up]  /* a=0 b=1 c=1 -> 优先级 =  11 */
UL OL LI.red    /* a=0 b=1 c=3 -> 优先级 =  13 */
LI.red.level    /* a=0 b=2 c=1 -> 优先级 =  21 */
#x34y           /* a=1 b=0 c=0 -> 优先级 = 100 */
#s12:not(FOO)   /* a=1 b=0 c=1 -> 优先级 = 101 */

参考链接:
css3选择器w3c标准
css2选择器w3c标准
MDN

2018目标

  1. 记7000高频单词——一天100个,大概三个月
  2. 读书——编程类70% 其它感兴趣的30%
  3. 画画不能丢

书单:

ECMAScript 6 变量解构赋值

为什么解构很有用

ECMAScript 5以及以前的版本:

let options = {
  repeat: true,
  save: false
};
// extract data from the object
let repeat = options.repeat, 
    save = options.save;

虽然这段代码看上去也挺简单的,但想象一下如果你要给大量的变量赋值,你得一个一个的赋值。
或者你需要取一个嵌套结构数据的某个值,也许你得遍历整个解构。
如果你能把数据解构成一些小小的片段,那获取信息将会更加容易。

对象的解构

let node = {
  type: "Identifier",
  name: "foo"
};
let { type, name } = node;
console.log(type);      // "Identifier"
console.log(name);      // "foo"

注意: 必须初始化

// syntax error!
var { type, name };
// syntax error!
let { type, name };
// syntax error!
const { type, name };

解构赋值

可以赋值给已经定义过的变量:

let node = {
    type: "Identifier",
    name: "foo"
},
type = "Literal",
name = 5;
// assign different values using destructuring
({ type, name } = node);
console.log(type);      // "Identifier"
console.log(name);      // "foo"

默认值

let node = {
    type: "Identifier",
    name: "foo"
};
let { type, name, value = true } = node;
console.log(type); // "Identifier"
console.log(name); // "foo"
console.log(value); // true

给不同名本地变量赋值

let node = {
    type: "Identifier",
    name: "foo"
};
let { type: localType, name: localName } = node;
console.log(localType);     // "Identifier"
console.log(localName);     // "foo"

嵌套对象解构

let node = {
    type: "Identifier",
    name: "foo",
    loc: {
        start: {
            line: 1,
            column: 1 
            },
        end: {
            line: 1,
            column: 4
            }
        } 
};
let { loc: { start }} = node;
console.log(start.line); // 1
console.log(start.column); // 1

数组的解构

let colors = [ "red", "green", "blue" ];
let [ firstColor, secondColor ] = colors;
console.log(firstColor);        // "red"
console.log(secondColor);       // "green"

只取你需要的部分

let colors = [ "red", "green", "blue" ];
let [ , , thirdColor ] = colors;
console.log(thirdColor);        // "blue"

注意: 和对象的解构一样,必须初始化

解构赋值

可以赋值给已经定义过的变量:

let colors = [ "red", "green", "blue" ],
    firstColor = "black",
    secondColor = "purple";
[ firstColor, secondColor ] = colors;
console.log(firstColor);        // "red"
console.log(secondColor);       // "green"

在ECMAScript 5 中交换变量值

let a = 1,
    b = 2, tmp;
tmp = a;
a = b;
b = tmp;
console.log(a); // 2
console.log(b); // 1

在ECMAScript 6 中交换变量值

let a = 1,
    b = 2;
[ a, b ] = [ b, a ];
console.log(a);     // 2
console.log(b);     // 1

默认值

let colors = [ "red" ];
let [ firstColor, secondColor = "green" ] = colors;
console.log(firstColor);        // "red"
console.log(secondColor);       // "green"

嵌套数组解构

let colors = [ "red", [ "green", "lightgreen" ], "blue" ];
// later
let [ firstColor, [ secondColor ] ] = colors;
console.log(firstColor);        // "red"
console.log(secondColor);       // "green"

剩余的元素

let colors = [ "red", "green", "blue" ];
let [ firstColor, ...restColors ] = colors;
console.log(firstColor);        // "red"
console.log(restColors.length); // 2
console.log(restColors[0]);     // "green"
console.log(restColors[1]);     // "blue"

数组的第一个值赋给了firstColor,剩下的值组成了一个新的数组赋给了restColors。

ECMAScript 5克隆一个数组:

var colors = [ "red", "green", "blue" ];
var clonedColors = colors.concat();
console.log(clonedColors);   // "[red,green,blue]"

ECMAScript 6克隆一个数组:

let colors = [ "red", "green", "blue" ];
let [ ...clonedColors ] = colors;
console.log(clonedColors);  // "[red,green,blue]"

注意: 剩余的元素必须是解构数组的最后一个元素,后面不能有逗号。

混合解构

对象与数组嵌套混合的解构:

let node = {
    type: "Identifier",
    name: "foo",
    loc: {
        start: {
            line: 1,
column: 1 },
        end: {
            line: 1,
column: 4 }
},
    range: [0, 3]
};
let {
    loc: { start },
    range: [ startIndex ]
} = node;
console.log(start.line); // 1
console.log(start.column); // 1
console.log(startIndex); // 0

参数解构

function setCookie(name, value, { secure, path, domain, expires }) {
    // code to set the cookie
}
setCookie("type", "js", {
    secure: true,
    expires: 60000
});

解构的参数是必需的

// error!
setCookie("type", "js");

它实际上是这样运行的:

function setCookie(name, value, options) {
    let { secure, path, domain, expires } = options;
    // code to set the cookie
}

当解构赋值的右边是null或者undefined,就会抛出错误。

如果你希望解构参数是可选的,你可以这样写:

function setCookie(name, value, { secure, path, domain, expires } = {}) {
    // empty }

解构参数的默认值

function setCookie(name, value,
    {
        secure = false,
        path = "/",
        domain = "example.com",
        expires = new Date(Date.now() + 360000000)
} = {} ){
// empty }

将本地仓库提交到github上

  1. 先在github上新建一个仓库,取名,例test

  2. 本地新建文件夹

  3. 打开命令行工具cd进这个文件夹

  4. 执行git init,执行后会在该文件夹下生成一个名为.git的文件夹,里面有有关git的配置

  5. 关联到远程仓库
    git remote add origin https://[email protected]/YehanZhou/test.git

  6. 添加所有文件到暂存区
    git add .

  7. 提交
    git commit -m "description"

  8. 推送
    git push -u origin master

this的四种用法

在函数执行时,this 总是指向调用该函数的对象。
要判断 this 的指向,其实就是判断 this 所在的函数属于谁。
在《javaScript语言精粹》这本书中,把 this出现的场景分为四类,简单的说就是:
有对象就指向调用对象,没调用对象就指向全局对象,用new构造就指向新对象,
通过 apply 或 call 或 bind 来改变 this 的所指。

1. 函数有所属对象时:指向所属对象

函数有所属对象时,通常通过 . 表达式调用,这时 this自然指向所属对象。比如下面的例子:

var myObject = {value: 100};
myObject.getValue = function () {
 console.log(this.value); // 输出 100
 console.log(this);// 输出 { value: 100, getValue: [Function] },
 // 其实就是 myObject 对象本身
 return this.value;
};
console.log(myObject.getValue()); // => 100
getValue() 属于对象 myObject,并由 myOjbect 进行 . 调用,因此 this 指向对象 myObject。

2. 函数没有所属对象:指向全局对象

var myObject = {value: 100};
myObject.getValue = function () {
 var foo = function () {
  console.log(this);// 输出全局对象 global
  console.log(this.value) // => undefined
 };
 foo();
 return this.value;
};
console.log(myObject.getValue()); // => 100

在上述代码块中,foo 函数虽然定义在 getValue的函数体内,但实际上它既不属于 getValue 也不属于 myObject。foo 并没有被绑定在任何对象上,所以当调用时,它的 this 指针指向了全局对象 global。据说这是个设计错误。

3. 构造器中的 this:指向新对象

js 中,我们通过 new 关键词来调用构造函数,此时 this会绑定在该新对象上。

var SomeClass = function(){
 this.value = 100;
}
var myCreate = new SomeClass();
console.log(myCreate.value); // 输出100

顺便说一句,在 js 中,构造函数、普通函数、对象方法、闭包,这四者没有明确界线。界线都在人的心中。

4. apply 和 call 调用以及 bind 绑定:指向绑定的对象

apply() 方法接受两个参数第一个是函数运行的作用域,另外一个是一个参数数组(arguments)。
call() 方法第一个参数的意义与apply()方法相同,只是其他的参数需要一个个列举出来。
简单来说,call 的方式更接近我们平时调用函数,而 apply 需要我们传递 Array 形式的数组给它。它们是可以互相转换的。

var myObject = {value: 100};
var foo = function(){
 console.log(this);
};
foo(); // 全局变量 global
foo.apply(myObject); // { value: 100 }
foo.call(myObject); // { value: 100 }
var newFoo = foo.bind(myObject);
newFoo(); // { value: 100 }

JS中Math.random()的使用和扩展

Math.random()方法返回大于等于 0 小于 1 的一个随机数。对于某些站点来说,这个方法非常实用,因为可以利用它来随机显示一些名人名言和新闻事件。

在连续整数中取得一个随机数

值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)

例:产生1-10的随机数

var rand1 = Math.floor(Math.random() * 10 + 1);

编写产生startNumber至endNumber随机数的函数

function selectFrom(startNumber, endNumber) {
    var choice = endNumber - startNumber + 1;
    return Math.floor(Math.random() * choice + startNumber)
}
var rand2 = selectFrom(2,8);//产生2至8的随机数

在不相邻整数中取得一个随机数

在不相邻的两个整数中取得一个随机数

例:随机产生2或4中的一个数

var rand3 = Math.random() < 0.5 ? 2 : 4;

在不相邻的多个整数中产生一个随机数

结合函数参数数组,可编写在不相邻的多个整数中产生一个随机值的函数

function selectFromMess() {
    return arguments[Math.floor(Math.random() * arguments.length)]
}
//随机产生1、6、8中的一个数
var rand4 = selectFromMess(1, 6, 8);

//也可随机产生文本
var randomTxt1 = selectFromMess("安慰奖", "二等奖", "一等奖");

每次要输入这么多参数比较麻烦,可以改写一下函数

function selectFromMessArray(arr) {
    return arr[Math.floor(Math.random() * arr.length)]
}
var arrayTxt=["一","二","三","四","五"];
var randTxt2 = selectFromMessArray(arrayTxt);

或者不改变原有方法,可以利用apply()这个方法传递数组参数

var randTxt3 = selectFromMess.apply(null,arrayTxt);

关于apply方法的使用可以看这里

vue2源码DOM Diff详解

参考:

【Vue原理】Diff - 白话版

【Vue原理】Diff - 源码版 之 Diff 流程

精读《DOM diff 原理详解》

Diff - 理论篇

1. Diff 的作用

Diff 的出现,就是为了减少更新量,找到最小差异部分DOM,只更新差异部分DOM就好了

这样消耗就会小一些

数据变化一下,没必要把其他没有涉及的没有变化的DOM 也替换了

2. Diff 的做法

理想的 Dom diff

精确复用所有节点就必须付出高昂的代价:时间复杂度 O(n³) 的 diff 算法

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617786018508-169187e7-a4fe-4069-9b79-eaf7ee156f06.png

Vue 只会对新旧节点中 父节点是相同节点 的 那一层子节点 进行比较

也可以说成是

只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点

最大的根节点一开始可以直接比较

这也叫做 同层级比较

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617786097975-53d2b7e4-6f2b-4b5a-8d41-0e4a412f6c92.png

如图所示,只按层比较,就可以将时间复杂度降低为 O(n)。

这样做确实非常高效,但代价就是,判断的有点傻,比如 ac 明明是一个移动操作,却被误识别为删除 + 新增。

好在跨 DOM 复用在实际业务场景中很少出现,因此这种笨拙出现的频率实际上非常低。

新旧节点(虚拟dom)

所有的 新旧节点 指的都是 Vnode 节点,Vue 只会比较 Vnode 节点,而不是比较 DOM

因为 Vnode 是 JS 对象,不受平台限制,所以以它作为比较基础,代码逻辑后期不需要改动

拿到比较结果后,根据不同平台调用相应的方法进行处理就好了

  • -了解vnode,看虚拟DOM篇

父节点是相同节点是什么意思?

比如下图出现的 四次比较(从 first 到 fouth),他们的共同特点都是有 相同的父节点

比如 蓝色方的比较,新旧子节点的父节点是相同节点 1

比如 红色方的比较,新旧子节点的父节点都是 2

所以他们才有比较的机会

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617717148127-a5445bbf-6656-4d31-bbbc-0169a3e02e52.png

而下图中,只有两次比较,就是因为在 蓝色方 比较中,并没有相同节点,所以不会再进行下级子节点比较

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617717872628-8edc4a3a-158a-402a-9d30-359af875a84f.png

3. Diff 的比较逻辑

Diff 比较的内核是 节点复用,所以 Diff 比较就是为了在 新旧节点中 找到 相同的节点

这个的比较逻辑是建立在上一步说过的同层比较基础之上的

所以说,节点复用,找到相同节点并不是无限制递归查找

比如下图中,的确 旧节点树 和 新节点树 中有相同节点 6,但是它们不在同一层,旧节点6并不会被复用

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617717940663-8e3d2206-6cb4-47a8-bad5-b59147647586.png

就算在同一层级,然而父节点不一样,依旧不会被复用

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718019863-393cdca2-a8d0-4aea-a3a8-a28bf0acef23.png

只有这种情况的节点会被复用,相同父节点 8

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718038544-db2f55c7-c8a7-4c3b-9310-ac820ceb2faa.png

Diff 的比较逻辑的宗旨

  1. 能不移动,尽量不移动
  2. 没得办法,只好移动
  3. 实在不行,新建或删除

比较处理流程

在新旧节点中

  1. 先找到 不需要移动的相同节点,消耗最小
  2. 再找相同但是需要移动的节点,消耗第二小
  3. 最后找不到,才会去新建删除节点,保底处理

比较是为了修改DOM树

其实这里存在 三种树,一个是 页面DOM 树,一个是 旧VNode 树,一个是 新 Vnode 树

页面DOM 树 和 旧VNode 树 节点一一对应的

而 新Vnode 树则是表示更新后 页面DOM 树 该有的样子

这里把 旧Vnode 树 和 新Vnode树 进行比较的过程中

不会对这两棵Vode树进行修改,而是以比较的结果直接对 真实DOM 进行修改

比如说,在 旧 Vnode 树同一层中,找到 和 新Vnode 树 中一样但位置不一样节点

此时需要移动这个节点,但是不是移动 旧 Vnode 树 中的节点

而是 直接移动 DOM

总的来说,新旧 Vnode 树是拿来比较的,页面DOM 树是拿来根据比较结果修改的

4. 简单的例子

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718307046-95323f07-32b2-4434-af98-1c75dda94bb3.png

第一轮比较

因为父节点都是 1,所以开始比较他们的子节点

按照我们上面的比较逻辑,所以先找 相同且不需移动 的点,遂找到 2

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718395384-e14a50aa-ac0f-405b-92e1-3ebe603e1a6c.png

拿到比较结果,这里不用修改DOM,所以 DOM 保留在原地

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718479349-621553f3-a3f7-486b-a0a6-8a32f8959b60.png

第二轮比较

然后,没有 相同且不需移动 的节点 了

只能第二个方案,开始找相同的点

找到 节点5,相同但是位置不同,所以需要移动

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718525746-1a8af79c-d20e-4648-994d-48ce500a8349.png

拿到比较结果,页面 DOM 树需要移动DOM 了,不修改,原样移动

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718556785-e808fb1e-ab32-4f58-8380-d01ad614bbf5.png

第三轮比较

继续,相同节点也没了,没得办法了,只能创建了

所以要根据 新Vnode 中没找到的节点去创建并且插入

然后旧Vnode 中有些节点不存在 新VNode 中,所以要删除

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718633907-bfb3c493-3c78-42e8-bd3c-d8c4a0d2592d.png

于是开始创建节点 6 和 9,并且删除节点 4 和 5

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617718654239-db0217a3-d348-4059-983c-dbc72832f43d.png

5. React 的 Dom diff

React 采用了 仅右移策略

  1. Vue 为了尽量不移动,先左右夹击跳过不变的,再找到最长连续子串保持不动,移动其他元素。
  2. React 采用仅右移方案,在大部分从左往右移的业务场景中,得到了较好的性能。

PS:最新版 React Dom diff 算法可能有更新,因为这种算法看来不如 Vue 的高效。

Diff-源码篇

从 新建实例 到 开始Diff 的流程

function Vue() {
    ... 已省略其他
    new Watcher(function() {
        vm._update(vm._render());
    })
    ... 已省略其他
}

Watcher 的源码

funciton Watcher(expOrFn){
    this.getter = expOrFn;
    this.get();
}

Watcher.prototype.get = function () {
    this.getter()
}

vm._render

生成页面模板对应的 Vnode 树, 比如

<div>
  <span></span>
  {{num}}
</div>

生成的 Vnode 树是( 其中num的值是111 )

{
    tag: "div",
    children:[{
        tag: "span"
    },{
        tag: undefined,
        text: "111"
    }]
}

这一步是通过 compile 生成的, 下次分享讲

vm._update

比较 旧Vnode 树 和 vm._render 生成的新 Vnode 树 进行比较

比较完后,更新页面的DOM,从而完成更新

ok,我们看下源码

Vue.prototype._update = function(vnode) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    if (!prevVnode) {
      	// 不存在旧根节点,直接创建新的DOM树
        vm.$el = vm.__patch__(
            vm.$el, vnode,
            vm.$options._parentElm,
            vm.$options._refElm
        );
    }
    else {
      	// 存在旧节点,比较vnode和新vnode,更新DOM树
        vm.$el = vm.__patch__(
            prevVnode, vnode
        );
    }
};

解释其中几个点

vm._vnode

这个属性保存的就是当前 Vnode 树

当页面开始更新,而生成了新的 Vnode 树之后

这个属性则会替换成新的Vnode

所以保存在这里,是为了方便拿到 旧 Vnode 树

vm.patch

这个东西就是 Diff 的主要内容

var patch = createPatchFunction();
Vue.prototype.__patch__ =  patch ;

createPatchFunction

function createPatchFunction() {
    return function patch(
        oldVnode, vnode, parentElm, refElm
    ) {
        // 没有旧节点,直接生成新节点
        if (!oldVnode) {
            createElm(vnode, parentElm, refElm);
        } else { // 有旧节点
            // 新旧是一样Vnode
            if (sameVnode(oldVnode, vnode)) {
                // 比较他们的子节点
                patchVnode(oldVnode, vnode);
            } else {
                // 新旧是不一样Vnode
                // 替换存在的元素
                var oldElm = oldVnode.elm;
                var _parentElm = oldElm.parentNode
                // 创建新节点
                createElm(vnode, _parentElm, oldElm.nextSibling);
                // 销毁旧节点
                if (_parentElm) {
                    removeVnodes([oldVnode], 0, 0);
                }
            }
        }
        return vnode.elm
    }
}

sameVnode

它的作用是判断两个节点是否相同

这里说的相同,并不是完全一样,而是关键属性一样,可以先看下源码

function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.tag === b.tag &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
    )
}

function sameInputType(a, b) {
    if (a.tag !== 'input') return true
    var i;
    var types = [
        'text','number','password',
        'search','email','tel','url'
    ]
    var typeA = (i = a.data) && (i = i.attrs) && i.type;
    var typeB = (i = b.data) && (i = i.attrs) && i.type;
    // input 的类型一样,或者都属于基本input类型
    return (
        typeA === typeB ||
        types.indexOf(typeA)>-1 &&
        types.indexOf(typeB)>-1
    )
}

判断的依据主要是 三点:key、tag、是否存在 data

这里判断的节点是只是相对于 节点本身,并不包括 children 在内。

也就是说,就算data不一样,children 不一样,两个节点还是可能一样。

有一种特殊情况,就是 input 节点。

input 需要额外判断:

两个节点的 type 是否相同

或者

两个节点的类型可以不同,但是必须属于那些 input 类型。

为什么只判断是否存在 data

因为DOM属性的值是可能是动态绑定动态更新变化的,所以变化前后的 两个 vnode,相应的 data 肯定不一样,但是其实他们是同一个 Vnode,所以 data 不在判断范畴

但是 data 在新旧节点中,必须都定义,或者都不定义

patchVnode

该函数的其中的一个作用是 比较两个Vnode 的子节点

function patchVnode(oldVnode, vnode) {
    if (oldVnode === vnode) return
    var elm = vnode.elm = oldVnode.elm;
    var oldCh = oldVnode.children;
    var ch = vnode.children;
    // 更新children
    if (!vnode.text) {
        // 存在 oldCh 和 ch
        if (oldCh && ch) {
            if (oldCh !== ch)
       					// 新旧节点同时存在,比较它们
                updateChildren(elm, oldCh, ch); // 是 Diff 的核心模块,蕴含着 Diff 的**
        }
        // 存在 newCh 时,oldCh 只能是不存在,如果存在,就跳到上面的条件了
        else if (ch) {
          	// 只有新节点,不存在旧节点,创建出所有新DOM,并且添加进父节点
            if (oldVnode.text) elm.textContent = '';
            for (var i = 0; i <= ch.length - 1; ++i) {
                createElm(
                  ch[i], elm, null
                );
            }
        }
        else if (oldCh) {
          	// 只有旧节点而没有新节点,就是把所有的旧节点删除,也就是直接把DOM 删除
            for (var i = 0; i<= oldCh.length - 1; ++i) {
                oldCh[i].parentNode.removeChild(el);
            }
        }
        else if (oldVnode.text) {
            elm.textContent = '';
        }
    }
  	// Vnode 是文本节点,则更新文本(文本节点不存在子节点)
    else if (oldVnode.text !== vnode.text) {
        elm.textContent = vnode.text;
    }
}

updateChildren

diff核心算法,从两边往中间遍历新旧节点

function updateChildren(parentElm, oldCh, newCh) {
    var oldStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newStartIdx = 0;
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

    // 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (!oldStartVnode) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (!oldEndVnode) {
            oldEndVnode = oldCh[--oldEndIdx];
        }
        //  旧头 和新头 比较
        else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode); // 比较它们的子节点
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        //  旧尾 和新尾 比较
        else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        // 旧头 和 新尾 比较
        else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode);
            // oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndVnode 后面的节点
            parentElm.insertBefore(
                oldStartVnode.elm,
                oldEndVnode.elm.nextSibling
            );
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        //  旧尾 和新头 比较
        else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode);
            // oldEndVnode 放到 oldStartVnode 前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        // 如果不属于以上四种情况,就进行常规的循环比对patch
        else {
            // oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 map
            if (!oldKeyToIdx) {
                oldKeyToIdx = createKeyToOldIdx(
                    oldCh, oldStartIdx, oldEndIdx
                );
            }
            // 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在
            idxInOld = oldKeyToIdx[newStartVnode.key]
            //  新孩子中,存在一个新节点,老节点中没有,需要新建
            if (!idxInOld) {
                //  把  newStartVnode 插入 oldStartVnode 的前面
                createElm(
                    newStartVnode,
                    parentElm,
                    oldStartVnode.elm
                );
            } else {
                //  找到 oldCh 中 和 newStartVnode 一样的节点
                vnodeToMove = oldCh[idxInOld];
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode);
                    // 删除这个 index
                    oldCh[idxInOld] = undefined;
                    // 把 vnodeToMove 移动到  oldStartVnode 前面
                    parentElm.insertBefore(
                        vnodeToMove.elm,
                        oldStartVnode.elm
                    );
                }
                // 只能创建一个新节点插入到 parentElm 的子节点中
                else {
                    // same key but different element. treat as new element
                    createElm(
                        newStartVnode,
                        parentElm,
                        oldStartVnode.elm
                    );
                }
            }
            // 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个
            newStartVnode = newCh[++newStartIdx];
        }
    }
    // 处理剩下的节点
    if (oldStartIdx > oldEndIdx) {
        var newEnd = newCh[newEndIdx + 1]
        refElm = newEnd ? newEnd.elm :null;
        for (; newStartIdx <= newEndIdx; ++newStartIdx) {
            createElm(
               newCh[newStartIdx], parentElm, refElm
            );
        }
    }
    // 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
    else if (newStartIdx > newEndIdx) {
        for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
            oldCh[oldStartIdx].parentNode.removeChild(el);
        }
    }
}

遵循理论篇第三节比较逻辑宗旨:

1.先找一样的不需要移动的

2.再找一样的要移动的

3.都找不到了,则新建删除节点

1. 旧头 == 新头 和 旧尾 == 新尾

用patchVnode比较它们的子节点

更新索引

2. 旧头 == 新尾

以 新子节点的位置 来移动的,旧头 在新子节点的 末尾

所以把 oldStartVnode 的 dom 放到 oldEndVnode 的后面

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617780332437-f3f75851-ba65-48bd-8bcc-958407b3fba7.png

3. 旧尾 == 新头

把 oldEndVnode DOM 直接放到 当前 oldStartVnode.elm 的前面

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617780451528-959a359d-6c80-4724-8230-ad6ed17f8981.png

4. 单个遍历查找

1. 生成map表

这个map 表的作用,就主要是判断存在什么旧子节点

比如你的旧子节点数组是

[{
    tag:"div",  key:1
},{
    tag:"strong", key:2
},{
    tag:"span",  key:4
}]

经过 createKeyToOldIdx 生成一个 map 表 oldKeyToIdx

oldKeyToIdx = {
    1:0,
    2:1,
    4:2
}

2. 判断 新子节点是否存在旧子节点数组中

oldKeyToIdx[newStartVnode.key]

不存在旧子节点数组: 直接创建DOM,并插入oldStartVnode 前面

createElm(newStartVnode, parentElm, oldStartVnode.elm);

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617781549551-76c9d5dc-c230-440c-8985-1fa3bd997a87.png

存在旧子节点数组中:

找到这个旧子节点,然后判断和新子节点是否 sameVnode

如果相同,直接移动到 oldStartVnode 前面

如果不同,直接创建插入 oldStartVnode 前面

5. 处理可能剩下的节点

在updateChildren 中,比较完新旧两个数组之后,可能某个数组会剩下部分节点没有被处理过,所以这里需要统一处理

1. 新子节点遍历完了

newStartIdx > newEndIdx

新子节点遍历完毕,旧子节点可能还有剩

所以我们要对可能剩下的旧节点进行 批量删除!

就是遍历剩下的节点,逐个删除DOM

for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
    oldCh[oldStartIdx]
    .parentNode
    .removeChild(el);
}

2. 旧子节点遍历完了

oldStartIdx > oldEndIdx

旧子节点遍历完毕,新子节点可能有剩

所以要对剩余的新子节点处理

很明显,剩余的新子节点不存在 旧子节点中,所以全部新建

但是新建有一个问题,就是插在哪里?

来看下refElm的代码:

var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;

refElm 获取的是 newEndIdx 后一位的节点

当前没有处理的节点是 newEndIdx

也就是说 newEndIdx+1 的节点如果存在的话,肯定被处理过了

如果 newEndIdx 没有移动过,一直是最后一位,那么就不存在 newCh[newEndIdx + 1]

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785572835-1663485a-9594-47c2-b608-2ce10adc0933.png

例子

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785716147-933c08b0-bc52-4616-a671-357fae83cf73.png

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785724634-fbd69716-d1dc-472e-8e9a-a68eec6b687a.png

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785735608-9cb89a32-0616-41d8-bcc8-46e3736314b4.png

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785745876-eba772c0-eb60-4103-906c-ba2fc8f126a5.png

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785757595-fb6a44f4-90b1-4a13-ae08-038ca8210fd2.png

https://cdn.nlark.com/yuque/0/2021/png/2384682/1617785770101-29683901-7382-4185-82b0-0541974fd5dc.png

newStartIdx++ while循环结束

newStartIdx > newEndIdx

剩余旧节点4,将其删除

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.